agent_ui: Make provider-owned terminals read-only (#64550)
Make provider-owned terminal cards read-only for user input while preserving local inspection. This builds on the explicit terminal ownership introduced in #64544. - Add a construction-time `TerminalView::with_read_only` option, selected from ACP process ownership rather than PTY presence. Existing views remain interactive by default, including client-owned headless terminals. - Reject typing, send actions, paste, file/path drops, IME input, clear/rerun actions, and focus reports in read-only views. - Preserve selection, copying, links, scrolling, search, and local Vi navigation. Leaving Vi mode does not enable terminal input. - Pass an explicit mouse-routing policy to the existing lower terminal handlers so provider-requested mouse and alternate-scroll modes cannot consume inspection gestures. Real modifiers remain unchanged, and Linux/FreeBSD middle-click paste is suppressed. Release Notes: - N/A
This commit is contained in:
1 parent
a18b1a1026
commit
50061c9cbd
5 files changed
+678
-152
No files matched your search
@@ -9562,6 +9562,7 @@ pub(crate) mod tests {
|
|||||||
)
|
)
|
||||||
.subscribe(cx)
|
.subscribe(cx)
|
||||||
});
|
});
|
||||||
|
let display_terminal = terminal.clone();
|
||||||
thread.update(cx, |thread, cx| {
|
thread.update(cx, |thread, cx| {
|
||||||
thread.on_terminal_provider_event(
|
thread.on_terminal_provider_event(
|
||||||
acp_thread::TerminalProviderEvent::Created {
|
acp_thread::TerminalProviderEvent::Created {
|
||||||
@@ -9593,6 +9594,49 @@ pub(crate) mod tests {
|
|||||||
state.expand_tool_call(tool_id.clone());
|
state.expand_tool_call(tool_id.clone());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
let acp_terminal = thread.read_with(cx, |thread, _| {
|
||||||
|
thread
|
||||||
|
.terminal(terminal_id.clone())
|
||||||
|
.expect("terminal tool call should contain a terminal")
|
||||||
|
});
|
||||||
|
let terminal_view = entry_state.read_with(cx, |state, _| {
|
||||||
|
state
|
||||||
|
.entry(0)
|
||||||
|
.and_then(|entry| entry.terminal(&acp_terminal))
|
||||||
|
.expect("terminal tool card should contain a terminal view")
|
||||||
|
});
|
||||||
|
assert!(terminal_view.read_with(cx, |view, _| view.is_read_only()));
|
||||||
|
|
||||||
|
display_terminal.update(cx, |terminal, _| {
|
||||||
|
assert!(terminal.take_input_log().is_empty());
|
||||||
|
});
|
||||||
|
terminal_view.update_in(cx, |view, window, cx| {
|
||||||
|
window.focus(&view.focus_handle(cx), cx);
|
||||||
|
});
|
||||||
|
cx.simulate_keystrokes("a");
|
||||||
|
assert!(
|
||||||
|
display_terminal
|
||||||
|
.update(cx, |terminal, _| terminal.take_input_log())
|
||||||
|
.is_empty(),
|
||||||
|
"input dispatched to a display-only terminal view must not reach the lower terminal",
|
||||||
|
);
|
||||||
|
|
||||||
|
thread.update(cx, |thread, cx| {
|
||||||
|
thread.on_terminal_provider_event(
|
||||||
|
acp_thread::TerminalProviderEvent::Output {
|
||||||
|
terminal_id: terminal_id.clone(),
|
||||||
|
data: b"provider output remains visible".to_vec(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert!(
|
||||||
|
display_terminal
|
||||||
|
.read_with(cx, |terminal, _| terminal.get_content())
|
||||||
|
.contains("provider output remains visible"),
|
||||||
|
"provider output should continue to render in a read-only terminal",
|
||||||
|
);
|
||||||
assert!(cx.debug_bounds("ICON-Stop").is_none());
|
assert!(cx.debug_bounds("ICON-Stop").is_none());
|
||||||
thread.update(cx, |thread, cx| {
|
thread.update(cx, |thread, cx| {
|
||||||
thread
|
thread
|
||||||
@@ -9627,6 +9671,85 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_acp_owned_terminal_tool_card_is_interactive(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let (conversation_view, cx) =
|
||||||
|
setup_conversation_view(StubAgentServer::new(StubAgentConnection::new()), cx).await;
|
||||||
|
add_to_workspace_with_size(conversation_view.clone(), true, cx);
|
||||||
|
let thread_view = active_thread(&conversation_view, cx);
|
||||||
|
let thread = thread_view.read_with(cx, |view, _| view.thread.clone());
|
||||||
|
let terminal_id = acp::TerminalId::new("acp-owned-terminal");
|
||||||
|
let tool_id = acp::ToolCallId::new("acp-owned-tool");
|
||||||
|
let lower_terminal = cx.new(|cx| {
|
||||||
|
terminal::TerminalBuilder::new_display_only(
|
||||||
|
Default::default(),
|
||||||
|
terminal::terminal_settings::AlternateScroll::On,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
cx.background_executor(),
|
||||||
|
util::paths::PathStyle::local(),
|
||||||
|
)
|
||||||
|
.subscribe(cx)
|
||||||
|
});
|
||||||
|
assert!(!lower_terminal.read_with(cx, |terminal, _| terminal.is_pty()));
|
||||||
|
|
||||||
|
let acp_terminal = thread.update(cx, |thread, cx| {
|
||||||
|
let acp_terminal = thread.register_terminal_created(
|
||||||
|
terminal_id.clone(),
|
||||||
|
"client-owned command".into(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
lower_terminal.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
thread
|
||||||
|
.handle_session_update(
|
||||||
|
acp::SessionUpdate::ToolCall(
|
||||||
|
acp::ToolCall::new(tool_id.clone(), "client-owned command")
|
||||||
|
.kind(acp::ToolKind::Execute)
|
||||||
|
.status(acp::ToolCallStatus::InProgress)
|
||||||
|
.content(vec![acp::ToolCallContent::Terminal(acp::Terminal::new(
|
||||||
|
terminal_id,
|
||||||
|
))]),
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.expect("terminal tool call");
|
||||||
|
acp_terminal
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
acp_terminal.read_with(cx, |terminal, _| terminal.is_process_backed()),
|
||||||
|
"registration ownership, not the lower renderer's PTY, marks this terminal process-backed",
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
let entry_state = thread_view.read_with(cx, |view, _| view.entry_view_state.clone());
|
||||||
|
entry_state.update(cx, |state, cx| {
|
||||||
|
state.expand_tool_call(tool_id);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
let terminal_view = entry_state.read_with(cx, |state, _| {
|
||||||
|
state
|
||||||
|
.entry(0)
|
||||||
|
.and_then(|entry| entry.terminal(&acp_terminal))
|
||||||
|
.expect("terminal tool card should contain a terminal view")
|
||||||
|
});
|
||||||
|
assert!(!terminal_view.read_with(cx, |view, _| view.is_read_only()));
|
||||||
|
|
||||||
|
lower_terminal.update(cx, |terminal, _| {
|
||||||
|
assert!(terminal.take_input_log().is_empty());
|
||||||
|
});
|
||||||
|
terminal_view.update_in(cx, |view, window, cx| {
|
||||||
|
window.focus(&view.focus_handle(cx), cx);
|
||||||
|
});
|
||||||
|
cx.simulate_keystrokes("a");
|
||||||
|
assert_eq!(
|
||||||
|
lower_terminal.update(cx, |terminal, _| terminal.take_input_log()),
|
||||||
|
vec![b"a".to_vec()],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_tool_permission_buttons_terminal_with_pattern(cx: &mut TestAppContext) {
|
async fn test_tool_permission_buttons_terminal_with_pattern(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|||||||
@@ -643,6 +643,7 @@ fn create_terminal(
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Entity<TerminalView> {
|
) -> Entity<TerminalView> {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
|
let read_only = !terminal.read(cx).is_process_backed();
|
||||||
let mut view = TerminalView::new(
|
let mut view = TerminalView::new(
|
||||||
terminal.read(cx).inner().clone(),
|
terminal.read(cx).inner().clone(),
|
||||||
workspace,
|
workspace,
|
||||||
@@ -650,7 +651,8 @@ fn create_terminal(
|
|||||||
project,
|
project,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
)
|
||||||
|
.with_read_only(read_only);
|
||||||
view.set_embedded_mode(Some(1000), cx);
|
view.set_embedded_mode(Some(1000), cx);
|
||||||
view
|
view
|
||||||
})
|
})
|
||||||
|
|||||||
+174
-26
@@ -934,6 +934,12 @@ fn init_command_startup_marker_command(shell_kind: ShellKind, marker_id: u64) ->
|
|||||||
/// sender and receiver remain paired.
|
/// sender and receiver remain paired.
|
||||||
pub struct TerminalMode(TerminalModeKind);
|
pub struct TerminalMode(TerminalModeKind);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MouseInputMode {
|
||||||
|
ReportToTerminal,
|
||||||
|
LocalSelection,
|
||||||
|
}
|
||||||
|
|
||||||
enum TerminalModeKind {
|
enum TerminalModeKind {
|
||||||
Interactive,
|
Interactive,
|
||||||
InteractiveWithCompletion(Sender<Option<ExitStatus>>),
|
InteractiveWithCompletion(Sender<Option<ExitStatus>>),
|
||||||
@@ -2476,13 +2482,15 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_mode(&self, shift: bool) -> bool {
|
pub fn mouse_mode(&self, shift: bool, mode: MouseInputMode) -> bool {
|
||||||
self.last_content.mode.intersects(Modes::MOUSE_MODE) && !shift
|
mode == MouseInputMode::ReportToTerminal
|
||||||
|
&& self.last_content.mode.intersects(Modes::MOUSE_MODE)
|
||||||
|
&& !shift
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_move(&mut self, e: &MouseMoveEvent, cx: &mut Context<Self>) {
|
pub fn mouse_move(&mut self, e: &MouseMoveEvent, mode: MouseInputMode, cx: &mut Context<Self>) {
|
||||||
let position = e.position - self.last_content.terminal_bounds.bounds.origin;
|
let position = e.position - self.last_content.terminal_bounds.bounds.origin;
|
||||||
if self.mouse_mode(e.modifiers.shift) {
|
if self.mouse_mode(e.modifiers.shift, mode) {
|
||||||
// A ctrl/cmd press on a link suppressed its button-press report in
|
// A ctrl/cmd press on a link suppressed its button-press report in
|
||||||
// `mouse_down`. Since the app never saw the press, we must swallow
|
// `mouse_down`. Since the app never saw the press, we must swallow
|
||||||
// the whole gesture rather than forward later motion/release
|
// the whole gesture rather than forward later motion/release
|
||||||
@@ -2580,10 +2588,11 @@ impl Terminal {
|
|||||||
&mut self,
|
&mut self,
|
||||||
e: &MouseMoveEvent,
|
e: &MouseMoveEvent,
|
||||||
region: Bounds<Pixels>,
|
region: Bounds<Pixels>,
|
||||||
|
mode: MouseInputMode,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let position = e.position - self.last_content.terminal_bounds.bounds.origin;
|
let position = e.position - self.last_content.terminal_bounds.bounds.origin;
|
||||||
if !self.mouse_mode(e.modifiers.shift) {
|
if !self.mouse_mode(e.modifiers.shift, mode) {
|
||||||
if let Some(hyperlink) = &self.mouse_down_hyperlink {
|
if let Some(hyperlink) = &self.mouse_down_hyperlink {
|
||||||
let point = grid_point(
|
let point = grid_point(
|
||||||
position,
|
position,
|
||||||
@@ -2646,7 +2655,7 @@ impl Terminal {
|
|||||||
Some(scroll_lines.clamp(-3, 3))
|
Some(scroll_lines.clamp(-3, 3))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_down(&mut self, e: &MouseDownEvent, cx: &mut Context<Self>) {
|
pub fn mouse_down(&mut self, e: &MouseDownEvent, mode: MouseInputMode, cx: &mut Context<Self>) {
|
||||||
let position = e.position - self.last_content.terminal_bounds.bounds.origin;
|
let position = e.position - self.last_content.terminal_bounds.bounds.origin;
|
||||||
let point = grid_point(
|
let point = grid_point(
|
||||||
position,
|
position,
|
||||||
@@ -2657,7 +2666,7 @@ impl Terminal {
|
|||||||
if e.button == MouseButton::Left
|
if e.button == MouseButton::Left
|
||||||
&& e.modifiers.secondary()
|
&& e.modifiers.secondary()
|
||||||
&& (TerminalSettings::get_global(cx).open_links_in_mouse_mode
|
&& (TerminalSettings::get_global(cx).open_links_in_mouse_mode
|
||||||
|| !self.mouse_mode(e.modifiers.shift))
|
|| !self.mouse_mode(e.modifiers.shift, mode))
|
||||||
{
|
{
|
||||||
self.mouse_down_hyperlink = self.find_hyperlink_at_point(point);
|
self.mouse_down_hyperlink = self.find_hyperlink_at_point(point);
|
||||||
|
|
||||||
@@ -2666,7 +2675,7 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.mouse_mode(e.modifiers.shift) {
|
if self.mouse_mode(e.modifiers.shift, mode) {
|
||||||
let bytes =
|
let bytes =
|
||||||
mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode);
|
mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode);
|
||||||
|
|
||||||
@@ -2717,7 +2726,9 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
MouseButton::Middle => {
|
MouseButton::Middle => {
|
||||||
if let Some(item) = cx.read_from_primary() {
|
if mode == MouseInputMode::ReportToTerminal
|
||||||
|
&& let Some(item) = cx.read_from_primary()
|
||||||
|
{
|
||||||
let text = item.text().unwrap_or_default();
|
let text = item.text().unwrap_or_default();
|
||||||
self.paste(&text);
|
self.paste(&text);
|
||||||
}
|
}
|
||||||
@@ -2727,7 +2738,7 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_up(&mut self, e: &MouseUpEvent, cx: &Context<Self>) {
|
pub fn mouse_up(&mut self, e: &MouseUpEvent, mode: MouseInputMode, cx: &Context<Self>) {
|
||||||
let setting = TerminalSettings::get_global(cx);
|
let setting = TerminalSettings::get_global(cx);
|
||||||
|
|
||||||
let position = e.position - self.last_content.terminal_bounds.bounds.origin;
|
let position = e.position - self.last_content.terminal_bounds.bounds.origin;
|
||||||
@@ -2750,7 +2761,7 @@ impl Terminal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.mouse_mode(e.modifiers.shift) {
|
if self.mouse_mode(e.modifiers.shift, mode) {
|
||||||
self.selection_phase = SelectionPhase::Ended;
|
self.selection_phase = SelectionPhase::Ended;
|
||||||
self.last_mouse = None;
|
self.last_mouse = None;
|
||||||
self.mouse_down_position = None;
|
self.mouse_down_position = None;
|
||||||
@@ -2758,7 +2769,7 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.mouse_mode(e.modifiers.shift) {
|
if self.mouse_mode(e.modifiers.shift, mode) {
|
||||||
let point = grid_point(
|
let point = grid_point(
|
||||||
position,
|
position,
|
||||||
self.last_content.terminal_bounds,
|
self.last_content.terminal_bounds,
|
||||||
@@ -2800,8 +2811,13 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
///Scroll the terminal
|
///Scroll the terminal
|
||||||
pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, scroll_multiplier: f32) {
|
pub fn scroll_wheel(
|
||||||
let mouse_mode = self.mouse_mode(e.shift);
|
&mut self,
|
||||||
|
e: &ScrollWheelEvent,
|
||||||
|
scroll_multiplier: f32,
|
||||||
|
mode: MouseInputMode,
|
||||||
|
) {
|
||||||
|
let mouse_mode = self.mouse_mode(e.shift, mode);
|
||||||
let scroll_multiplier = if mouse_mode { 1. } else { scroll_multiplier };
|
let scroll_multiplier = if mouse_mode { 1. } else { scroll_multiplier };
|
||||||
|
|
||||||
if let Some(scroll_lines) = self.determine_scroll_lines(e, scroll_multiplier)
|
if let Some(scroll_lines) = self.determine_scroll_lines(e, scroll_multiplier)
|
||||||
@@ -2820,10 +2836,11 @@ impl Terminal {
|
|||||||
self.write_to_pty(scroll);
|
self.write_to_pty(scroll);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else if self
|
} else if mode == MouseInputMode::ReportToTerminal
|
||||||
.last_content
|
&& self
|
||||||
.mode
|
.last_content
|
||||||
.contains(Modes::ALT_SCREEN | Modes::ALTERNATE_SCROLL)
|
.mode
|
||||||
|
.contains(Modes::ALT_SCREEN | Modes::ALTERNATE_SCROLL)
|
||||||
&& !e.shift
|
&& !e.shift
|
||||||
{
|
{
|
||||||
self.write_to_pty(alt_scroll(scroll_lines));
|
self.write_to_pty(alt_scroll(scroll_lines));
|
||||||
@@ -3580,7 +3597,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
ClipboardItem, Entity, Pixels, TestAppContext, VisualTestContext, bounds, point, size,
|
ClipboardItem, Entity, Pixels, ScrollDelta, TestAppContext, VisualContext,
|
||||||
|
VisualTestContext, bounds, point, size,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rand::{Rng, distr, rngs::StdRng};
|
use rand::{Rng, distr, rngs::StdRng};
|
||||||
@@ -3927,7 +3945,7 @@ mod tests {
|
|||||||
click_count: 1,
|
click_count: 1,
|
||||||
first_mouse: true,
|
first_mouse: true,
|
||||||
};
|
};
|
||||||
terminal.mouse_down(&mouse_down, cx);
|
terminal.mouse_down(&mouse_down, MouseInputMode::ReportToTerminal, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn left_mouse_up_at(
|
fn left_mouse_up_at(
|
||||||
@@ -3941,7 +3959,7 @@ mod tests {
|
|||||||
modifiers: Modifiers::none(),
|
modifiers: Modifiers::none(),
|
||||||
click_count: 1,
|
click_count: 1,
|
||||||
};
|
};
|
||||||
terminal.mouse_up(&mouse_up, cx);
|
terminal.mouse_up(&mouse_up, MouseInputMode::ReportToTerminal, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn left_mouse_drag_to(
|
fn left_mouse_drag_to(
|
||||||
@@ -3955,7 +3973,7 @@ mod tests {
|
|||||||
pressed_button: Some(MouseButton::Left),
|
pressed_button: Some(MouseButton::Left),
|
||||||
modifiers: Modifiers::none(),
|
modifiers: Modifiers::none(),
|
||||||
};
|
};
|
||||||
terminal.mouse_drag(&drag_event, region, cx);
|
terminal.mouse_drag(&drag_event, region, MouseInputMode::ReportToTerminal, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A left click that jitters by a pixel or two (e.g. the window-focusing
|
/// A left click that jitters by a pixel or two (e.g. the window-focusing
|
||||||
@@ -4006,6 +4024,92 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_local_selection_does_not_paste_primary_selection(cx: &mut TestAppContext) {
|
||||||
|
let terminal = init_terminal_test(cx, b"hello world\r\n");
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
cx.write_to_primary(ClipboardItem::new_string("primary selection".into()));
|
||||||
|
let event = MouseDownEvent {
|
||||||
|
button: MouseButton::Middle,
|
||||||
|
position: point(px(50.), px(10.)),
|
||||||
|
modifiers: Modifiers::none(),
|
||||||
|
click_count: 1,
|
||||||
|
first_mouse: true,
|
||||||
|
};
|
||||||
|
terminal.mouse_down(&event, MouseInputMode::LocalSelection, cx);
|
||||||
|
assert!(terminal.take_input_log().is_empty());
|
||||||
|
assert!(terminal.take_pty_write_log().is_empty());
|
||||||
|
|
||||||
|
terminal.mouse_down(&event, MouseInputMode::ReportToTerminal, cx);
|
||||||
|
assert_eq!(
|
||||||
|
terminal.take_input_log(),
|
||||||
|
vec![b"primary selection".to_vec()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
terminal.take_pty_write_log(),
|
||||||
|
vec![b"primary selection".to_vec()]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_local_selection_scrolls_without_terminal_reports(cx: &mut TestAppContext) {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
output.extend_from_slice(b"\x1b[?1002h\x1b[?1006h");
|
||||||
|
for line in 0..80 {
|
||||||
|
output.extend_from_slice(format!("scrollback line {line}\r\n").as_bytes());
|
||||||
|
}
|
||||||
|
let (terminal, cx) = init_terminal_test_with_window(cx, &output);
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.update_window_entity(&terminal, |terminal, window, cx| {
|
||||||
|
terminal.sync(window, cx);
|
||||||
|
assert!(terminal.last_content.mode.intersects(Modes::MOUSE_MODE));
|
||||||
|
assert_eq!(display_offset(&terminal.term.lock()), 0);
|
||||||
|
|
||||||
|
let scroll_event = ScrollWheelEvent {
|
||||||
|
position: point(px(50.0), px(10.0)),
|
||||||
|
delta: ScrollDelta::Lines(point(0.0, 1.0)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
terminal.scroll_wheel(&scroll_event, 1.0, MouseInputMode::LocalSelection);
|
||||||
|
terminal.sync(window, cx);
|
||||||
|
assert!(display_offset(&terminal.term.lock()) > 0);
|
||||||
|
assert!(terminal.take_input_log().is_empty());
|
||||||
|
assert!(terminal.take_pty_write_log().is_empty());
|
||||||
|
|
||||||
|
terminal.scroll_wheel(&scroll_event, 1.0, MouseInputMode::ReportToTerminal);
|
||||||
|
assert!(!terminal.take_pty_write_log().is_empty());
|
||||||
|
|
||||||
|
terminal.write_output(b"\x1b[?1049h\x1b[?1007h", cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.update_window_entity(&terminal, |terminal, window, cx| {
|
||||||
|
terminal.sync(window, cx);
|
||||||
|
assert!(
|
||||||
|
terminal
|
||||||
|
.last_content
|
||||||
|
.mode
|
||||||
|
.contains(Modes::ALT_SCREEN | Modes::ALTERNATE_SCROLL)
|
||||||
|
);
|
||||||
|
assert_eq!(display_offset(&terminal.term.lock()), 0);
|
||||||
|
|
||||||
|
let scroll_event = ScrollWheelEvent {
|
||||||
|
position: point(px(50.0), px(10.0)),
|
||||||
|
delta: ScrollDelta::Lines(point(0.0, 1.0)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
terminal.scroll_wheel(&scroll_event, 1.0, MouseInputMode::LocalSelection);
|
||||||
|
terminal.sync(window, cx);
|
||||||
|
assert_eq!(display_offset(&terminal.term.lock()), 0);
|
||||||
|
assert!(terminal.take_input_log().is_empty());
|
||||||
|
assert!(terminal.take_pty_write_log().is_empty());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// With mouse tracking active (e.g. htop), Shift is the escape hatch to
|
/// With mouse tracking active (e.g. htop), Shift is the escape hatch to
|
||||||
/// select terminal text. Shift+drag must start a selection rather than being
|
/// select terminal text. Shift+drag must start a selection rather than being
|
||||||
/// swallowed as a "extend existing selection" no-op. Regression test for #60254.
|
/// swallowed as a "extend existing selection" no-op. Regression test for #60254.
|
||||||
@@ -4032,6 +4136,7 @@ mod tests {
|
|||||||
click_count: 1,
|
click_count: 1,
|
||||||
first_mouse: true,
|
first_mouse: true,
|
||||||
},
|
},
|
||||||
|
MouseInputMode::ReportToTerminal,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4054,6 +4159,7 @@ mod tests {
|
|||||||
modifiers: shift,
|
modifiers: shift,
|
||||||
},
|
},
|
||||||
region,
|
region,
|
||||||
|
MouseInputMode::ReportToTerminal,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4094,6 +4200,7 @@ mod tests {
|
|||||||
click_count: 1,
|
click_count: 1,
|
||||||
first_mouse: true,
|
first_mouse: true,
|
||||||
},
|
},
|
||||||
|
MouseInputMode::ReportToTerminal,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4822,7 +4929,7 @@ mod tests {
|
|||||||
click_count: 1,
|
click_count: 1,
|
||||||
first_mouse: true,
|
first_mouse: true,
|
||||||
};
|
};
|
||||||
terminal.mouse_down(&mouse_down, cx);
|
terminal.mouse_down(&mouse_down, MouseInputMode::ReportToTerminal, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ctrl_mouse_drag_to(
|
fn ctrl_mouse_drag_to(
|
||||||
@@ -4836,7 +4943,12 @@ mod tests {
|
|||||||
pressed_button: Some(MouseButton::Left),
|
pressed_button: Some(MouseButton::Left),
|
||||||
modifiers: Modifiers::secondary_key(),
|
modifiers: Modifiers::secondary_key(),
|
||||||
};
|
};
|
||||||
terminal.mouse_drag(&drag_event, terminal_bounds, cx);
|
terminal.mouse_drag(
|
||||||
|
&drag_event,
|
||||||
|
terminal_bounds,
|
||||||
|
MouseInputMode::ReportToTerminal,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ctrl_mouse_up_at(
|
fn ctrl_mouse_up_at(
|
||||||
@@ -4850,7 +4962,7 @@ mod tests {
|
|||||||
modifiers: Modifiers::secondary_key(),
|
modifiers: Modifiers::secondary_key(),
|
||||||
click_count: 1,
|
click_count: 1,
|
||||||
};
|
};
|
||||||
terminal.mouse_up(&mouse_up, cx);
|
terminal.mouse_up(&mouse_up, MouseInputMode::ReportToTerminal, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! any_event_matches {
|
macro_rules! any_event_matches {
|
||||||
@@ -4897,6 +5009,38 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_local_selection_activates_hyperlink_in_mouse_mode(cx: &mut TestAppContext) {
|
||||||
|
let terminal = init_terminal_test(cx, b"Visit https://zed.dev/ for more\r\n");
|
||||||
|
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
terminal.last_content.mode = Modes::MOUSE_MODE;
|
||||||
|
let position = point(px(80.0), px(10.0));
|
||||||
|
let mouse_down = MouseDownEvent {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
position,
|
||||||
|
modifiers: Modifiers::secondary_key(),
|
||||||
|
click_count: 1,
|
||||||
|
first_mouse: true,
|
||||||
|
};
|
||||||
|
let mouse_up = MouseUpEvent {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
position,
|
||||||
|
modifiers: Modifiers::secondary_key(),
|
||||||
|
click_count: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
terminal.mouse_down(&mouse_down, MouseInputMode::LocalSelection, cx);
|
||||||
|
terminal.mouse_up(&mouse_up, MouseInputMode::LocalSelection, cx);
|
||||||
|
|
||||||
|
assert!(any_event_matches!(
|
||||||
|
terminal,
|
||||||
|
InternalEvent::ProcessHyperlink(_, true)
|
||||||
|
));
|
||||||
|
assert!(terminal.take_pty_write_log().is_empty());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_hyperlink_ctrl_click_mismatch_in_mouse_mode_consumes_gesture(
|
async fn test_hyperlink_ctrl_click_mismatch_in_mouse_mode_consumes_gesture(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
@@ -4920,6 +5064,7 @@ mod tests {
|
|||||||
pressed_button: Some(MouseButton::Left),
|
pressed_button: Some(MouseButton::Left),
|
||||||
modifiers: Modifiers::secondary_key(),
|
modifiers: Modifiers::secondary_key(),
|
||||||
},
|
},
|
||||||
|
MouseInputMode::ReportToTerminal,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
ctrl_mouse_up_at(terminal, up_position, cx);
|
ctrl_mouse_up_at(terminal, up_position, cx);
|
||||||
@@ -5205,7 +5350,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
self.window.simulate_mouse_move(position, self.cx);
|
self.window.simulate_mouse_move(position, self.cx);
|
||||||
self.unthrottle();
|
self.unthrottle();
|
||||||
self.terminal.mouse_move(&move_event, self.cx);
|
self.terminal
|
||||||
|
.mouse_move(&move_event, MouseInputMode::ReportToTerminal, self.cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_modifiers_change(&mut self, modifiers: Modifiers) {
|
fn try_modifiers_change(&mut self, modifiers: Modifiers) {
|
||||||
@@ -5752,6 +5898,7 @@ mod tests {
|
|||||||
position,
|
position,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
MouseInputMode::ReportToTerminal,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5762,6 +5909,7 @@ mod tests {
|
|||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
1.0,
|
1.0,
|
||||||
|
MouseInputMode::ReportToTerminal,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use editor::{CursorLayout, EditorSettings, HighlightedRange, HighlightedRangeLine};
|
use editor::{CursorLayout, EditorSettings, HighlightedRange, HighlightedRangeLine};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase,
|
AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Element,
|
||||||
Element, ElementId, Entity, FocusHandle, Font, FontFeatures, FontStyle, FontWeight,
|
ElementId, Entity, FocusHandle, Font, FontFeatures, FontStyle, FontWeight, GlobalElementId,
|
||||||
GlobalElementId, HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity,
|
HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, IntoElement,
|
||||||
IntoElement, LayoutId, Length, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels,
|
LayoutId, Length, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels,
|
||||||
Point as GpuiPoint, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle,
|
Point as GpuiPoint, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle,
|
||||||
UTF16Selection, UnderlineStyle, WeakEntity, WhiteSpace, Window, div, fill, point, px, relative,
|
UTF16Selection, UnderlineStyle, WeakEntity, WhiteSpace, Window, div, fill, point, px, relative,
|
||||||
size,
|
size,
|
||||||
@@ -13,9 +13,10 @@ use language::CursorShape as EditorCursorShape;
|
|||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use terminal::{
|
use terminal::{
|
||||||
Cell, Color, Content, CursorShape, IndexedCell, Modes, NamedColor, Point, Range, Terminal,
|
Cell, Color, Content, CursorShape, IndexedCell, Modes, MouseInputMode, NamedColor, Point,
|
||||||
TerminalBounds, is_app_chosen_exact_color as terminal_is_app_chosen_exact_color,
|
Range, Terminal, TerminalBounds,
|
||||||
is_default_background_color, terminal_settings::TerminalSettings,
|
is_app_chosen_exact_color as terminal_is_app_chosen_exact_color, is_default_background_color,
|
||||||
|
terminal_settings::TerminalSettings,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, Theme};
|
use theme::{ActiveTheme, Theme};
|
||||||
use theme_settings::ThemeSettings;
|
use theme_settings::ThemeSettings;
|
||||||
@@ -40,7 +41,6 @@ pub struct LayoutState {
|
|||||||
ime_cursor_bounds: Option<Bounds<Pixels>>,
|
ime_cursor_bounds: Option<Bounds<Pixels>>,
|
||||||
background_color: Hsla,
|
background_color: Hsla,
|
||||||
dimensions: TerminalBounds,
|
dimensions: TerminalBounds,
|
||||||
mode: Modes,
|
|
||||||
display_offset: usize,
|
display_offset: usize,
|
||||||
hyperlink_tooltip: Option<AnyElement>,
|
hyperlink_tooltip: Option<AnyElement>,
|
||||||
block_below_cursor_element: Option<AnyElement>,
|
block_below_cursor_element: Option<AnyElement>,
|
||||||
@@ -940,31 +940,11 @@ impl TerminalElement {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generic_button_handler<E>(
|
|
||||||
connection: Entity<Terminal>,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
steal_focus: bool,
|
|
||||||
f: impl Fn(&mut Terminal, &E, &mut Context<Terminal>),
|
|
||||||
) -> impl Fn(&E, &mut Window, &mut App) {
|
|
||||||
move |event, window, cx| {
|
|
||||||
if steal_focus {
|
|
||||||
window.focus(&focus_handle, cx);
|
|
||||||
} else if !focus_handle.is_focused(window) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connection.update(cx, |terminal, cx| {
|
|
||||||
f(terminal, event, cx);
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_mouse_listeners(
|
fn register_mouse_listeners(
|
||||||
&mut self,
|
&mut self,
|
||||||
mode: Modes,
|
|
||||||
hitbox: &Hitbox,
|
hitbox: &Hitbox,
|
||||||
content_mode: &ContentMode,
|
content_mode: &ContentMode,
|
||||||
|
mouse_input_mode: MouseInputMode,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
) {
|
) {
|
||||||
let focus = self.focus.clone();
|
let focus = self.focus.clone();
|
||||||
@@ -979,13 +959,14 @@ impl TerminalElement {
|
|||||||
move |e, window, cx| {
|
move |e, window, cx| {
|
||||||
window.focus(&focus, cx);
|
window.focus(&focus, cx);
|
||||||
|
|
||||||
let scroll_top = terminal_view.read(cx).scroll_top;
|
let view = terminal_view.read(cx);
|
||||||
|
let scroll_top = view.scroll_top;
|
||||||
terminal.update(cx, |terminal, cx| {
|
terminal.update(cx, |terminal, cx| {
|
||||||
let mut adjusted_event = e.clone();
|
let mut adjusted_event = e.clone();
|
||||||
if scroll_top > Pixels::ZERO {
|
if scroll_top > Pixels::ZERO {
|
||||||
adjusted_event.position.y += scroll_top;
|
adjusted_event.position.y += scroll_top;
|
||||||
}
|
}
|
||||||
terminal.mouse_down(&adjusted_event, cx);
|
terminal.mouse_down(&adjusted_event, mouse_input_mode, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -995,7 +976,6 @@ impl TerminalElement {
|
|||||||
let terminal = self.terminal.clone();
|
let terminal = self.terminal.clone();
|
||||||
let hitbox = hitbox.clone();
|
let hitbox = hitbox.clone();
|
||||||
let focus = focus.clone();
|
let focus = focus.clone();
|
||||||
let terminal_view = terminal_view;
|
|
||||||
move |e: &MouseMoveEvent, phase, window, cx| {
|
move |e: &MouseMoveEvent, phase, window, cx| {
|
||||||
if phase != DispatchPhase::Bubble {
|
if phase != DispatchPhase::Bubble {
|
||||||
return;
|
return;
|
||||||
@@ -1011,7 +991,12 @@ impl TerminalElement {
|
|||||||
if scroll_top > Pixels::ZERO {
|
if scroll_top > Pixels::ZERO {
|
||||||
adjusted_event.position.y += scroll_top;
|
adjusted_event.position.y += scroll_top;
|
||||||
}
|
}
|
||||||
terminal.mouse_drag(&adjusted_event, hitbox.bounds, cx);
|
terminal.mouse_drag(
|
||||||
|
&adjusted_event,
|
||||||
|
hitbox.bounds,
|
||||||
|
mouse_input_mode,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1019,34 +1004,60 @@ impl TerminalElement {
|
|||||||
|
|
||||||
if hitbox.is_hovered(window) {
|
if hitbox.is_hovered(window) {
|
||||||
terminal.update(cx, |terminal, cx| {
|
terminal.update(cx, |terminal, cx| {
|
||||||
terminal.mouse_move(e, cx);
|
terminal.mouse_move(e, mouse_input_mode, cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.interactivity.on_mouse_up(
|
for button in [MouseButton::Left, MouseButton::Middle, MouseButton::Right] {
|
||||||
MouseButton::Left,
|
self.interactivity.on_mouse_up(button, {
|
||||||
TerminalElement::generic_button_handler(
|
let terminal = terminal.clone();
|
||||||
terminal.clone(),
|
let focus = focus.clone();
|
||||||
focus.clone(),
|
move |event, window, cx| {
|
||||||
false,
|
if !focus.is_focused(window) {
|
||||||
move |terminal, e, cx| {
|
return;
|
||||||
terminal.mouse_up(e, cx);
|
}
|
||||||
},
|
if button != MouseButton::Left
|
||||||
),
|
&& (mouse_input_mode == MouseInputMode::LocalSelection
|
||||||
);
|
|| !terminal
|
||||||
self.interactivity.on_mouse_down(
|
.read(cx)
|
||||||
MouseButton::Middle,
|
.last_content
|
||||||
TerminalElement::generic_button_handler(
|
.mode
|
||||||
terminal.clone(),
|
.intersects(Modes::MOUSE_MODE))
|
||||||
focus.clone(),
|
{
|
||||||
true,
|
return;
|
||||||
move |terminal, e, cx| {
|
}
|
||||||
terminal.mouse_down(e, cx);
|
terminal.update(cx, |terminal, cx| {
|
||||||
},
|
terminal.mouse_up(event, mouse_input_mode, cx);
|
||||||
),
|
cx.notify();
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for button in [MouseButton::Middle, MouseButton::Right] {
|
||||||
|
self.interactivity.on_mouse_down(button, {
|
||||||
|
let terminal = terminal.clone();
|
||||||
|
let focus = focus.clone();
|
||||||
|
move |event, window, cx| {
|
||||||
|
if mouse_input_mode == MouseInputMode::LocalSelection
|
||||||
|
|| (button == MouseButton::Right
|
||||||
|
&& !terminal
|
||||||
|
.read(cx)
|
||||||
|
.last_content
|
||||||
|
.mode
|
||||||
|
.intersects(Modes::MOUSE_MODE))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.focus(&focus, cx);
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
terminal.mouse_down(event, mouse_input_mode, cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if content_mode.is_scrollable() {
|
if content_mode.is_scrollable() {
|
||||||
self.interactivity.on_scroll_wheel({
|
self.interactivity.on_scroll_wheel({
|
||||||
@@ -1065,44 +1076,6 @@ impl TerminalElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse mode handlers:
|
|
||||||
// All mouse modes need the extra click handlers
|
|
||||||
if mode.intersects(Modes::MOUSE_MODE) {
|
|
||||||
self.interactivity.on_mouse_down(
|
|
||||||
MouseButton::Right,
|
|
||||||
TerminalElement::generic_button_handler(
|
|
||||||
terminal.clone(),
|
|
||||||
focus.clone(),
|
|
||||||
true,
|
|
||||||
move |terminal, e, cx| {
|
|
||||||
terminal.mouse_down(e, cx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.interactivity.on_mouse_up(
|
|
||||||
MouseButton::Right,
|
|
||||||
TerminalElement::generic_button_handler(
|
|
||||||
terminal.clone(),
|
|
||||||
focus.clone(),
|
|
||||||
false,
|
|
||||||
move |terminal, e, cx| {
|
|
||||||
terminal.mouse_up(e, cx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.interactivity.on_mouse_up(
|
|
||||||
MouseButton::Middle,
|
|
||||||
TerminalElement::generic_button_handler(
|
|
||||||
terminal,
|
|
||||||
focus,
|
|
||||||
false,
|
|
||||||
move |terminal, e, cx| {
|
|
||||||
terminal.mouse_up(e, cx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
|
fn rem_size(&self, cx: &mut App) -> Option<Pixels> {
|
||||||
@@ -1395,14 +1368,12 @@ impl Element for TerminalElement {
|
|||||||
|
|
||||||
let Content {
|
let Content {
|
||||||
cells,
|
cells,
|
||||||
mode,
|
|
||||||
display_offset,
|
display_offset,
|
||||||
cursor_char,
|
cursor_char,
|
||||||
selection,
|
selection,
|
||||||
cursor,
|
cursor,
|
||||||
..
|
..
|
||||||
} = &self.terminal.read(cx).last_content;
|
} = &self.terminal.read(cx).last_content;
|
||||||
let mode = *mode;
|
|
||||||
let display_offset = *display_offset;
|
let display_offset = *display_offset;
|
||||||
|
|
||||||
// searches, highlights to a single range representations
|
// searches, highlights to a single range representations
|
||||||
@@ -1589,7 +1560,6 @@ impl Element for TerminalElement {
|
|||||||
dimensions,
|
dimensions,
|
||||||
rects,
|
rects,
|
||||||
relative_highlighted_ranges,
|
relative_highlighted_ranges,
|
||||||
mode,
|
|
||||||
display_offset,
|
display_offset,
|
||||||
hyperlink_tooltip,
|
hyperlink_tooltip,
|
||||||
block_below_cursor_element,
|
block_below_cursor_element,
|
||||||
@@ -1612,7 +1582,9 @@ impl Element for TerminalElement {
|
|||||||
) {
|
) {
|
||||||
let paint_start = Instant::now();
|
let paint_start = Instant::now();
|
||||||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||||
let scroll_top = self.terminal_view.read(cx).scroll_top;
|
let terminal_view = self.terminal_view.read(cx);
|
||||||
|
let scroll_top = terminal_view.scroll_top;
|
||||||
|
let mouse_input_mode = terminal_view.mouse_input_mode();
|
||||||
|
|
||||||
window.paint_quad(fill(bounds, layout.background_color));
|
window.paint_quad(fill(bounds, layout.background_color));
|
||||||
let origin = layout.dimensions.bounds.origin - GpuiPoint::new(px(0.), scroll_top);
|
let origin = layout.dimensions.bounds.origin - GpuiPoint::new(px(0.), scroll_top);
|
||||||
@@ -1634,9 +1606,9 @@ impl Element for TerminalElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.register_mouse_listeners(
|
self.register_mouse_listeners(
|
||||||
layout.mode,
|
|
||||||
&layout.hitbox,
|
&layout.hitbox,
|
||||||
&layout.content_mode,
|
&layout.content_mode,
|
||||||
|
mouse_input_mode,
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
if window.modifiers().secondary()
|
if window.modifiers().secondary()
|
||||||
@@ -1799,10 +1771,13 @@ struct TerminalInputHandler {
|
|||||||
impl InputHandler for TerminalInputHandler {
|
impl InputHandler for TerminalInputHandler {
|
||||||
fn selected_text_range(
|
fn selected_text_range(
|
||||||
&mut self,
|
&mut self,
|
||||||
_ignore_disabled_input: bool,
|
ignore_disabled_input: bool,
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
_cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<UTF16Selection> {
|
) -> Option<UTF16Selection> {
|
||||||
|
if self.terminal_view.read(cx).is_read_only() && !ignore_disabled_input {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
// Always return a valid selection for IME positioning,
|
// Always return a valid selection for IME positioning,
|
||||||
// even in ALT_SCREEN mode (fullscreen TUI apps like opencode, vim, etc.)
|
// even in ALT_SCREEN mode (fullscreen TUI apps like opencode, vim, etc.)
|
||||||
// The terminal still has a cursor position that should be used for IME candidate window placement.
|
// The terminal still has a cursor position that should be used for IME candidate window placement.
|
||||||
@@ -1837,6 +1812,9 @@ impl InputHandler for TerminalInputHandler {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
|
if self.terminal_view.read(cx).is_read_only() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.terminal_view.update(cx, |view, view_cx| {
|
self.terminal_view.update(cx, |view, view_cx| {
|
||||||
view.clear_marked_text(view_cx);
|
view.clear_marked_text(view_cx);
|
||||||
view.commit_text(text, view_cx);
|
view.commit_text(text, view_cx);
|
||||||
@@ -2030,9 +2008,62 @@ pub fn convert_color(fg: &Color, theme: &Theme) -> Hsla {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{AbsoluteLength, Hsla, font};
|
use gpui::{AbsoluteLength, Hsla, TestAppContext, font};
|
||||||
use ui::utils::apca_contrast;
|
use ui::utils::apca_contrast;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn terminal_input_handler_respects_read_only_mode(cx: &mut TestAppContext) {
|
||||||
|
let (project, workspace, window_handle) = crate::tests::init_test_with_window(cx).await;
|
||||||
|
for read_only in [false, true] {
|
||||||
|
let (_pane, terminal, terminal_view) = crate::tests::add_display_only_terminal(
|
||||||
|
&project,
|
||||||
|
window_handle,
|
||||||
|
true,
|
||||||
|
read_only,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
window_handle
|
||||||
|
.update(cx, |_multi_workspace, window, cx| {
|
||||||
|
let mut input_handler = TerminalInputHandler {
|
||||||
|
terminal_view,
|
||||||
|
workspace: workspace.downgrade(),
|
||||||
|
cursor_bounds: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
input_handler
|
||||||
|
.selected_text_range(false, window, cx)
|
||||||
|
.is_none(),
|
||||||
|
read_only
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
input_handler
|
||||||
|
.selected_text_range(true, window, cx)
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
|
||||||
|
input_handler.replace_and_mark_text_in_range(None, "あ", None, window, cx);
|
||||||
|
assert_eq!(
|
||||||
|
input_handler.marked_text_range(window, cx),
|
||||||
|
(!read_only).then_some(0..1)
|
||||||
|
);
|
||||||
|
|
||||||
|
input_handler.replace_text_in_range(None, "亜", window, cx);
|
||||||
|
assert_eq!(input_handler.marked_text_range(window, cx), None);
|
||||||
|
assert_eq!(
|
||||||
|
terminal.update(cx, |terminal, _| terminal.take_input_log()),
|
||||||
|
if read_only {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec!["亜".as_bytes().to_vec()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.expect("test window should remain open");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_decorative_character() {
|
fn test_is_decorative_character() {
|
||||||
// Box Drawing characters (U+2500 to U+257F)
|
// Box Drawing characters (U+2500 to U+257F)
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ use std::{
|
|||||||
};
|
};
|
||||||
use task::TaskId;
|
use task::TaskId;
|
||||||
use terminal::{
|
use terminal::{
|
||||||
Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Modes, Paste, PasteText, Point, Range,
|
Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Modes, MouseInputMode, Paste,
|
||||||
ScrollLineDown, ScrollLineUp, ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop,
|
PasteText, Point, Range, ScrollLineDown, ScrollLineUp, ScrollPageDown, ScrollPageUp,
|
||||||
Search, ShowCharacterPalette, TaskState, TaskStatus, Terminal, TerminalBounds, ToggleViMode,
|
ScrollToBottom, ScrollToTop, Search, ShowCharacterPalette, TaskState, TaskStatus, Terminal,
|
||||||
|
TerminalBounds, ToggleViMode,
|
||||||
terminal_settings::{CursorShape, TerminalSettings},
|
terminal_settings::{CursorShape, TerminalSettings},
|
||||||
};
|
};
|
||||||
use terminal_element::TerminalElement;
|
use terminal_element::TerminalElement;
|
||||||
@@ -138,6 +139,7 @@ pub struct TerminalView {
|
|||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
blink_manager: Entity<BlinkManager>,
|
blink_manager: Entity<BlinkManager>,
|
||||||
mode: TerminalMode,
|
mode: TerminalMode,
|
||||||
|
read_only: bool,
|
||||||
// Explicit override for whether workspace-specific context menu actions are shown.
|
// Explicit override for whether workspace-specific context menu actions are shown.
|
||||||
// When `None`, visibility is derived from `mode` (hidden for embedded terminals).
|
// When `None`, visibility is derived from `mode` (hidden for embedded terminals).
|
||||||
show_workspace_actions: Option<bool>,
|
show_workspace_actions: Option<bool>,
|
||||||
@@ -290,6 +292,7 @@ impl TerminalView {
|
|||||||
hover: None,
|
hover: None,
|
||||||
hover_tooltip_update: Task::ready(()),
|
hover_tooltip_update: Task::ready(()),
|
||||||
mode: TerminalMode::Standalone,
|
mode: TerminalMode::Standalone,
|
||||||
|
read_only: false,
|
||||||
show_workspace_actions: None,
|
show_workspace_actions: None,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
|
show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
|
||||||
@@ -319,6 +322,25 @@ impl TerminalView {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_read_only(&self) -> bool {
|
||||||
|
self.read_only
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fixes the view's input policy at construction, without restricting producer output
|
||||||
|
/// or local navigation.
|
||||||
|
pub fn with_read_only(mut self, read_only: bool) -> Self {
|
||||||
|
self.read_only = read_only;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_input_mode(&self) -> MouseInputMode {
|
||||||
|
if self.read_only {
|
||||||
|
MouseInputMode::LocalSelection
|
||||||
|
} else {
|
||||||
|
MouseInputMode::ReportToTerminal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Explicitly override whether workspace-specific context menu actions (e.g. creating or
|
/// Explicitly override whether workspace-specific context menu actions (e.g. creating or
|
||||||
/// closing terminal tabs, inline assist) are shown.
|
/// closing terminal tabs, inline assist) are shown.
|
||||||
///
|
///
|
||||||
@@ -371,6 +393,9 @@ impl TerminalView {
|
|||||||
|
|
||||||
/// Sets the marked (pre-edit) text from the IME.
|
/// Sets the marked (pre-edit) text from the IME.
|
||||||
pub(crate) fn set_marked_text(&mut self, text: String, cx: &mut Context<Self>) {
|
pub(crate) fn set_marked_text(&mut self, text: String, cx: &mut Context<Self>) {
|
||||||
|
if self.read_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
return self.clear_marked_text(cx);
|
return self.clear_marked_text(cx);
|
||||||
}
|
}
|
||||||
@@ -395,7 +420,7 @@ impl TerminalView {
|
|||||||
|
|
||||||
/// Commits (sends) the given text to the PTY. Called by InputHandler::replace_text_in_range.
|
/// Commits (sends) the given text to the PTY. Called by InputHandler::replace_text_in_range.
|
||||||
pub(crate) fn commit_text(&mut self, text: &str, cx: &mut Context<Self>) {
|
pub(crate) fn commit_text(&mut self, text: &str, cx: &mut Context<Self>) {
|
||||||
if !text.is_empty() {
|
if !self.read_only && !text.is_empty() {
|
||||||
self.terminal.update(cx, |term, _| {
|
self.terminal.update(cx, |term, _| {
|
||||||
term.input(text.to_string().into_bytes());
|
term.input(text.to_string().into_bytes());
|
||||||
});
|
});
|
||||||
@@ -543,7 +568,7 @@ impl TerminalView {
|
|||||||
})
|
})
|
||||||
.action("Copy", Box::new(Copy))
|
.action("Copy", Box::new(Copy))
|
||||||
.when(
|
.when(
|
||||||
!matches!(self.mode, TerminalMode::Embedded { .. }),
|
!self.read_only && !matches!(self.mode, TerminalMode::Embedded { .. }),
|
||||||
|menu| {
|
|menu| {
|
||||||
menu.action("Paste", Box::new(Paste))
|
menu.action("Paste", Box::new(Paste))
|
||||||
.action("Paste Text", Box::new(PasteText))
|
.action("Paste Text", Box::new(PasteText))
|
||||||
@@ -551,14 +576,16 @@ impl TerminalView {
|
|||||||
)
|
)
|
||||||
.action("Select All", Box::new(SelectAll))
|
.action("Select All", Box::new(SelectAll))
|
||||||
.when(
|
.when(
|
||||||
!matches!(self.mode, TerminalMode::Embedded { .. }),
|
!self.read_only && !matches!(self.mode, TerminalMode::Embedded { .. }),
|
||||||
|menu| menu.action("Clear", Box::new(Clear)),
|
|menu| menu.action("Clear", Box::new(Clear)),
|
||||||
)
|
)
|
||||||
.when(
|
.when(
|
||||||
assistant_enabled && !matches!(self.mode, TerminalMode::Embedded { .. }),
|
assistant_enabled && !matches!(self.mode, TerminalMode::Embedded { .. }),
|
||||||
|menu| {
|
|menu| {
|
||||||
menu.separator()
|
menu.separator()
|
||||||
.action("Inline Assist", Box::new(InlineAssist::default()))
|
.when(!self.read_only, |menu| {
|
||||||
|
menu.action("Inline Assist", Box::new(InlineAssist::default()))
|
||||||
|
})
|
||||||
.when(has_selection && self.shows_workspace_actions(), |menu| {
|
.when(has_selection && self.shows_workspace_actions(), |menu| {
|
||||||
menu.action("Add to Agent Thread", Box::new(AddSelectionToThread))
|
menu.action("Add to Agent Thread", Box::new(AddSelectionToThread))
|
||||||
})
|
})
|
||||||
@@ -633,6 +660,9 @@ impl TerminalView {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if self.read_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self
|
if self
|
||||||
.terminal
|
.terminal
|
||||||
.read(cx)
|
.read(cx)
|
||||||
@@ -657,6 +687,9 @@ impl TerminalView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn rerun_task(&mut self, _: &RerunTask, window: &mut Window, cx: &mut Context<Self>) {
|
fn rerun_task(&mut self, _: &RerunTask, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.read_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let task = self
|
let task = self
|
||||||
.terminal
|
.terminal
|
||||||
.read(cx)
|
.read(cx)
|
||||||
@@ -667,6 +700,9 @@ impl TerminalView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&mut self, _: &Clear, _: &mut Window, cx: &mut Context<Self>) {
|
fn clear(&mut self, _: &Clear, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.read_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.scroll_top = px(0.);
|
self.scroll_top = px(0.);
|
||||||
self.terminal.update(cx, |term, _| term.clear());
|
self.terminal.update(cx, |term, _| term.clear());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -711,6 +747,7 @@ impl TerminalView {
|
|||||||
term.scroll_wheel(
|
term.scroll_wheel(
|
||||||
event,
|
event,
|
||||||
TerminalSettings::get_global(cx).scroll_multiplier.max(0.01),
|
TerminalSettings::get_global(cx).scroll_multiplier.max(0.01),
|
||||||
|
self.mouse_input_mode(),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -910,6 +947,9 @@ impl TerminalView {
|
|||||||
|
|
||||||
///Attempt to paste the clipboard into the terminal
|
///Attempt to paste the clipboard into the terminal
|
||||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.read_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -944,6 +984,9 @@ impl TerminalView {
|
|||||||
|
|
||||||
///Attempt to paste the clipboard text into the terminal
|
///Attempt to paste the clipboard text into the terminal
|
||||||
fn paste_text(&mut self, _: &PasteText, _: &mut Window, cx: &mut Context<Self>) {
|
fn paste_text(&mut self, _: &PasteText, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.read_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -963,6 +1006,9 @@ impl TerminalView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_paths_to_terminal(&self, paths: &[PathBuf], window: &mut Window, cx: &mut App) {
|
pub fn add_paths_to_terminal(&self, paths: &[PathBuf], window: &mut Window, cx: &mut App) {
|
||||||
|
if self.read_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut text = paths
|
let mut text = paths
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|path| Some(format!(" {}", shlex::try_quote(path.to_str()?).ok()?)))
|
.filter_map(|path| Some(format!(" {}", shlex::try_quote(path.to_str()?).ok()?)))
|
||||||
@@ -975,6 +1021,9 @@ impl TerminalView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
|
fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.read_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.clear_bell(cx);
|
self.clear_bell(cx);
|
||||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||||
self.terminal.update(cx, |term, _| {
|
self.terminal.update(cx, |term, _| {
|
||||||
@@ -1085,8 +1134,8 @@ impl TerminalView {
|
|||||||
self.terminal = terminal;
|
self.terminal = terminal;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rerun_button(task: &TaskState) -> Option<IconButton> {
|
fn rerun_button(&self, task: &TaskState) -> Option<IconButton> {
|
||||||
if !task.spawned_task.show_rerun {
|
if self.read_only || !task.spawned_task.show_rerun {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1271,6 +1320,9 @@ impl TerminalView {
|
|||||||
/// updates the cursor locally without sending data to the shell, so there's no
|
/// updates the cursor locally without sending data to the shell, so there's no
|
||||||
/// shell output to automatically trigger a re-render.
|
/// shell output to automatically trigger a re-render.
|
||||||
fn process_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) -> bool {
|
fn process_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) -> bool {
|
||||||
|
if self.read_only && !self.terminal.read(cx).vi_mode_enabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let (handled, vi_mode_enabled) = self.terminal.update(cx, |term, cx| {
|
let (handled, vi_mode_enabled) = self.terminal.update(cx, |term, cx| {
|
||||||
(
|
(
|
||||||
term.try_keystroke(keystroke, TerminalSettings::get_global(cx).option_as_meta),
|
term.try_keystroke(keystroke, TerminalSettings::get_global(cx).option_as_meta),
|
||||||
@@ -1304,7 +1356,9 @@ impl TerminalView {
|
|||||||
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.terminal.update(cx, |terminal, _| {
|
self.terminal.update(cx, |terminal, _| {
|
||||||
terminal.set_cursor_shape(self.cursor_shape);
|
terminal.set_cursor_shape(self.cursor_shape);
|
||||||
terminal.focus_in();
|
if !self.read_only {
|
||||||
|
terminal.focus_in();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let should_blink = match TerminalSettings::get_global(cx).blinking {
|
let should_blink = match TerminalSettings::get_global(cx).blinking {
|
||||||
@@ -1324,7 +1378,9 @@ impl TerminalView {
|
|||||||
fn focus_out(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
fn focus_out(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.blink_manager.update(cx, BlinkManager::disable);
|
self.blink_manager.update(cx, BlinkManager::disable);
|
||||||
self.terminal.update(cx, |terminal, _| {
|
self.terminal.update(cx, |terminal, _| {
|
||||||
terminal.focus_out();
|
if !self.read_only {
|
||||||
|
terminal.focus_out();
|
||||||
|
}
|
||||||
terminal.set_cursor_shape(CursorShape::Hollow);
|
terminal.set_cursor_shape(CursorShape::Hollow);
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -1381,7 +1437,11 @@ impl Render for TerminalView {
|
|||||||
.on_mouse_down(
|
.on_mouse_down(
|
||||||
MouseButton::Right,
|
MouseButton::Right,
|
||||||
cx.listener(|this, event: &MouseDownEvent, window, cx| {
|
cx.listener(|this, event: &MouseDownEvent, window, cx| {
|
||||||
if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) {
|
if !this
|
||||||
|
.terminal
|
||||||
|
.read(cx)
|
||||||
|
.mouse_mode(event.modifiers.shift, this.mouse_input_mode())
|
||||||
|
{
|
||||||
let had_selection = this.terminal.read(cx).last_content.selection.is_some();
|
let had_selection = this.terminal.read(cx).last_content.selection.is_some();
|
||||||
if !had_selection {
|
if !had_selection {
|
||||||
this.terminal.update(cx, |terminal, _| {
|
this.terminal.update(cx, |terminal, _| {
|
||||||
@@ -1482,15 +1542,15 @@ impl Item for TerminalView {
|
|||||||
TaskStatus::Running => (
|
TaskStatus::Running => (
|
||||||
IconName::PlayFilled,
|
IconName::PlayFilled,
|
||||||
Color::Disabled,
|
Color::Disabled,
|
||||||
TerminalView::rerun_button(terminal_task),
|
self.rerun_button(terminal_task),
|
||||||
),
|
),
|
||||||
TaskStatus::Unknown => (
|
TaskStatus::Unknown => (
|
||||||
IconName::Warning,
|
IconName::Warning,
|
||||||
Color::Warning,
|
Color::Warning,
|
||||||
TerminalView::rerun_button(terminal_task),
|
self.rerun_button(terminal_task),
|
||||||
),
|
),
|
||||||
TaskStatus::Completed { success } => {
|
TaskStatus::Completed { success } => {
|
||||||
let rerun_button = TerminalView::rerun_button(terminal_task);
|
let rerun_button = self.rerun_button(terminal_task);
|
||||||
|
|
||||||
if *success {
|
if *success {
|
||||||
(IconName::Check, Color::Success, rerun_button)
|
(IconName::Check, Color::Success, rerun_button)
|
||||||
@@ -1591,6 +1651,9 @@ impl Item for TerminalView {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
if self.read_only {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let Some(project) = self.project.upgrade() else {
|
let Some(project) = self.project.upgrade() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -2233,7 +2296,7 @@ mod tests {
|
|||||||
) {
|
) {
|
||||||
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
||||||
let (_pane, terminal, _terminal_view) =
|
let (_pane, terminal, _terminal_view) =
|
||||||
add_display_only_terminal(&project, window_handle, true, cx);
|
add_display_only_terminal(&project, window_handle, true, false, cx);
|
||||||
|
|
||||||
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
@@ -2253,12 +2316,169 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn read_only_blocks_input_actions_and_preserves_output(cx: &mut TestAppContext) {
|
||||||
|
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
||||||
|
cx.update(load_default_keymap);
|
||||||
|
let rerun_count = Rc::new(std::cell::Cell::new(0));
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.on_action({
|
||||||
|
let rerun_count = rerun_count.clone();
|
||||||
|
move |_: &zed_actions::Rerun, _| rerun_count.set(rerun_count.get() + 1)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let (pane, terminal, terminal_view) =
|
||||||
|
add_display_only_terminal(&project, window_handle, true, true, cx);
|
||||||
|
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
||||||
|
|
||||||
|
for output in [
|
||||||
|
b"provider output".as_slice(),
|
||||||
|
b"\x1b[?1049hprovider alternate-screen output".as_slice(),
|
||||||
|
] {
|
||||||
|
let content = cx.update(|window, cx| {
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
terminal.write_output(output, cx);
|
||||||
|
terminal.sync(window, cx);
|
||||||
|
terminal.get_content()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
cx.simulate_keystrokes("a enter ctrl-v shift-up");
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string("clipboard input".into()));
|
||||||
|
});
|
||||||
|
|
||||||
|
let actions: Vec<Box<dyn Action>> = vec![
|
||||||
|
Box::new(SendText("direct input".into())),
|
||||||
|
Box::new(SendKeystroke("enter".into())),
|
||||||
|
Box::new(Paste),
|
||||||
|
Box::new(editor::actions::Paste),
|
||||||
|
Box::new(PasteText),
|
||||||
|
Box::new(Clear),
|
||||||
|
Box::new(ShowCharacterPalette),
|
||||||
|
Box::new(RerunTask),
|
||||||
|
];
|
||||||
|
for action in actions {
|
||||||
|
cx.update(|window, cx| window.dispatch_action(action, cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
}
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
let paths = ExternalPaths(vec![PathBuf::from(util::path!("/root.txt"))].into());
|
||||||
|
assert!(!pane.update(cx, |pane, cx| {
|
||||||
|
terminal_view.update(cx, |view, cx| view.handle_drop(pane, &paths, window, cx))
|
||||||
|
}));
|
||||||
|
terminal_view.update(cx, |view, cx| {
|
||||||
|
view.add_paths_to_terminal(paths.paths(), window, cx);
|
||||||
|
});
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
terminal.sync(window, cx);
|
||||||
|
assert!(terminal.take_input_log().is_empty());
|
||||||
|
assert!(terminal.take_pty_write_log().is_empty());
|
||||||
|
assert_eq!(terminal.get_content(), content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert_eq!(rerun_count.get(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn read_only_preserves_vi_navigation_without_enabling_input(cx: &mut TestAppContext) {
|
||||||
|
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
||||||
|
let (_pane, terminal, _terminal_view) =
|
||||||
|
add_display_only_terminal(&project, window_handle, true, true, cx);
|
||||||
|
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
terminal.write_output(b"first line\nsecond line\n", cx);
|
||||||
|
terminal.sync(window, cx);
|
||||||
|
});
|
||||||
|
window.dispatch_action(Box::new(ToggleViMode), cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
let cursor = terminal.read_with(&cx, |terminal, _| {
|
||||||
|
assert!(terminal.vi_mode_enabled());
|
||||||
|
terminal.last_content.cursor.point
|
||||||
|
});
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.dispatch_action(Box::new(SendKeystroke("k".into())), cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
terminal.read_with(&cx, |terminal, _| terminal.last_content.cursor.point.line),
|
||||||
|
cursor.line - 1,
|
||||||
|
);
|
||||||
|
cx.simulate_keystrokes("v l");
|
||||||
|
assert!(
|
||||||
|
terminal
|
||||||
|
.read_with(&cx, |terminal, _| terminal
|
||||||
|
.last_content
|
||||||
|
.selection_text
|
||||||
|
.clone())
|
||||||
|
.is_some_and(|text| !text.is_empty()),
|
||||||
|
);
|
||||||
|
cx.simulate_keystrokes("y");
|
||||||
|
assert!(
|
||||||
|
cx.update(|_, cx| cx.read_from_clipboard().and_then(|item| item.text()))
|
||||||
|
.is_some_and(|text| !text.is_empty()),
|
||||||
|
);
|
||||||
|
cx.simulate_keystrokes("i");
|
||||||
|
assert!(!terminal.read_with(&cx, |terminal, _| terminal.vi_mode_enabled()));
|
||||||
|
cx.simulate_keystrokes("a enter");
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.dispatch_action(Box::new(SendKeystroke("enter".into())), cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert!(
|
||||||
|
terminal
|
||||||
|
.update(&mut cx, |terminal, _| terminal.take_input_log())
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn read_only_mouse_selection_and_focus_do_not_report_input(cx: &mut TestAppContext) {
|
||||||
|
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
||||||
|
let (_pane, terminal, _terminal_view) =
|
||||||
|
add_display_only_terminal(&project, window_handle, true, true, cx);
|
||||||
|
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
terminal.write_output(b"\x1b[?1003h\x1b[?1006h\x1b[?1004hhello world\n", cx);
|
||||||
|
terminal.sync(window, cx);
|
||||||
|
terminal.take_pty_write_log();
|
||||||
|
});
|
||||||
|
window.blur(cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
let bounds = terminal.read_with(&cx, |terminal, _| terminal.last_content.terminal_bounds);
|
||||||
|
let start =
|
||||||
|
bounds.bounds.origin + gpui::point(bounds.cell_width * 0.1, bounds.line_height * 0.5);
|
||||||
|
let end =
|
||||||
|
bounds.bounds.origin + gpui::point(bounds.cell_width * 5.1, bounds.line_height * 0.5);
|
||||||
|
cx.simulate_mouse_move(start, None, gpui::Modifiers::default());
|
||||||
|
cx.simulate_mouse_down(start, MouseButton::Left, gpui::Modifiers::default());
|
||||||
|
cx.simulate_mouse_move(end, Some(MouseButton::Left), gpui::Modifiers::default());
|
||||||
|
cx.simulate_mouse_up(end, MouseButton::Left, gpui::Modifiers::default());
|
||||||
|
cx.update(|window, cx| window.dispatch_action(Box::new(Copy), cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
cx.update(|_, cx| cx.read_from_clipboard().and_then(|item| item.text())),
|
||||||
|
Some("hello".into()),
|
||||||
|
);
|
||||||
|
cx.update(|window, cx| window.blur(cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert!(
|
||||||
|
terminal
|
||||||
|
.update(&mut cx, |terminal, _| terminal.take_pty_write_log())
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn shift_up_scrolls_history_in_normal_screen(cx: &mut TestAppContext) {
|
async fn shift_up_scrolls_history_in_normal_screen(cx: &mut TestAppContext) {
|
||||||
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
||||||
cx.update(load_default_keymap);
|
cx.update(load_default_keymap);
|
||||||
let (_pane, terminal, _terminal_view) =
|
let (_pane, terminal, _terminal_view) =
|
||||||
add_display_only_terminal(&project, window_handle, true, cx);
|
add_display_only_terminal(&project, window_handle, true, false, cx);
|
||||||
|
|
||||||
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
@@ -2303,7 +2523,7 @@ mod tests {
|
|||||||
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
||||||
cx.update(load_default_keymap);
|
cx.update(load_default_keymap);
|
||||||
let (_pane, terminal, _terminal_view) =
|
let (_pane, terminal, _terminal_view) =
|
||||||
add_display_only_terminal(&project, window_handle, true, cx);
|
add_display_only_terminal(&project, window_handle, true, false, cx);
|
||||||
|
|
||||||
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
@@ -2335,7 +2555,7 @@ mod tests {
|
|||||||
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
let (project, _workspace, window_handle) = init_test_with_window(cx).await;
|
||||||
cx.update(load_default_keymap);
|
cx.update(load_default_keymap);
|
||||||
let (_pane, terminal, _terminal_view) =
|
let (_pane, terminal, _terminal_view) =
|
||||||
add_display_only_terminal(&project, window_handle, true, cx);
|
add_display_only_terminal(&project, window_handle, true, false, cx);
|
||||||
|
|
||||||
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
@@ -2362,7 +2582,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
let (_pane, terminal, _terminal_view) =
|
let (_pane, terminal, _terminal_view) =
|
||||||
add_display_only_terminal(&project, window_handle, true, cx);
|
add_display_only_terminal(&project, window_handle, true, false, cx);
|
||||||
|
|
||||||
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
@@ -2597,10 +2817,11 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_display_only_terminal(
|
pub(super) fn add_display_only_terminal(
|
||||||
project: &Entity<Project>,
|
project: &Entity<Project>,
|
||||||
window_handle: gpui::WindowHandle<MultiWorkspace>,
|
window_handle: gpui::WindowHandle<MultiWorkspace>,
|
||||||
focus: bool,
|
focus: bool,
|
||||||
|
read_only: bool,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> (Entity<Pane>, Entity<Terminal>, Entity<TerminalView>) {
|
) -> (Entity<Pane>, Entity<Terminal>, Entity<TerminalView>) {
|
||||||
let project = project.clone();
|
let project = project.clone();
|
||||||
@@ -2629,6 +2850,7 @@ mod tests {
|
|||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
.with_read_only(read_only)
|
||||||
});
|
});
|
||||||
|
|
||||||
active_pane.update(cx, |pane, cx| {
|
active_pane.update(cx, |pane, cx| {
|
||||||
@@ -2653,7 +2875,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a worktree with 1 file /root.txt and returns the project, workspace, and window handle.
|
/// Creates a worktree with 1 file /root.txt and returns the project, workspace, and window handle.
|
||||||
async fn init_test_with_window(
|
pub(super) async fn init_test_with_window(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> (
|
) -> (
|
||||||
Entity<Project>,
|
Entity<Project>,
|
||||||
@@ -2853,7 +3075,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (active_pane, terminal, terminal_view) =
|
let (active_pane, terminal, terminal_view) =
|
||||||
add_display_only_terminal(&project, window_handle, false, cx);
|
add_display_only_terminal(&project, window_handle, false, false, cx);
|
||||||
|
|
||||||
let tab_item = window_handle
|
let tab_item = window_handle
|
||||||
.update(cx, |_, window, cx| {
|
.update(cx, |_, window, cx| {
|
||||||
|
|||||||
Reference in new issue
Block a user