Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crates/terminal_view/src/view/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ impl TerminalView {
}
let text = normalize_paste_line_endings(text);
let text = text.as_ref();
let mode = self.terminal_frame_snapshot.mode;
// 粘贴决策读实时模式:渲染快照可能滞后于远端 2004 翻转(issue #198)。
let mode = self.live_term_mode(cx);

// ALT_SCREEN(如 Vim、less)属于全屏交互程序,粘贴内容不会像 shell 那样直接执行。
// 这里跳过高危/多行确认,避免编辑器场景误弹确认框。
Expand Down Expand Up @@ -202,7 +203,9 @@ impl TerminalView {
let text = text.as_ref();
// 仅在应用请求 bracketed paste 模式时才包装,避免把控制序列
// 原样送进不支持的程序(例如 Vim 未开启时可能导致光标/位置异常)。
let mode = self.terminal_frame_snapshot.mode;
// 与 paste_text 相同:这里也必须读实时模式,弹窗确认期间模式可能
// 已被远端翻转(issue #198)。
let mode = self.live_term_mode(cx);
self.apply_paste_to_history_prompt(text, cx);
self.write_to_pty(terminal_paste_bytes(text, mode), cx);
self.focus_terminal(window, cx);
Expand Down
3 changes: 2 additions & 1 deletion crates/terminal_view/src/view/clipboard_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ impl TerminalView {
if !self.is_live_ssh_terminal(cx) {
return;
}
let mode = self.terminal_frame_snapshot.mode;
// 图片路径粘贴同样读实时模式,避免 2004 过期包装(issue #198)。
let mode = self.live_term_mode(cx);
self.apply_paste_to_history_prompt(path, cx);
self.write_to_pty(terminal_paste_bytes(path, mode), cx);
}
Expand Down
18 changes: 18 additions & 0 deletions crates/terminal_view/src/view/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use alacritty_terminal::sync::FairMutex;

pub(super) const REMOTE_CLIPBOARD_IMAGE_DIR: &str = "/tmp";
pub(super) const REMOTE_CLIPBOARD_IMAGE_PREFIX: &str = "onetcli-paste";
Expand Down Expand Up @@ -115,6 +116,23 @@ pub(super) fn terminal_paste_bytes(text: &str, mode: TermMode) -> Vec<u8> {
}
}

/// 粘贴等一次性输入决策用的实时终端模式。
///
/// 渲染帧缓存的 `terminal_frame_snapshot.mode` 在解析线程持锁时会保留旧值
/// (见 `terminal_render.rs` 的 with_terminal_if_ready 注释),而 bash 5.1+
/// 每次执行命令都会翻转 2004 bracketed paste 模式:用过期快照判断是否包装
/// `\x1b[200~` 会让远端把包装序列当字面文本显示(issue #198)。这里在输入
/// 事件线程上用 try_lock 读最新模式;锁被解析器占用时回退快照(与既有
/// 行为一致,且该场景下粘贴本身也会被写队列排队)。
pub(super) fn resolve_live_term_mode(
term: &FairMutex<Term<GpuiEventProxy>>,
fallback: TermMode,
) -> TermMode {
term.try_lock_unfair()
.map(|term| *term.mode())
.unwrap_or(fallback)
}

pub(super) fn should_direct_paste_on_right_click(enabled: bool, button: MouseButton) -> bool {
enabled && button == MouseButton::Right
}
Expand Down
11 changes: 11 additions & 0 deletions crates/terminal_view/src/view/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ impl TerminalView {
&& !terminal.is_locked()
}

/// 粘贴路径的实时终端模式。
///
/// 代理到 `resolve_live_term_mode`(见 helpers.rs),集中维护
/// 「实时读取 + 快照回退」语义,避免各粘贴入口散落 try_lock。
pub(super) fn live_term_mode(&self, cx: &App) -> TermMode {
resolve_live_term_mode(
self.terminal.read(cx).term(),
self.terminal_frame_snapshot.mode,
)
}

pub(super) fn has_blocking_auth_prompt(&self, cx: &App) -> bool {
let terminal = self.terminal.read(cx);
terminal.ssh_credential_request().is_some()
Expand Down
4 changes: 2 additions & 2 deletions crates/terminal_view/src/view/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use super::{
history_prompt_overlay_bounds, is_large_paste, join_paste_as_single_line,
live_ssh_feature_supported, live_terminal_input_supported, mouse_button_code,
multiline_non_empty_line_count, recording_playback_display_name, remote_clipboard_image_path,
resolve_ssh_reconnect_source, sgr_mouse_button_report, sgr_mouse_mode_enabled,
sgr_mouse_wheel_report, should_confirm_local_terminal_close,
resolve_live_term_mode, resolve_ssh_reconnect_source, sgr_mouse_button_report,
sgr_mouse_mode_enabled, sgr_mouse_wheel_report, should_confirm_local_terminal_close,
should_defer_inline_history_prompt_input_to_text_system, should_defer_sgr_left_press,
should_direct_paste_on_right_click, should_dismiss_history_prompt_for_keystroke,
should_dismiss_history_prompt_for_mouse, should_dismiss_history_prompt_for_scroll,
Expand Down
80 changes: 80 additions & 0 deletions crates/terminal_view/src/view/tests/paste.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,86 @@ fn terminal_paste_bytes_keeps_bracketed_wrap_untouched() {
assert_eq!(b"\x1b[200~echo hi\x1b[201~".to_vec(), bytes);
}

/// 供 resolve_live_term_mode 测试使用的最小终端尺寸。
struct PasteModeTestDimensions {
columns: usize,
screen_lines: usize,
}

impl alacritty_terminal::grid::Dimensions for PasteModeTestDimensions {
fn total_lines(&self) -> usize {
self.screen_lines
}
fn screen_lines(&self) -> usize {
self.screen_lines
}
fn columns(&self) -> usize {
self.columns
}
}

/// 构造一个不带 2004 模式的最小 Term,供模式解析测试使用。
fn paste_mode_test_term() -> (
alacritty_terminal::sync::FairMutex<
alacritty_terminal::term::Term<terminal::pty_backend::GpuiEventProxy>,
>,
alacritty_terminal::vte::ansi::Processor<alacritty_terminal::vte::ansi::StdSyncHandler>,
) {
use alacritty_terminal::sync::FairMutex;
use alacritty_terminal::term::{Config as TermConfig, Term};
use alacritty_terminal::vte::ansi::Processor;
use terminal::pty_backend::GpuiEventProxy;
use tokio::sync::mpsc::unbounded_channel;

let (event_tx, _event_rx) = unbounded_channel();
let dimensions = PasteModeTestDimensions {
columns: 16,
screen_lines: 4,
};
let term = Term::new(
TermConfig::default(),
&dimensions,
GpuiEventProxy::new(event_tx),
);
(FairMutex::new(term), Processor::new())
}

#[test]
fn resolve_live_term_mode_prefers_realtime_term_mode_over_stale_snapshot() {
let (locked, mut processor) = paste_mode_test_term();

// 远端开启 2004:实时读取应看到 ON,即便快照仍是过期的 OFF。
{
let mut term = locked.lock_unfair();
processor.advance(&mut *term, b"\x1b[?2004h");
assert!(term.mode().contains(TermMode::BRACKETED_PASTE));
}
let resolved = resolve_live_term_mode(&locked, TermMode::empty());
assert!(resolved.contains(TermMode::BRACKETED_PASTE));

// 回归场景(issue #198):快照停留在命令执行前的 ON,而远端 bash 已发
// 2004l 关闭。实时读取必须报告 OFF,粘贴才不会带 \x1b[200~ 字面泄漏。
{
let mut term = locked.lock_unfair();
processor.advance(&mut *term, b"\x1b[?2004l");
assert!(!term.mode().contains(TermMode::BRACKETED_PASTE));
}
let stale_snapshot = TermMode::BRACKETED_PASTE;
let resolved = resolve_live_term_mode(&locked, stale_snapshot);
assert!(!resolved.contains(TermMode::BRACKETED_PASTE));
}

#[test]
fn resolve_live_term_mode_falls_back_to_snapshot_when_term_locked() {
let (locked, _processor) = paste_mode_test_term();
// 模拟解析线程持锁:try_lock 失败时必须回退快照而不是 panic/阻塞。
let guard = locked.lock_unfair();
let fallback = TermMode::BRACKETED_PASTE;
let resolved = resolve_live_term_mode(&locked, fallback);
drop(guard);
assert!(resolved.contains(TermMode::BRACKETED_PASTE));
}

#[test]
fn join_paste_as_single_line_collapses_blank_lines() {
assert_eq!(
Expand Down
Loading