Merge remote-tracking branch 'origin/main' into review/269-integration-20260924
This commit is contained in:
commit
d531ebb34b
23 files changed
+378
-58
No files matched your search
@@ -45,6 +45,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+Linksklick zum Öffnen",
|
||||
"link_ctrl_click": "Strg+Linksklick zum Öffnen",
|
||||
"settings": "Einstellungen",
|
||||
"language": "Sprache",
|
||||
"about": "Über",
|
||||
|
||||
@@ -209,6 +209,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+left click to open",
|
||||
"link_ctrl_click": "Ctrl+left click to open",
|
||||
"settings": "Settings",
|
||||
"language": "Language",
|
||||
"about": "About",
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+clic izquierdo para abrir",
|
||||
"link_ctrl_click": "Ctrl+clic izquierdo para abrir",
|
||||
"settings": "Configuración",
|
||||
"language": "Idioma",
|
||||
"about": "Acerca de",
|
||||
|
||||
@@ -95,6 +95,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+clic gauche pour ouvrir",
|
||||
"link_ctrl_click": "Ctrl+clic gauche pour ouvrir",
|
||||
"settings": "Paramètres",
|
||||
"language": "Langue",
|
||||
"about": "À propos",
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+clic sinistro per aprire",
|
||||
"link_ctrl_click": "Ctrl+clic sinistro per aprire",
|
||||
"settings": "Impostazioni",
|
||||
"language": "Lingua",
|
||||
"about": "Informazioni",
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+左クリックで開く",
|
||||
"link_ctrl_click": "Ctrl+左クリックで開く",
|
||||
"settings": "設定",
|
||||
"language": "言語",
|
||||
"about": "このアプリについて",
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+왼쪽 클릭으로 열기",
|
||||
"link_ctrl_click": "Ctrl+왼쪽 클릭으로 열기",
|
||||
"settings": "설정",
|
||||
"language": "언어",
|
||||
"about": "정보",
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+clique esquerdo para abrir",
|
||||
"link_ctrl_click": "Ctrl+clique esquerdo para abrir",
|
||||
"settings": "Configurações",
|
||||
"language": "Idioma",
|
||||
"about": "Sobre",
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+щелчок левой кнопкой для открытия",
|
||||
"link_ctrl_click": "Ctrl+щелчок левой кнопкой для открытия",
|
||||
"settings": "Настройки",
|
||||
"language": "Язык",
|
||||
"about": "О программе",
|
||||
|
||||
@@ -209,6 +209,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+左键跳转",
|
||||
"link_ctrl_click": "Ctrl+左键跳转",
|
||||
"settings": "设置",
|
||||
"language": "语言",
|
||||
"about": "关于",
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"link_command_click": "Command+左鍵開啟",
|
||||
"link_ctrl_click": "Ctrl+左鍵開啟",
|
||||
"settings": "設定",
|
||||
"language": "語言",
|
||||
"about": "關於",
|
||||
|
||||
@@ -469,6 +469,18 @@ pub fn highlighted_at<T>(
|
||||
config: &UiConfig,
|
||||
point: Point,
|
||||
mouse_mods: ModifiersState,
|
||||
) -> Option<HintMatch> {
|
||||
highlighted_at_with_mouse_override(term, config, point, mouse_mods, false)
|
||||
}
|
||||
|
||||
/// Allow a shell's explicit link gesture to override application mouse reporting.
|
||||
/// Configured hint modifiers and the mouse-enabled flag are still respected.
|
||||
pub fn highlighted_at_with_mouse_override<T>(
|
||||
term: &Term<T>,
|
||||
config: &UiConfig,
|
||||
point: Point,
|
||||
mouse_mods: ModifiersState,
|
||||
override_mouse_mode: bool,
|
||||
) -> Option<HintMatch> {
|
||||
let mouse_mode = term.mode().intersects(TermMode::MOUSE_MODE);
|
||||
|
||||
@@ -477,7 +489,9 @@ pub fn highlighted_at<T>(
|
||||
let highlight = hint.mouse.is_some_and(|mouse| {
|
||||
mouse.enabled
|
||||
&& mouse_mods.contains(mouse.mods.0)
|
||||
&& (!mouse_mode || mouse_mods.contains(ModifiersState::SHIFT))
|
||||
&& (!mouse_mode
|
||||
|| override_mouse_mode
|
||||
|| mouse_mods.contains(ModifiersState::SHIFT))
|
||||
});
|
||||
if !highlight {
|
||||
return None;
|
||||
|
||||
@@ -64,6 +64,21 @@ impl SshHostLists {
|
||||
)
|
||||
}
|
||||
|
||||
/// 与 [`Self::merged`] 同一顺序权威,同时带出用户在 SSH 设置里起的
|
||||
/// 主机名称(地址 → 别名;没起名的是空串,展示端回落地址本身)。
|
||||
/// profiles 在 [`Self::load`] 时已经读盘,这里不重复读 JSON;launcher /
|
||||
/// Quick Jump / 命令面板三处 SSH 行共用,避免各自再解析一次。
|
||||
pub fn merged_with_labels(&self) -> Vec<(String, String)> {
|
||||
let labels = self.profiles.labels();
|
||||
self.merged()
|
||||
.into_iter()
|
||||
.map(|host| {
|
||||
let label = labels.get(&host).cloned().unwrap_or_default();
|
||||
(host, label)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 隐藏区(设置页"已隐藏"折叠列表的数据源)。
|
||||
pub fn hidden_hosts(&self) -> Vec<String> {
|
||||
self.hidden.iter().filter(|host| self.configured.contains(host)).cloned().collect()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! GPUI 壳的 OSC 8 / 正则 hint 接线:虚线下划线、悬停预览、Ctrl+点击打开。
|
||||
//! GPUI 壳的 OSC 8 / 正则 hint 接线:虚线下划线、悬停预览、平台修饰键+点击打开。
|
||||
//!
|
||||
//! 匹配与动作全部复用旧壳 `display::hint` + `file_uri` + `daemon`,这里只做
|
||||
//! 视口坐标、GPUI 修饰键和打开入口。
|
||||
@@ -18,6 +18,15 @@ use winit::keyboard::ModifiersState;
|
||||
use crate::config::UiConfig;
|
||||
use crate::config::ui_config::{HintAction, HintInternalAction, default_hint_command};
|
||||
use crate::display::hint::{self, HintMatch};
|
||||
use crate::i18n::{Message, UiLanguage};
|
||||
use crate::platform::Platform;
|
||||
|
||||
pub(super) fn link_modifier(mods: &gpui::Modifiers) -> bool {
|
||||
match Platform::current() {
|
||||
Platform::MacOS => mods.platform,
|
||||
Platform::Windows | Platform::Linux => mods.control,
|
||||
}
|
||||
}
|
||||
|
||||
/// 悬停目标:旧壳 `highlighted_hint` + 已经解码好的预览文案。
|
||||
#[derive(Clone)]
|
||||
@@ -97,7 +106,13 @@ pub(super) fn highlighted_at<T: EventListener>(
|
||||
point: Point,
|
||||
mods: &gpui::Modifiers,
|
||||
) -> Option<HintMatch> {
|
||||
hint::highlighted_at(term, config, point, winit_mouse_mods(mods))
|
||||
hint::highlighted_at_with_mouse_override(
|
||||
term,
|
||||
config,
|
||||
point,
|
||||
winit_mouse_mods(mods),
|
||||
link_modifier(mods),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn hover_from_hint<T: EventListener>(
|
||||
@@ -105,6 +120,7 @@ pub(super) fn hover_from_hint<T: EventListener>(
|
||||
hint: HintMatch,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
language: UiLanguage,
|
||||
) -> Option<LinkHover> {
|
||||
let raw = hint
|
||||
.hyperlink()
|
||||
@@ -117,12 +133,15 @@ pub(super) fn hover_from_hint<T: EventListener>(
|
||||
point_to_viewport_from(origin, start).filter(|vp| vp.line < rows && vp.column.0 < cols);
|
||||
let (anchor_row, anchor_col) =
|
||||
vp.map(|vp| (vp.line as u16, vp.column.0 as u16)).unwrap_or((0, 0));
|
||||
const HINT: &str = " · Ctrl+点击";
|
||||
let gesture = language.text(match Platform::current() {
|
||||
Platform::MacOS => Message::CommonLinkCommandClick,
|
||||
Platform::Windows | Platform::Linux => Message::CommonLinkCtrlClick,
|
||||
});
|
||||
let width = |s: &str| -> usize { s.chars().map(|c| c.width().unwrap_or(0)).sum() };
|
||||
let target = crate::display::strip_file_scheme(uri);
|
||||
let budget = cols.saturating_sub(width(HINT) + 1);
|
||||
let budget = cols.saturating_sub(width(gesture) + width(" · ") + 1);
|
||||
let target = crate::display::fit_tail(&target, budget);
|
||||
Some(LinkHover { hint, preview: format!("{target}{HINT}"), anchor_row, anchor_col })
|
||||
Some(LinkHover { hint, preview: format!("{target} · {gesture}"), anchor_row, anchor_col })
|
||||
}
|
||||
|
||||
pub(super) fn open_hint_match(
|
||||
|
||||
@@ -402,7 +402,7 @@ pub struct TerminalView {
|
||||
selection_scroll_epoch: u64,
|
||||
/// 是否已有一条自动回滚定时器链在跑——每次 move 都开一条会叠出 N 倍速。
|
||||
selection_scroll_active: bool,
|
||||
/// OSC 8 / 正则 URL:虚线下划线、悬停预览、Ctrl+点击打开。
|
||||
/// OSC 8 / 正则 URL:虚线下划线、悬停预览、平台修饰键+点击打开。
|
||||
pub(super) hint_config: Arc<UiConfig>,
|
||||
pub(super) link_hover: Option<super::osc_links::LinkHover>,
|
||||
pending_link_open: bool,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::super::osc_links::link_modifier;
|
||||
use super::*;
|
||||
use gpui_component::WindowExt as _;
|
||||
|
||||
@@ -143,7 +144,13 @@ impl TerminalView {
|
||||
let term = session.term.lock();
|
||||
super::super::osc_links::highlighted_at(&term, &self.hint_config, point, mods)
|
||||
.and_then(|hint| {
|
||||
super::super::osc_links::hover_from_hint(&term, hint, self.rows, self.cols)
|
||||
super::super::osc_links::hover_from_hint(
|
||||
&term,
|
||||
hint,
|
||||
self.rows,
|
||||
self.cols,
|
||||
crate::gpui_shell::config::ui_language(cx),
|
||||
)
|
||||
})
|
||||
})
|
||||
};
|
||||
@@ -362,6 +369,7 @@ impl TerminalView {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
window.focus(&self.focus_handle, cx);
|
||||
self.pending_link_open = false;
|
||||
cx.emit(TerminalViewEvent::FocusRequested);
|
||||
if self.session.is_none() {
|
||||
return;
|
||||
@@ -407,6 +415,17 @@ impl TerminalView {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
if link_modifier(&event.modifiers) && event.click_count == 1 {
|
||||
self.update_link_hover(event.position, &event.modifiers, cx);
|
||||
if self.link_hover.is_some() {
|
||||
if let Some(session) = &self.session {
|
||||
session.term.lock().selection = None;
|
||||
}
|
||||
self.selecting = false;
|
||||
self.pending_link_open = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if self.mouse_mode_active(&event.modifiers) {
|
||||
self.send_mouse_report(
|
||||
event.position,
|
||||
@@ -416,28 +435,6 @@ impl TerminalView {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if event.modifiers.control && event.click_count == 1 {
|
||||
let (point, _) = self.grid_point(event.position);
|
||||
let hit = self.session.as_ref().is_some_and(|session| {
|
||||
let term = session.term.lock();
|
||||
super::super::osc_links::highlighted_at(
|
||||
&term,
|
||||
&self.hint_config,
|
||||
point,
|
||||
&event.modifiers,
|
||||
)
|
||||
.is_some()
|
||||
});
|
||||
if hit {
|
||||
if let Some(session) = &self.session {
|
||||
session.term.lock().selection = None;
|
||||
}
|
||||
self.selecting = false;
|
||||
self.pending_link_open = true;
|
||||
self.update_link_hover(event.position, &event.modifiers, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let (point, side) = self.grid_point(event.position);
|
||||
let ty = match event.click_count {
|
||||
1 => SelectionType::Simple,
|
||||
@@ -472,6 +469,11 @@ impl TerminalView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Retain the pressed link until release; dragging must not retarget it
|
||||
// or leak part of the consumed gesture to the application.
|
||||
if self.pending_link_open {
|
||||
return;
|
||||
}
|
||||
if self.move_completion_popup_scrollbar(event, cx) {
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
@@ -531,6 +533,12 @@ impl TerminalView {
|
||||
window.focus(&self.focus_handle, cx);
|
||||
cx.emit(TerminalViewEvent::FocusRequested);
|
||||
}
|
||||
if event.pressed_button.is_none() && link_modifier(&event.modifiers) {
|
||||
self.update_link_hover(event.position, &event.modifiers, cx);
|
||||
if self.link_hover.is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if self.mouse_mode_active(&event.modifiers) {
|
||||
self.clear_link_hover(cx);
|
||||
// 鼠标模式的移动上报:拖动 = 按钮码+32(需 DRAG 或 MOTION 任一),
|
||||
@@ -581,7 +589,8 @@ impl TerminalView {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
if !self.selecting && self.mouse_mode_active(&event.modifiers) {
|
||||
let pending_link_open = std::mem::take(&mut self.pending_link_open);
|
||||
if !pending_link_open && !self.selecting && self.mouse_mode_active(&event.modifiers) {
|
||||
self.send_mouse_report(
|
||||
event.position,
|
||||
mouse_protocol::BUTTON_LEFT,
|
||||
@@ -591,15 +600,15 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
self.selecting = false;
|
||||
let open_link = (self.pending_link_open || event.modifiers.control)
|
||||
let (point, _) = self.grid_point(event.position);
|
||||
let open_link = pending_link_open
|
||||
&& self.selection_is_empty()
|
||||
&& self.link_hover.is_some();
|
||||
&& self.link_hover.as_ref().is_some_and(|hover| hover.hint.bounds().contains(&point));
|
||||
if open_link {
|
||||
self.try_open_hovered_link(window, cx);
|
||||
} else if self.copy_on_select {
|
||||
self.copy_selection(false, window, cx);
|
||||
}
|
||||
self.pending_link_open = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -619,6 +628,7 @@ impl TerminalView {
|
||||
}
|
||||
self.stop_selection_scroll();
|
||||
let dragging_scrollbar = self.scrollbar_drag.take().is_some();
|
||||
self.pending_link_open = false;
|
||||
if !self.selecting {
|
||||
if dragging_scrollbar {
|
||||
cx.notify();
|
||||
@@ -626,7 +636,6 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
self.selecting = false;
|
||||
self.pending_link_open = false;
|
||||
// 指针不在终端上,链接打开不该发生;只补选中即复制这一条收尾。
|
||||
if self.copy_on_select {
|
||||
self.copy_selection(false, window, cx);
|
||||
|
||||
@@ -151,3 +151,178 @@ fn hover_does_not_steal_focus_during_drag_overlay_dialog_or_inactive_window(
|
||||
move_over(&mut cx, None);
|
||||
assert_eq!(probe.read_with(&cx, |probe, _| probe.requests.get()), 0);
|
||||
}
|
||||
|
||||
fn link_modifiers() -> Modifiers {
|
||||
match crate::platform::Platform::current() {
|
||||
crate::platform::Platform::MacOS => Modifiers { platform: true, ..Modifiers::default() },
|
||||
_ => Modifiers { control: true, ..Modifiers::default() },
|
||||
}
|
||||
}
|
||||
|
||||
fn link_fixture(
|
||||
cx: &mut TestAppContext,
|
||||
text: &[u8],
|
||||
) -> (Entity<TerminalView>, VisualTestContext, std::sync::mpsc::Receiver<Msg>) {
|
||||
cx.update(crate::gpui_shell::math_view::register);
|
||||
let (probe, mut cx) = open(cx);
|
||||
let terminal = probe.read_with(&cx, |probe, _| probe.terminal.clone());
|
||||
let receiver = terminal.update(&mut cx, |view, cx| {
|
||||
let (session, receiver) = session::test_session();
|
||||
view.session = Some(session);
|
||||
view.error = None;
|
||||
view.exited = None;
|
||||
view.copy_on_select = false;
|
||||
// Use the production matcher and dispatcher with a deterministic action;
|
||||
// tests must not launch a user's browser or editor.
|
||||
let mut config = UiConfig::default();
|
||||
Arc::make_mut(&mut config.hints.enabled[0]).action =
|
||||
crate::config::ui_config::HintAction::Action(
|
||||
crate::config::ui_config::HintInternalAction::Copy,
|
||||
);
|
||||
view.hint_config = Arc::new(config);
|
||||
super::super::startup_tests::feed(view, text);
|
||||
cx.notify();
|
||||
receiver
|
||||
});
|
||||
draw(&mut cx);
|
||||
cx.update(|_, cx| cx.write_to_clipboard(gpui::ClipboardItem::new_string("before".into())));
|
||||
(terminal, cx, receiver)
|
||||
}
|
||||
|
||||
fn cell(view: &Entity<TerminalView>, cx: &VisualTestContext, col: usize) -> Point<Pixels> {
|
||||
view.read_with(cx, |view, _| {
|
||||
point(
|
||||
view.origin.x + view.cell_width * (col as f32 + 0.5),
|
||||
view.origin.y + view.line_height * 0.5,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn clipboard(cx: &mut VisualTestContext) -> Option<String> {
|
||||
cx.update(|_, cx| cx.read_from_clipboard().and_then(|item| item.text()))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn link_gesture_opens_regex_files_and_osc8_with_or_without_mouse_reporting(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
for (output, target) in [
|
||||
("https://example.com", "https://example.com"),
|
||||
("file:///tmp/pebrel-notes.md", "file:///tmp/pebrel-notes.md"),
|
||||
("[notes](./notes.md)", "[notes](./notes.md)"),
|
||||
("\x1b]8;;https://example.com/osc8\x1b\\site\x1b]8;;\x1b\\", "https://example.com/osc8"),
|
||||
] {
|
||||
for mouse_mode in [false, true] {
|
||||
let output = if mouse_mode {
|
||||
format!("\x1b[?1003h\x1b[?1006h{output}")
|
||||
} else {
|
||||
output.to_owned()
|
||||
};
|
||||
let (view, mut window, receiver) = link_fixture(cx, output.as_bytes());
|
||||
let position = cell(&view, &window, 1);
|
||||
window.simulate_mouse_move(position, None, link_modifiers());
|
||||
assert!(view.read_with(&window, |view, _| view.link_hover.is_some()));
|
||||
window.simulate_mouse_down(position, MouseButton::Left, link_modifiers());
|
||||
assert_eq!(clipboard(&mut window).as_deref(), Some("before"));
|
||||
assert!(view.read_with(&window, |view, _| view.pending_link_open));
|
||||
// Releasing Command/Ctrl first must not send an orphan mouse-up to the TUI.
|
||||
window.simulate_mouse_up(position, MouseButton::Left, Modifiers::default());
|
||||
assert_eq!(clipboard(&mut window).as_deref(), Some(target));
|
||||
assert!(!receiver.try_iter().any(|event| matches!(event, Msg::Input(_))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn plain_click_and_wrong_modifier_do_not_open_links(cx: &mut TestAppContext) {
|
||||
let (view, mut window, _) = link_fixture(cx, b"https://example.com");
|
||||
let position = cell(&view, &window, 1);
|
||||
let wrong = if link_modifiers().platform {
|
||||
Modifiers { control: true, ..Modifiers::default() }
|
||||
} else {
|
||||
Modifiers { platform: true, ..Modifiers::default() }
|
||||
};
|
||||
for modifiers in [Modifiers::default(), wrong] {
|
||||
window.simulate_mouse_move(position, None, modifiers);
|
||||
window.simulate_mouse_down(position, MouseButton::Left, modifiers);
|
||||
window.simulate_mouse_up(position, MouseButton::Left, modifiers);
|
||||
assert_eq!(clipboard(&mut window).as_deref(), Some("before"));
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn link_drag_cannot_retarget_or_leave_a_pending_open(cx: &mut TestAppContext) {
|
||||
let (view, mut window, _) = link_fixture(cx, b"https://one.test https://two.test");
|
||||
let start = cell(&view, &window, 1);
|
||||
for end in [cell(&view, &window, 20), point(px(10.0), px(10.0))] {
|
||||
window.simulate_mouse_down(start, MouseButton::Left, link_modifiers());
|
||||
window.simulate_mouse_move(end, Some(MouseButton::Left), link_modifiers());
|
||||
window.simulate_mouse_up(end, MouseButton::Left, link_modifiers());
|
||||
assert_eq!(clipboard(&mut window).as_deref(), Some("before"));
|
||||
assert!(!view.read_with(&window, |view, _| view.pending_link_open));
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn mouse_reporting_still_receives_ordinary_and_non_link_clicks(cx: &mut TestAppContext) {
|
||||
let (view, mut window, receiver) =
|
||||
link_fixture(cx, b"\x1b[?1000h\x1b[?1006hhttps://example.com plain");
|
||||
for (col, modifiers) in [(1, Modifiers::default()), (22, link_modifiers())] {
|
||||
let position = cell(&view, &window, col);
|
||||
window.simulate_mouse_down(position, MouseButton::Left, modifiers);
|
||||
window.simulate_mouse_up(position, MouseButton::Left, modifiers);
|
||||
let reports: Vec<_> = receiver
|
||||
.try_iter()
|
||||
.filter_map(|event| match event {
|
||||
Msg::Input(bytes) => Some(bytes.into_owned()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(reports.len(), 2);
|
||||
assert!(reports[0].ends_with(b"M"));
|
||||
assert!(reports[1].ends_with(b"m"));
|
||||
assert_eq!(clipboard(&mut window).as_deref(), Some("before"));
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn link_hover_uses_current_language_and_platform(cx: &mut TestAppContext) {
|
||||
use crate::i18n::UiLanguage;
|
||||
let (view, mut window, _) = link_fixture(cx, b"https://example.com");
|
||||
let position = cell(&view, &window, 1);
|
||||
let modifier = if link_modifiers().platform { "Command" } else { "Ctrl" };
|
||||
for (language, suffix) in [
|
||||
(UiLanguage::ZhCn, "+左键跳转"),
|
||||
(UiLanguage::EnUs, "+left click to open"),
|
||||
(UiLanguage::KoKr, "+왼쪽 클릭으로 열기"),
|
||||
] {
|
||||
window.update(|_, cx| cx.global_mut::<Settings>().ui_language = language);
|
||||
window.simulate_mouse_move(position, None, Modifiers::default());
|
||||
let preview =
|
||||
view.read_with(&window, |view, _| view.link_hover.as_ref().unwrap().preview.clone());
|
||||
assert!(preview.ends_with(&format!(" · {modifier}{suffix}")), "{preview}");
|
||||
draw(&mut window);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn explicit_link_gesture_respects_disabled_hints_and_required_modifiers(cx: &mut TestAppContext) {
|
||||
let (view, mut window, _) = link_fixture(cx, b"\x1b[?1000hhttps://example.com");
|
||||
let position = cell(&view, &window, 1);
|
||||
for enabled in [false, true] {
|
||||
view.update(&mut window, |view, _| {
|
||||
let config = Arc::make_mut(&mut view.hint_config);
|
||||
let hint = Arc::make_mut(&mut config.hints.enabled[0]);
|
||||
let mouse = hint.mouse.as_mut().unwrap();
|
||||
mouse.enabled = enabled;
|
||||
mouse.mods.0 = winit::keyboard::ModifiersState::SHIFT;
|
||||
});
|
||||
window.simulate_mouse_down(position, MouseButton::Left, link_modifiers());
|
||||
window.simulate_mouse_up(position, MouseButton::Left, link_modifiers());
|
||||
assert_eq!(clipboard(&mut window).as_deref(), Some("before"));
|
||||
}
|
||||
let modifiers = Modifiers { shift: true, ..link_modifiers() };
|
||||
window.simulate_mouse_down(position, MouseButton::Left, modifiers);
|
||||
window.simulate_mouse_up(position, MouseButton::Left, modifiers);
|
||||
assert_eq!(clipboard(&mut window).as_deref(), Some("https://example.com"));
|
||||
}
|
||||
@@ -2228,7 +2228,7 @@ impl NebulaWorkspace {
|
||||
crate::terminal_profiles::TerminalProfiles::load()
|
||||
.map(|store| store.as_config_profiles())
|
||||
.unwrap_or_default(),
|
||||
crate::gpui_shell::ssh_hosts::SshHostLists::load().merged(),
|
||||
crate::gpui_shell::ssh_hosts::SshHostLists::load().merged_with_labels(),
|
||||
&default_shell_id,
|
||||
language,
|
||||
window.scale_factor().max(0.5),
|
||||
|
||||
@@ -37,7 +37,7 @@ fn open(cx: &mut TestAppContext) -> (Entity<NebulaWorkspace>, VisualTestContext)
|
||||
workspace.palette_override = Some(shell_palette_rows(
|
||||
vec![shell("cmd"), shell("pwsh")],
|
||||
vec![],
|
||||
["test-host".into()],
|
||||
[("test-host".into(), String::new())],
|
||||
"cmd",
|
||||
workspace_ui_language(),
|
||||
1.0,
|
||||
|
||||
@@ -100,26 +100,31 @@ impl NebulaWorkspace {
|
||||
// 次序由共享 merge 权威裁定。
|
||||
let ssh_icons = ssh_host_icon_ids(&crate::display::nebula_data_dir());
|
||||
rows.extend(
|
||||
crate::gpui_shell::ssh_hosts::SshHostLists::load().merged().into_iter().map(
|
||||
|host| {
|
||||
crate::gpui_shell::ssh_hosts::SshHostLists::load()
|
||||
.merged_with_labels()
|
||||
.into_iter()
|
||||
.map(|(host, label)| {
|
||||
let glyph = crate::display::ui::os_icons::resolve(
|
||||
ssh_icons.get(&host).map(String::as_str),
|
||||
)
|
||||
.glyph;
|
||||
let named = (!label.is_empty()).then_some(label.as_str());
|
||||
let (label, hint) = shell_picker::ssh_host_display(named, &host);
|
||||
let search =
|
||||
format!("{label} {host} ssh host remote lianjie 连接").to_lowercase();
|
||||
WorkspacePaletteRow {
|
||||
group_order: usize::MAX,
|
||||
group: language.pick("SSH 主机", "SSH HOSTS").to_owned(),
|
||||
label: host.clone(),
|
||||
hint: "SSH".to_owned(),
|
||||
label,
|
||||
hint,
|
||||
hint_style: WorkspacePaletteHintStyle::Metadata,
|
||||
search: format!("{host} ssh host remote lianjie 连接").to_lowercase(),
|
||||
search,
|
||||
action: WorkspacePaletteAction::LaunchSshHost(host),
|
||||
icon: None,
|
||||
icon_glyph: Some(glyph),
|
||||
icon_path: None,
|
||||
}
|
||||
},
|
||||
),
|
||||
}),
|
||||
);
|
||||
rows
|
||||
});
|
||||
|
||||
@@ -143,18 +143,21 @@ fn ssh_rows(language: crate::display::UiLanguage) -> Vec<WorkspacePaletteRow> {
|
||||
let group = language.pick("SSH 主机", "SSH hosts");
|
||||
let icons = ssh_host_icon_ids(&crate::display::nebula_data_dir());
|
||||
crate::gpui_shell::ssh_hosts::SshHostLists::load()
|
||||
.merged()
|
||||
.merged_with_labels()
|
||||
.into_iter()
|
||||
.map(|host| {
|
||||
.map(|(host, label)| {
|
||||
let glyph =
|
||||
crate::display::ui::os_icons::resolve(icons.get(&host).map(String::as_str)).glyph;
|
||||
let named = (!label.is_empty()).then_some(label.as_str());
|
||||
let (label, hint) = shell_picker::ssh_host_display(named, &host);
|
||||
let search = format!("{label} {host} ssh host remote 远程 连接").to_lowercase();
|
||||
WorkspacePaletteRow {
|
||||
group_order: 3,
|
||||
group: group.to_owned(),
|
||||
label: host.clone(),
|
||||
hint: "SSH".to_owned(),
|
||||
label,
|
||||
hint,
|
||||
hint_style: super::WorkspacePaletteHintStyle::Metadata,
|
||||
search: format!("{host} ssh host remote 远程 连接").to_lowercase(),
|
||||
search,
|
||||
action: WorkspacePaletteAction::LaunchSshHost(host),
|
||||
icon: None,
|
||||
icon_glyph: Some(glyph),
|
||||
|
||||
@@ -15,13 +15,24 @@ fn fallback_shell_glyph(id: &str, has_brand: bool) -> Option<char> {
|
||||
crate::shell_detect::icon_for_id(id).chars().next()
|
||||
}
|
||||
|
||||
/// SSH 主机的展示名与次要信息,与旧壳 `SshRow` 同口径:
|
||||
///
|
||||
/// - 用户在 SSH 设置里起过"主机名称":label 显示别名,hint 给真实连接地址;
|
||||
/// - 没起名:回落地址本身当 label,hint 保持 "SSH" 类型标签,避免重复。
|
||||
pub(super) fn ssh_host_display(label: Option<&str>, host: &str) -> (String, String) {
|
||||
match label {
|
||||
Some(label) if !label.trim().is_empty() => (label.trim().to_owned(), host.to_owned()),
|
||||
_ => (host.to_owned(), "SSH".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 新建终端弹窗的行:已检测 shell + SSH 主机,分组对照旧壳
|
||||
/// `CommandPalette::open_profiles`(推荐 / 所有 Shell / SSH 主机)。
|
||||
/// 三点菜单与 Ctrl+K 打开的是这份列表,不是通用命令面板。
|
||||
pub(super) fn shell_palette_rows(
|
||||
shells: Vec<crate::shell_detect::DetectedShell>,
|
||||
profiles: Vec<crate::config::ui_config::Profile>,
|
||||
ssh_hosts: impl IntoIterator<Item = String>,
|
||||
ssh_hosts: impl IntoIterator<Item = (String, String)>,
|
||||
default_shell_id: &str,
|
||||
language: crate::display::UiLanguage,
|
||||
scale_factor: f32,
|
||||
@@ -85,16 +96,20 @@ pub(super) fn shell_palette_rows(
|
||||
rows.insert(0, default_row);
|
||||
}
|
||||
let ssh_icons = ssh_host_icon_ids(&crate::display::nebula_data_dir());
|
||||
rows.extend(ssh_hosts.into_iter().map(|host| {
|
||||
rows.extend(ssh_hosts.into_iter().map(|(host, label)| {
|
||||
let glyph =
|
||||
crate::display::ui::os_icons::resolve(ssh_icons.get(&host).map(String::as_str)).glyph;
|
||||
// 空串 = 没起名:`ssh_host_display` 回落地址本身,hint 保持 "SSH"。
|
||||
let named = (!label.is_empty()).then_some(label.as_str());
|
||||
let (label, hint) = ssh_host_display(named, &host);
|
||||
let search = format!("{label} {host} ssh host remote lianjie 连接").to_lowercase();
|
||||
WorkspacePaletteRow {
|
||||
group_order: 2,
|
||||
group: ssh_group.to_owned(),
|
||||
label: host.clone(),
|
||||
hint: "SSH".to_owned(),
|
||||
label,
|
||||
hint,
|
||||
hint_style: WorkspacePaletteHintStyle::Metadata,
|
||||
search: format!("{host} ssh host remote lianjie 连接").to_lowercase(),
|
||||
search,
|
||||
action: WorkspacePaletteAction::LaunchSshHost(host),
|
||||
icon: None,
|
||||
icon_glyph: Some(glyph),
|
||||
|
||||
@@ -556,7 +556,7 @@ fn shell_palette_puts_the_default_shell_first() {
|
||||
let rows = shell_palette_rows(
|
||||
shells.clone(),
|
||||
Vec::new(),
|
||||
["box.example".to_owned()],
|
||||
[("box.example".to_owned(), String::new())],
|
||||
"nu",
|
||||
language,
|
||||
1.0,
|
||||
@@ -583,7 +583,14 @@ fn shell_palette_puts_the_default_shell_first() {
|
||||
assert_eq!(rows[4].group, "SSH 主机");
|
||||
|
||||
// 默认 id 没在检测结果里(WSL 发行版被卸载等):不置顶也不 panic。
|
||||
let rows = shell_palette_rows(shells, Vec::new(), None::<String>, "wsl:Ghost", language, 1.0);
|
||||
let rows = shell_palette_rows(
|
||||
shells,
|
||||
Vec::new(),
|
||||
None::<(String, String)>,
|
||||
"wsl:Ghost",
|
||||
language,
|
||||
1.0,
|
||||
);
|
||||
assert!(matches!(
|
||||
&rows[0].action,
|
||||
WorkspacePaletteAction::LaunchShell(shell) if shell.id == "pwsh"
|
||||
@@ -591,6 +598,42 @@ fn shell_palette_puts_the_default_shell_first() {
|
||||
assert_eq!(rows[0].group, "所有 Shell");
|
||||
}
|
||||
|
||||
/// 启动器 SSH 行的"主机名称":起过名的显示别名、hint 给真实地址;没起名
|
||||
/// 回落地址本身、hint 保持 "SSH"。搜索词必须同时覆盖别名与地址。
|
||||
#[test]
|
||||
fn shell_palette_ssh_rows_show_the_configured_host_name() {
|
||||
let rows = shell_palette_rows(
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
[
|
||||
("root@box.example".to_owned(), "生产机".to_owned()),
|
||||
("plain.example".to_owned(), String::new()),
|
||||
],
|
||||
"cmd",
|
||||
crate::display::UiLanguage::ZhCn,
|
||||
1.0,
|
||||
);
|
||||
assert_eq!(rows.len(), 2);
|
||||
|
||||
let named = &rows[0];
|
||||
assert_eq!(named.label, "生产机");
|
||||
assert_eq!(named.hint, "root@box.example");
|
||||
assert!(named.search.contains("生产机"));
|
||||
assert!(named.search.contains("root@box.example"));
|
||||
assert!(matches!(
|
||||
&named.action,
|
||||
WorkspacePaletteAction::LaunchSshHost(host) if host == "root@box.example"
|
||||
));
|
||||
|
||||
let plain = &rows[1];
|
||||
assert_eq!(plain.label, "plain.example");
|
||||
assert_eq!(plain.hint, "SSH");
|
||||
assert!(matches!(
|
||||
&plain.action,
|
||||
WorkspacePaletteAction::LaunchSshHost(host) if host == "plain.example"
|
||||
));
|
||||
}
|
||||
|
||||
/// 回归锁:新建终端弹窗里,没有品牌贴图的 shell 不能空着——必须回落到按 id
|
||||
/// 取字的 Nerd Font 字形。之前这一列只填了 `icon`,于是 zsh、csh、ksh 这些
|
||||
/// 整行左边什么都没有。
|
||||
@@ -608,7 +651,7 @@ fn shell_palette_falls_back_to_an_id_glyph_when_brand_art_is_absent() {
|
||||
let rows = shell_palette_rows(
|
||||
shells,
|
||||
Vec::new(),
|
||||
None::<String>,
|
||||
None::<(String, String)>,
|
||||
"zsh",
|
||||
crate::display::UiLanguage::ZhCn,
|
||||
1.0,
|
||||
@@ -649,7 +692,7 @@ fn shell_palette_includes_imported_terminal_profiles() {
|
||||
let rows = shell_palette_rows(
|
||||
Vec::new(),
|
||||
vec![profile],
|
||||
None::<String>,
|
||||
None::<(String, String)>,
|
||||
&id,
|
||||
crate::display::UiLanguage::ZhCn,
|
||||
1.0,
|
||||
|
||||
Reference in new issue
Block a user