Two changes addressing both halves of #958: 1. IPv6 highlighting The built-in 'URL, IP & MAC' rule only shipped URL, IPv4 and MAC patterns, so compressed IPv6 addresses such as 2001:11:22:33::5 or fe80::d2dd:bff:fe79:f2bb were never highlighted. Add an IPv6 regex covering full and compressed forms (including ::1 and leading-/trailing- :: variants) and merge it into the same 'ip-mac' rule's patterns. The normalizer's existing "fill missing defaults" path means existing users pick this up on next start with no migration step. 2. Editable built-in rules Add an optional `customized` flag to KeywordHighlightRule. When false / absent, normalize re-syncs the rule's label/patterns with the shipped defaults (so future default-pattern upgrades reach users automatically). When true, normalize keeps the user's label/patterns/color/enabled verbatim, allowing built-ins like 'ip-mac' to be tailored. SettingsTerminalTab: - Pencil icon now appears on built-ins too. Editing one routes through the same dialog and flips `customized` on save. - The pattern field becomes a Textarea so multi-pattern built-ins (e.g. 'error' ships seven spellings) can all be edited in one go. - A per-rule "↺" reset icon appears on customized built-ins and restores the shipped label/patterns while preserving the user's color/enabled. - The footer's "Reset to default colors" button is broadened into "Reset built-ins to defaults", restoring every built-in to shipped label/patterns/color and clearing `customized`. Tests: New domain/keywordHighlight.test.ts (6 tests) covers IPv6 matches for both #958 examples plus loopback and full-form, IPv4/MAC still match, normalize migrates legacy non-customized 'ip-mac' to include IPv6, normalize preserves customized patterns, and normalize keeps user custom rules verbatim. Full suite: 808/0/3. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -358,12 +358,16 @@ const en: Messages = {
|
||||
'settings.terminal.scrollback.rows': 'Number of rows *',
|
||||
'settings.terminal.keywordHighlight.title': 'Keyword highlighting',
|
||||
'settings.terminal.keywordHighlight.resetColors': 'Reset to default colors',
|
||||
'settings.terminal.keywordHighlight.resetDefaults': 'Reset built-ins to defaults',
|
||||
'settings.terminal.keywordHighlight.resetBuiltIn': 'Restore default label and patterns',
|
||||
'settings.terminal.keywordHighlight.addCustom': 'Add Custom Rule',
|
||||
'settings.terminal.keywordHighlight.editCustom': 'Edit Rule',
|
||||
'settings.terminal.keywordHighlight.editBuiltIn': 'Edit Built-in Rule',
|
||||
'settings.terminal.keywordHighlight.labelField': 'Label & Color',
|
||||
'settings.terminal.keywordHighlight.labelPlaceholder': 'Label (e.g., Down)',
|
||||
'settings.terminal.keywordHighlight.patternField': 'Regex Pattern',
|
||||
'settings.terminal.keywordHighlight.patternPlaceholder': 'Regex (e.g., \\bdown\\b)',
|
||||
'settings.terminal.keywordHighlight.patternField': 'Regex Patterns',
|
||||
'settings.terminal.keywordHighlight.patternPlaceholder': 'One regex per line (e.g., \\bdown\\b)',
|
||||
'settings.terminal.keywordHighlight.patternHint': 'One regex per line. Patterns are matched case-insensitively with the global flag.',
|
||||
'settings.terminal.keywordHighlight.invalidPattern': 'Invalid regex pattern',
|
||||
'settings.terminal.keywordHighlight.preview': 'Preview',
|
||||
'settings.terminal.section.localShell': 'Local Shell',
|
||||
|
||||
@@ -1490,12 +1490,16 @@ const zhCN: Messages = {
|
||||
'settings.terminal.scrollback.rows': '行数 *',
|
||||
'settings.terminal.keywordHighlight.title': '关键字高亮',
|
||||
'settings.terminal.keywordHighlight.resetColors': '重置为默认颜色',
|
||||
'settings.terminal.keywordHighlight.resetDefaults': '把内置规则恢复为默认',
|
||||
'settings.terminal.keywordHighlight.resetBuiltIn': '恢复内置标签与正则',
|
||||
'settings.terminal.keywordHighlight.addCustom': '添加自定义规则',
|
||||
'settings.terminal.keywordHighlight.editCustom': '编辑规则',
|
||||
'settings.terminal.keywordHighlight.editBuiltIn': '编辑内置规则',
|
||||
'settings.terminal.keywordHighlight.labelField': '标签与颜色',
|
||||
'settings.terminal.keywordHighlight.labelPlaceholder': '标签(如 Down)',
|
||||
'settings.terminal.keywordHighlight.patternField': '正则表达式',
|
||||
'settings.terminal.keywordHighlight.patternPlaceholder': '正则表达式(如 \\bdown\\b)',
|
||||
'settings.terminal.keywordHighlight.patternPlaceholder': '每行一个正则(如 \\bdown\\b)',
|
||||
'settings.terminal.keywordHighlight.patternHint': '每行一个正则。匹配忽略大小写,全局匹配。',
|
||||
'settings.terminal.keywordHighlight.invalidPattern': '无效的正则表达式',
|
||||
'settings.terminal.keywordHighlight.preview': '预览',
|
||||
'settings.terminal.section.localShell': '本地 Shell',
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Button } from "../../ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../../ui/dialog";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Label } from "../../ui/label";
|
||||
import { Textarea } from "../../ui/textarea";
|
||||
import { Select as ShadcnSelect, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/select";
|
||||
import { SectionHeader, Select, SettingsTabContent, SettingRow, Toggle } from "../settings-ui";
|
||||
import { ThemeSelectModal } from "../ThemeSelectModal";
|
||||
@@ -34,21 +35,25 @@ const AddCustomRuleDialog: React.FC<{
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editRule?: KeywordHighlightRule | null;
|
||||
isBuiltIn?: boolean;
|
||||
onAdd: (rule: KeywordHighlightRule) => void;
|
||||
}> = ({ open, onOpenChange, editRule, onAdd }) => {
|
||||
}> = ({ open, onOpenChange, editRule, isBuiltIn = false, onAdd }) => {
|
||||
const { t } = useI18n();
|
||||
const [label, setLabel] = useState('');
|
||||
const [pattern, setPattern] = useState('');
|
||||
// Multi-line text: one regex pattern per line. Built-in rules typically
|
||||
// ship multiple patterns (e.g. several spellings of "error"), and the user
|
||||
// is allowed to add as many as they like.
|
||||
const [patternsText, setPatternsText] = useState('');
|
||||
const [color, setColor] = useState(DEFAULT_NEW_RULE_COLOR);
|
||||
const [patternError, setPatternError] = useState<string | null>(null);
|
||||
|
||||
const reset = () => { setLabel(''); setPattern(''); setColor(DEFAULT_NEW_RULE_COLOR); setPatternError(null); };
|
||||
const reset = () => { setLabel(''); setPatternsText(''); setColor(DEFAULT_NEW_RULE_COLOR); setPatternError(null); };
|
||||
|
||||
// Populate form when editing
|
||||
useEffect(() => {
|
||||
if (open && editRule) {
|
||||
setLabel(editRule.label);
|
||||
setPattern(editRule.patterns[0] || '');
|
||||
setPatternsText(editRule.patterns.join('\n'));
|
||||
setColor(editRule.color);
|
||||
setPatternError(null);
|
||||
} else if (!open) {
|
||||
@@ -57,25 +62,43 @@ const AddCustomRuleDialog: React.FC<{
|
||||
}, [open, editRule]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!label.trim() || !pattern.trim()) return;
|
||||
try { new RegExp(pattern, 'gi'); } catch {
|
||||
setPatternError(t('settings.terminal.keywordHighlight.invalidPattern'));
|
||||
return;
|
||||
if (!label.trim()) return;
|
||||
const patterns = patternsText
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
if (patterns.length === 0) return;
|
||||
for (const p of patterns) {
|
||||
try { new RegExp(p, 'gi'); } catch {
|
||||
setPatternError(t('settings.terminal.keywordHighlight.invalidPattern'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// When editing, replace only the first pattern and keep any additional ones
|
||||
const patterns = editRule
|
||||
? [pattern, ...editRule.patterns.slice(1)]
|
||||
: [pattern];
|
||||
onAdd({ id: editRule?.id ?? crypto.randomUUID(), label: label.trim(), patterns, color, enabled: editRule?.enabled ?? true });
|
||||
onAdd({
|
||||
id: editRule?.id ?? crypto.randomUUID(),
|
||||
label: label.trim(),
|
||||
patterns,
|
||||
color,
|
||||
enabled: editRule?.enabled ?? true,
|
||||
// Editing a built-in rule flips it into "user-customized" mode so the
|
||||
// normalizer keeps the user's patterns across restarts.
|
||||
customized: isBuiltIn ? true : editRule?.customized,
|
||||
});
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const dialogTitleKey = editRule
|
||||
? (isBuiltIn
|
||||
? 'settings.terminal.keywordHighlight.editBuiltIn'
|
||||
: 'settings.terminal.keywordHighlight.editCustom')
|
||||
: 'settings.terminal.keywordHighlight.addCustom';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => { if (!v) reset(); onOpenChange(v); }}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogContent className="sm:max-w-[440px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editRule ? t('settings.terminal.keywordHighlight.editCustom') : t('settings.terminal.keywordHighlight.addCustom')}</DialogTitle>
|
||||
<DialogTitle>{t(dialogTitleKey)}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div className="space-y-1.5">
|
||||
@@ -95,16 +118,19 @@ const AddCustomRuleDialog: React.FC<{
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">{t('settings.terminal.keywordHighlight.patternField')}</Label>
|
||||
<Input
|
||||
<Textarea
|
||||
placeholder={t('settings.terminal.keywordHighlight.patternPlaceholder')}
|
||||
value={pattern}
|
||||
onChange={(e) => { setPattern(e.target.value); if (patternError) setPatternError(null); }}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }}
|
||||
className={cn("font-mono", patternError && "border-destructive")}
|
||||
value={patternsText}
|
||||
onChange={(e) => { setPatternsText(e.target.value); if (patternError) setPatternError(null); }}
|
||||
rows={Math.max(3, Math.min(10, patternsText.split('\n').length + 1))}
|
||||
className={cn("font-mono text-xs", patternError && "border-destructive")}
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{t('settings.terminal.keywordHighlight.patternHint')}
|
||||
</p>
|
||||
{patternError && <div className="text-xs text-destructive">{patternError}</div>}
|
||||
</div>
|
||||
{label.trim() && pattern.trim() && !patternError && (
|
||||
{label.trim() && patternsText.trim() && !patternError && (
|
||||
<div className="flex items-center gap-2 p-2 rounded-md bg-muted/50">
|
||||
<span className="text-xs text-muted-foreground">{t('settings.terminal.keywordHighlight.preview')}:</span>
|
||||
<span className="text-sm font-medium" style={{ color }}>{label}</span>
|
||||
@@ -113,7 +139,7 @@ const AddCustomRuleDialog: React.FC<{
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => { reset(); onOpenChange(false); }}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSubmit} disabled={!label.trim() || !pattern.trim()}>{editRule ? t('common.save') : t('common.add')}</Button>
|
||||
<Button onClick={handleSubmit} disabled={!label.trim() || !patternsText.trim()}>{editRule ? t('common.save') : t('common.add')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -133,26 +159,43 @@ const KeywordHighlightRulesEditor: React.FC<{
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
{rules.map((rule) => {
|
||||
const custom = !isBuiltIn(rule.id);
|
||||
const builtIn = isBuiltIn(rule.id);
|
||||
const customized = builtIn && rule.customized;
|
||||
return (
|
||||
<div key={rule.id} className="flex items-center gap-2 group">
|
||||
<div className="flex-1 min-w-0 flex items-center gap-1.5">
|
||||
<span className={cn("text-sm truncate", !rule.enabled && "text-muted-foreground line-through")} style={rule.enabled ? { color: rule.color } : undefined}>
|
||||
{rule.label}
|
||||
</span>
|
||||
{custom && (
|
||||
<>
|
||||
<Pencil
|
||||
size={10}
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer"
|
||||
onClick={() => { setEditingRule(rule); setAddDialogOpen(true); }}
|
||||
/>
|
||||
<Trash2
|
||||
size={10}
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer"
|
||||
onClick={() => onChange(rules.filter((r) => r.id !== rule.id))}
|
||||
/>
|
||||
</>
|
||||
<Pencil
|
||||
size={10}
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer hover:text-foreground"
|
||||
onClick={() => { setEditingRule(rule); setAddDialogOpen(true); }}
|
||||
/>
|
||||
{!builtIn && (
|
||||
<Trash2
|
||||
size={10}
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer hover:text-destructive"
|
||||
onClick={() => onChange(rules.filter((r) => r.id !== rule.id))}
|
||||
/>
|
||||
)}
|
||||
{customized && (
|
||||
<RotateCcw
|
||||
size={10}
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer hover:text-foreground"
|
||||
aria-label={t('settings.terminal.keywordHighlight.resetBuiltIn')}
|
||||
onClick={() => {
|
||||
// Drop the user's customizations and restore the shipped
|
||||
// defaults for label/patterns. Color stays whatever the
|
||||
// user picked (color is the only built-in property they
|
||||
// can edit without flipping `customized`).
|
||||
const def = DEFAULT_KEYWORD_HIGHLIGHT_RULES.find((r) => r.id === rule.id);
|
||||
if (!def) return;
|
||||
onChange(rules.map((r) => r.id === rule.id
|
||||
? { ...def, color: r.color, enabled: r.enabled, customized: false }
|
||||
: r));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<label className="relative flex-shrink-0">
|
||||
@@ -186,14 +229,18 @@ const KeywordHighlightRulesEditor: React.FC<{
|
||||
size="sm"
|
||||
className="flex-1 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
// Restore every built-in rule back to shipped defaults
|
||||
// (label/patterns/color), drop customizations, and keep the user's
|
||||
// custom rules untouched.
|
||||
onChange(rules.map((rule) => {
|
||||
const def = DEFAULT_KEYWORD_HIGHLIGHT_RULES.find((r) => r.id === rule.id);
|
||||
return def ? { ...rule, color: def.color } : rule;
|
||||
if (!def) return rule;
|
||||
return { ...def, enabled: rule.enabled, customized: false };
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<RotateCcw size={14} className="mr-1.5" />
|
||||
{t("settings.terminal.keywordHighlight.resetColors")}
|
||||
{t("settings.terminal.keywordHighlight.resetDefaults")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -201,6 +248,7 @@ const KeywordHighlightRulesEditor: React.FC<{
|
||||
open={addDialogOpen}
|
||||
onOpenChange={(v) => { setAddDialogOpen(v); if (!v) setEditingRule(null); }}
|
||||
editRule={editingRule}
|
||||
isBuiltIn={editingRule ? isBuiltIn(editingRule.id) : false}
|
||||
onAdd={(rule) => {
|
||||
if (editingRule) {
|
||||
onChange(rules.map((r) => r.id === editingRule.id ? rule : r));
|
||||
|
||||
151
domain/keywordHighlight.test.ts
Normal file
151
domain/keywordHighlight.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
DEFAULT_KEYWORD_HIGHLIGHT_RULES,
|
||||
KeywordHighlightRule,
|
||||
normalizeTerminalSettings,
|
||||
} from "./models";
|
||||
|
||||
const IP_MAC_RULE = "ip-mac";
|
||||
|
||||
const ipMacDefault = () => {
|
||||
const def = DEFAULT_KEYWORD_HIGHLIGHT_RULES.find((r) => r.id === IP_MAC_RULE);
|
||||
if (!def) throw new Error("ip-mac default rule missing");
|
||||
return def;
|
||||
};
|
||||
|
||||
const getRule = (
|
||||
rules: KeywordHighlightRule[],
|
||||
id: string,
|
||||
): KeywordHighlightRule => {
|
||||
const rule = rules.find((r) => r.id === id);
|
||||
if (!rule) throw new Error(`rule ${id} missing`);
|
||||
return rule;
|
||||
};
|
||||
|
||||
const matchesAny = (patterns: string[], input: string): boolean =>
|
||||
patterns.some((p) => new RegExp(p, "gi").test(input));
|
||||
|
||||
test("ip-mac built-in rule includes IPv6 patterns by default", () => {
|
||||
const def = ipMacDefault();
|
||||
// Compressed mid-form (issue #958 example #1)
|
||||
assert.ok(
|
||||
matchesAny(def.patterns, "2001:11:22:33::5"),
|
||||
"expected default ip-mac rule to match 2001:11:22:33::5",
|
||||
);
|
||||
// Link-local compressed (issue #958 example #2)
|
||||
assert.ok(
|
||||
matchesAny(def.patterns, "fe80::d2dd:bff:fe79:f2bb"),
|
||||
"expected default ip-mac rule to match fe80::d2dd:bff:fe79:f2bb",
|
||||
);
|
||||
// Loopback
|
||||
assert.ok(matchesAny(def.patterns, "::1"), "expected ::1 to match");
|
||||
// Full form
|
||||
assert.ok(
|
||||
matchesAny(def.patterns, "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
||||
"expected full-form IPv6 to match",
|
||||
);
|
||||
});
|
||||
|
||||
test("ip-mac IPv6 regex still matches IPv4 and MAC", () => {
|
||||
const def = ipMacDefault();
|
||||
assert.ok(matchesAny(def.patterns, "10.0.0.1"), "expected IPv4 still matches");
|
||||
assert.ok(
|
||||
matchesAny(def.patterns, "aa:bb:cc:dd:ee:ff"),
|
||||
"expected MAC still matches",
|
||||
);
|
||||
});
|
||||
|
||||
test("ip-mac IPv6 regex does not match obviously-not-IPv6 hex blobs", () => {
|
||||
const def = ipMacDefault();
|
||||
// A single hex word without colons must not match
|
||||
assert.ok(!matchesAny(def.patterns, "deadbeef"), "single hex word matched");
|
||||
// A typical sha-like string with colons separating fewer than two groups
|
||||
assert.ok(!matchesAny(def.patterns, "abc"), "stray hex matched");
|
||||
});
|
||||
|
||||
test("normalize adds newly-shipped default rules to legacy saved sets", () => {
|
||||
// Simulate an older save that only has 'error' and an old-shape 'ip-mac'
|
||||
// (i.e. without IPv6). Because the rule is NOT marked customized, normalize
|
||||
// should re-sync it with the latest shipped patterns.
|
||||
const legacyIpMacPatterns = ["legacy-pattern-from-old-default"];
|
||||
const saved: KeywordHighlightRule[] = [
|
||||
{
|
||||
id: "error",
|
||||
label: "Error",
|
||||
patterns: ["\\berror\\b"],
|
||||
color: "#F87171",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: IP_MAC_RULE,
|
||||
label: "URL, IP & MAC",
|
||||
patterns: legacyIpMacPatterns,
|
||||
color: "#EC4899",
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const settings = normalizeTerminalSettings({
|
||||
keywordHighlightRules: saved,
|
||||
});
|
||||
const rules = settings.keywordHighlightRules;
|
||||
|
||||
// Every shipped default exists (warning/ok/info/debug get added).
|
||||
for (const def of DEFAULT_KEYWORD_HIGHLIGHT_RULES) {
|
||||
assert.ok(
|
||||
rules.some((r) => r.id === def.id),
|
||||
`expected normalize to include shipped rule ${def.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ip-mac was not customized → patterns re-sync to defaults, picking up IPv6.
|
||||
const ipMac = getRule(rules, IP_MAC_RULE);
|
||||
assert.deepEqual(ipMac.patterns, ipMacDefault().patterns);
|
||||
assert.ok(matchesAny(ipMac.patterns, "2001:11:22:33::5"));
|
||||
});
|
||||
|
||||
test("normalize preserves user-edited patterns when rule.customized is set", () => {
|
||||
const customPatterns = ["\\bMY_CUSTOM\\b", "\\bANOTHER\\b"];
|
||||
const customLabel = "My Errors";
|
||||
const saved: KeywordHighlightRule[] = [
|
||||
{
|
||||
id: "error",
|
||||
label: customLabel,
|
||||
patterns: customPatterns,
|
||||
color: "#FF0000",
|
||||
enabled: false,
|
||||
customized: true,
|
||||
},
|
||||
];
|
||||
|
||||
const settings = normalizeTerminalSettings({
|
||||
keywordHighlightRules: saved,
|
||||
});
|
||||
const rule = getRule(settings.keywordHighlightRules, "error");
|
||||
|
||||
assert.equal(rule.label, customLabel);
|
||||
assert.deepEqual(rule.patterns, customPatterns);
|
||||
assert.equal(rule.color, "#FF0000");
|
||||
assert.equal(rule.enabled, false);
|
||||
assert.equal(rule.customized, true);
|
||||
});
|
||||
|
||||
test("normalize keeps custom (non-built-in) rules verbatim", () => {
|
||||
const customRule: KeywordHighlightRule = {
|
||||
id: "user-uuid-1",
|
||||
label: "Pager",
|
||||
patterns: ["\\b[A-Z]{3}-\\d+\\b"],
|
||||
color: "#00FF00",
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const settings = normalizeTerminalSettings({
|
||||
keywordHighlightRules: [customRule],
|
||||
});
|
||||
|
||||
const rule = getRule(settings.keywordHighlightRules, "user-uuid-1");
|
||||
assert.deepEqual(rule.patterns, customRule.patterns);
|
||||
assert.equal(rule.label, customRule.label);
|
||||
});
|
||||
@@ -464,6 +464,11 @@ export interface KeywordHighlightRule {
|
||||
patterns: string[]; // Regex patterns to match
|
||||
color: string; // Highlight color (hex)
|
||||
enabled: boolean;
|
||||
// Set to true when the user edits a built-in rule's label/patterns so
|
||||
// normalize keeps the user-edited values instead of overwriting them with
|
||||
// the latest shipped defaults. Absent / false means "still tracking defaults"
|
||||
// and the rule picks up new built-in patterns added in later versions.
|
||||
customized?: boolean;
|
||||
}
|
||||
|
||||
export interface TerminalSettings {
|
||||
@@ -560,6 +565,24 @@ const URL_HIGHLIGHT_PATTERN =
|
||||
"(?:\\bhttps?:\\/\\/\\[[0-9A-Fa-f:.]+\\](?::\\d+)?(?:[/?#][^\\s<>\"'`]*)?(?<![.,;:!?\\)}])|\\b(?:https?:\\/\\/|www\\.)[^\\s<>\"'`]+(?<![.,;:!?\\])}]))";
|
||||
const IPV4_HIGHLIGHT_PATTERN =
|
||||
`(?<![\\w.])(?<!\\bver\\s)(?<!\\bversion\\s)(?:${STRICT_IPV4_OCTET_PATTERN}\\.){3}${STRICT_IPV4_OCTET_PATTERN}(?![\\w.])`;
|
||||
// Covers full and compressed forms (1:2:3:4:5:6:7:8, fe80::1, ::1, 2001:db8::,
|
||||
// etc.). Bracketed `[…]:port` URLs are matched by URL_HIGHLIGHT_PATTERN.
|
||||
// Zone IDs (%eth0) and IPv4-mapped (::ffff:192.0.2.1) are intentionally out
|
||||
// of scope here — add them as custom patterns if you need them.
|
||||
const IPV6_HIGHLIGHT_PATTERN =
|
||||
'(?<![\\w:.])' +
|
||||
'(?:' +
|
||||
'(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}' +
|
||||
'|(?:[0-9A-Fa-f]{1,4}:){1,7}:' +
|
||||
'|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}' +
|
||||
'|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}' +
|
||||
'|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}' +
|
||||
'|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}' +
|
||||
'|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}' +
|
||||
'|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}' +
|
||||
'|::(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}' +
|
||||
')' +
|
||||
'(?![\\w:.])';
|
||||
const MAC_ADDRESS_HIGHLIGHT_PATTERN =
|
||||
'\\b([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\\b';
|
||||
|
||||
@@ -569,7 +592,7 @@ export const DEFAULT_KEYWORD_HIGHLIGHT_RULES: KeywordHighlightRule[] = [
|
||||
{ id: 'ok', label: 'OK', patterns: ['\\[ok\\]', '\\bok\\b', '\\bsuccess(ful)?\\b', '\\bpassed\\b', '\\bcompleted\\b', '\\bdone\\b'], color: '#34D399', enabled: true },
|
||||
{ id: 'info', label: 'Info', patterns: ['\\[info\\]', '\\[notice\\]', '\\[note\\]', '\\bnotice\\b', '\\bnote\\b'], color: '#3B82F6', enabled: true },
|
||||
{ id: 'debug', label: 'Debug', patterns: ['\\[debug\\]', '\\[trace\\]', '\\[verbose\\]', '\\bdebug\\b', '\\btrace\\b', '\\bverbose\\b'], color: '#A78BFA', enabled: true },
|
||||
{ id: 'ip-mac', label: 'URL, IP & MAC', patterns: [URL_HIGHLIGHT_PATTERN, IPV4_HIGHLIGHT_PATTERN, MAC_ADDRESS_HIGHLIGHT_PATTERN], color: '#EC4899', enabled: true },
|
||||
{ id: 'ip-mac', label: 'URL, IP & MAC', patterns: [URL_HIGHLIGHT_PATTERN, IPV4_HIGHLIGHT_PATTERN, IPV6_HIGHLIGHT_PATTERN, MAC_ADDRESS_HIGHLIGHT_PATTERN], color: '#EC4899', enabled: true },
|
||||
];
|
||||
|
||||
const cloneKeywordHighlightRule = (rule: KeywordHighlightRule): KeywordHighlightRule => ({
|
||||
@@ -594,6 +617,21 @@ const normalizeKeywordHighlightRules = (
|
||||
return cloneKeywordHighlightRule(rule);
|
||||
}
|
||||
|
||||
// A built-in rule the user has explicitly edited keeps its label/patterns;
|
||||
// otherwise we re-sync with the latest defaults so newly shipped patterns
|
||||
// (e.g. the IPv6 entry in `ip-mac`) propagate to existing users without
|
||||
// a manual reset.
|
||||
if (rule.customized) {
|
||||
return {
|
||||
...defaultRule,
|
||||
label: rule.label,
|
||||
patterns: [...rule.patterns],
|
||||
color: rule.color,
|
||||
enabled: rule.enabled,
|
||||
customized: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultRule,
|
||||
color: rule.color,
|
||||
|
||||
Reference in New Issue
Block a user