关键字高亮设置改成可直接编辑的规则表,总开关默认关闭

- 设置页分三组:常规(启用关键字高亮,默认关闭)、预览(当前终端字体和
  配色下的一段示例日志)、规则(N)。
- 规则表在行里直接改,不再弹对话框:拖动把手排序、启用、正则表达式、
  备注、颜色(取色器加 #rrggbb 输入框)、通知、删除。规则都是正则,区分
  大小写;颜色可任意取,不跟主题;去掉加粗。通知标题用备注。
- 预置 ERROR、WARN、IPv4 三条示例,各自启用、不通知,打开总开关就能用。
- 取色器自带一行与调色板不重复的常用色:gpui-kit 默认的常用色也在调色板
  里,同色两格是重复的无障碍节点,调试版会因此崩溃。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-06 19:46:18 +08:00
1 parent 7c9501e4cb
commit 2aa5d5be49
22 files changed
+1121 -968

No files matched your search

+163 -105
View File
@@ -318,70 +318,12 @@ fn the_appearance_setting_drives_the_theme_and_the_title_bar_switch(cx: &mut Tes
.unwrap();
}
/// The patterns of the 关键字高亮 rules, in order.
fn highlight_patterns(workspace: &Entity<Workspace>, cx: &App) -> Vec<String> {
workspace
.read(cx)
.settings()
.read(cx)
.settings()
.terminal_highlight
.rules
.iter()
.map(|rule| rule.pattern.clone())
.collect()
}
#[gpui_kit::test]
async fn highlight_rules_are_added_edited_and_deleted_on_their_page(cx: &mut TestAppContext) {
use shellrs::terminal::{HighlightColor, HighlightRule, PatternKind, TerminalHighlights};
let (handle, workspace) = open_workspace(cx);
in_frame(cx, handle, |window, cx| window.click("open-settings", cx));
// 关键字高亮 comes right after 终端, with the examples a new
// installation starts with.
in_frame(cx, handle, |window, cx| {
window.within("settings").click("0-2", cx)
});
in_frame(cx, handle, |window, _| {
assert!(window.find(("highlight-rule", 2usize)).visible());
assert!(window.try_find(("highlight-rule", 3usize)).is_none());
});
let examples = [
"ERROR".to_string(),
"WARN".into(),
r"\b\d{1,3}(\.\d{1,3}){3}\b".into(),
];
assert_eq!(cx.update(|cx| highlight_patterns(&workspace, cx)), examples);
// A new rule takes the keyboard at once; a regex that does not compile
// is refused, the dialog staying open.
in_frame(cx, handle, |window, cx| {
window.click("add-highlight-rule", cx)
});
in_frame(cx, handle, |window, cx| {
assert_eq!(window.find("highlight-pattern").focused(), Some(true));
window.input("OutOf(Memory", cx);
window.within("highlight-kind").click(1usize, cx);
window.click("commit", cx);
});
cx.wait_for(handle.into(), Duration::from_secs(2), |window, _| {
window
.try_find("form-error")
.is_some_and(|error| error.label() == Some("正则表达式写法有误"))
})
.await;
in_frame(cx, handle, |window, cx| {
window.click("highlight-pattern", cx);
window.press("cmd-a", cx);
window.input(r"OutOfMemory\w*", cx);
window.within("highlight-color").click(5usize, cx);
window.click("highlight-bold", cx);
window.click("highlight-notify", cx);
window.click("commit", cx);
});
cx.run_until_parked();
let rules = cx.update(|cx| {
/// The 关键字高亮 rules, as the settings hold them.
fn highlight_rules(
workspace: &Entity<Workspace>,
cx: &mut TestAppContext,
) -> Vec<shellrs::terminal::HighlightRule> {
cx.update(|cx| {
workspace
.read(cx)
.settings()
@@ -389,50 +331,166 @@ async fn highlight_rules_are_added_edited_and_deleted_on_their_page(cx: &mut Tes
.settings()
.terminal_highlight
.rules
});
assert_eq!(
rules.last(),
Some(&HighlightRule {
pattern: r"OutOfMemory\w*".into(),
kind: PatternKind::Regex,
color: HighlightColor::Magenta,
bold: true,
notify: true,
})
);
})
}
// With the dialog closed nothing has focus, and the row's button still
// reaches the workspace.
in_frame(cx, handle, |window, cx| {
window.click(("edit-highlight-rule", 3usize), cx)
});
in_frame(cx, handle, |window, cx| {
assert_eq!(window.find("highlight-pattern").focused(), Some(true));
window.press("cmd-a", cx);
window.input("OOM", cx);
window.within("highlight-kind").click(0usize, cx);
window.click("commit", cx);
});
cx.run_until_parked();
assert_eq!(cx.update(|cx| highlight_patterns(&workspace, cx))[3], "OOM");
fn patterns(rules: &[shellrs::terminal::HighlightRule]) -> Vec<&str> {
rules.iter().map(|rule| rule.pattern.as_str()).collect()
}
// Deleted once confirmed; the terminals follow.
/// Scroll the settings page down to the rules, below the window's fold.
fn scroll_to_highlight_rules(cx: &mut TestAppContext, handle: WindowHandle<Root>) {
in_frame(cx, handle, |window, cx| {
window.click(("delete-highlight-rule", 0usize), cx)
});
in_frame(cx, handle, |window, cx| window.click("ok", cx));
cx.run_until_parked();
cx.update(|cx| {
let patterns = highlight_patterns(&workspace, cx);
assert_eq!(
patterns,
[examples[1].clone(), examples[2].clone(), "OOM".into()]
let position = window.find("highlight-preview").bounds().center();
window.dispatch_event(
gpui_kit::ScrollWheelEvent {
position,
delta: gpui_kit::ScrollDelta::Pixels(point(px(0.), px(-600.))),
modifiers: gpui_kit::Modifiers::default(),
touch_phase: gpui_kit::TouchPhase::Moved,
}
.to_platform_input(),
cx,
);
});
}
#[gpui_kit::test]
async fn highlight_rules_are_edited_in_place_on_their_page(cx: &mut TestAppContext) {
use shellrs::terminal::{HighlightColor, TerminalHighlights};
let (handle, workspace) = open_workspace(cx);
in_frame(cx, handle, |window, cx| window.click("open-settings", cx));
// 关键字高亮 comes right after 终端, with the examples a new
// installation starts with: rows 0 to 2.
in_frame(cx, handle, |window, cx| {
window.within("settings").click("0-2", cx)
});
let ip = r"\b\d{1,3}(\.\d{1,3}){3}\b";
assert_eq!(
patterns(&highlight_rules(&workspace, cx)),
["ERROR", "WARN", ip]
);
in_frame(cx, handle, |window, _| {
assert!(window.find(("highlight-rule", 2usize)).visible());
assert!(window.find("highlight-preview").visible());
});
// Off until turned on: the examples color nothing before.
let colored = |cx: &mut TestAppContext| {
cx.update(|cx| !TerminalHighlights::current(cx).spans("ERROR").is_empty())
};
assert!(!colored(cx));
in_frame(cx, handle, |window, cx| {
// 常规 › 启用关键字高亮.
let switch = window
.within("settings")
.within("group-0")
.within("item-0")
.find("check");
assert_eq!(switch.checked(), Some(false));
window
.within("settings")
.within("group-0")
.within("item-0")
.click("check", cx);
});
cx.run_until_parked();
assert!(colored(cx));
// A new row takes the keyboard at once; what does not compile says so
// under it, and the settings have it as typed.
in_frame(cx, handle, |window, cx| {
window.click("add-highlight-rule", cx)
});
scroll_to_highlight_rules(cx, handle);
in_frame(cx, handle, |window, cx| {
assert_eq!(
window.find(("highlight-rule-pattern", 3usize)).focused(),
Some(true)
);
window.input("OutOf(Memory", cx);
});
in_frame(cx, handle, |window, cx| {
assert_eq!(
window.find(("highlight-rule-error", 3usize)).label(),
Some("正则表达式写法有误")
);
window.press("cmd-a", cx);
window.input(r"OutOfMemory\w*", cx);
});
in_frame(cx, handle, |window, cx| {
assert!(window.try_find(("highlight-rule-error", 3usize)).is_none());
window.click(("highlight-rule-note", 3usize), cx);
window.input("内存溢出", cx);
window.click(("highlight-rule-color", 3usize), cx);
window.press("cmd-a", cx);
window.input("#D946EF", cx);
window.click(("highlight-rule-notify", 3usize), cx);
window.click(("highlight-rule-enabled", 0usize), cx);
});
cx.run_until_parked();
let rules = highlight_rules(&workspace, cx);
assert_eq!(rules[3].pattern, r"OutOfMemory\w*");
assert_eq!(rules[3].note, "内存溢出");
assert_eq!(rules[3].color, HighlightColor::from_hex("#d946ef").unwrap());
assert!(rules[3].enabled && rules[3].notify);
assert!(!rules[0].enabled);
// The color picker opens on its palette. (Its swatches are keyed by
// color; the a11y tree that would catch two of one color is not built
// in tests, see FEATURED_COLORS.)
in_frame(cx, handle, |window, cx| {
window.click(("highlight-rule-picker", 3usize), cx)
});
in_frame(cx, handle, |window, cx| window.press("escape", cx));
// Dragged by its handle, the new row goes first.
in_frame(cx, handle, |window, cx| {
window.drag_to(
("highlight-rule-handle", 3usize),
("highlight-rule", 0usize),
cx,
)
});
cx.run_until_parked();
assert_eq!(
patterns(&highlight_rules(&workspace, cx)),
[r"OutOfMemory\w*", "ERROR", "WARN", ip]
);
// Deleted at once, WARN goes; the terminals follow every change.
in_frame(cx, handle, |window, cx| {
window.click(("delete-highlight-rule", 1usize), cx)
});
cx.run_until_parked();
assert_eq!(
patterns(&highlight_rules(&workspace, cx)),
[r"OutOfMemory\w*", "ERROR", ip]
);
cx.update(|cx| {
let colored = TerminalHighlights::current(cx).spans("OutOfMemoryError ERROR 10.0.0.1");
// ERROR is off; the new rule and the address color.
assert_eq!(
colored
.iter()
.map(|(range, _)| range.clone())
.collect::<Vec<_>>(),
[0..16, 23..31]
);
});
// 启用关键字高亮 off, nothing colors.
let settings = cx.update(|cx| workspace.read(cx).settings().clone());
settings.update(cx, |settings, cx| {
settings.update(|settings| settings.terminal_highlight.enabled = false, cx)
});
cx.run_until_parked();
cx.update(|cx| {
assert!(
TerminalHighlights::current(cx)
.spans("OutOfMemoryError")
.is_empty()
);
let in_effect: Vec<_> = TerminalHighlights::current(cx)
.rules()
.iter()
.map(|rule| rule.pattern.clone())
.collect();
assert_eq!(in_effect, patterns);
});
}
+9 -2
View File
@@ -1001,7 +1001,8 @@ async fn terminals_notify_where_the_user_will_see_it(cx: &mut TestAppContext) {
});
}
/// Have the settings hold `rules`, as the 关键字高亮 page writes them.
/// Have the settings hold `rules`, as the 关键字高亮 page writes them, with
/// highlighting on.
fn set_highlight_rules(
cx: &mut TestAppContext,
workspace: &Entity<Workspace>,
@@ -1009,7 +1010,13 @@ fn set_highlight_rules(
) {
let settings = cx.update(|cx| workspace.read(cx).settings().clone());
settings.update(cx, |settings, cx| {
settings.update(|settings| settings.terminal_highlight.rules = rules, cx)
settings.update(
|settings| {
settings.terminal_highlight.enabled = true;
settings.terminal_highlight.rules = rules;
},
cx,
)
});
cx.run_until_parked();
}