From 1977fb05fec112800388cd3afa4c34a0e12e708d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 1 Jun 2026 16:49:31 +0200 Subject: [PATCH] terminal: Add backend-neutral terminal types (#57483) Refactor to isolate alacritty types into the terminal crate and offers comparable apis where needed for similar functionality. However, these are Zed-owned types instead of the dependency leaking across multiple crates, making alacritty an implementation detail of the terminal crate. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- Cargo.lock | 6 +- Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 15 +- crates/agent_servers/Cargo.toml | 10 +- crates/agent_servers/src/acp.rs | 3 +- crates/agent_ui/src/agent_panel.rs | 2 +- crates/agent_ui/src/inline_prompt_editor.rs | 1 - crates/debugger_ui/Cargo.toml | 2 +- .../src/session/running/console.rs | 198 +-- crates/git_ui/Cargo.toml | 3 +- crates/git_ui/src/git_panel.rs | 39 +- crates/repl/Cargo.toml | 1 - crates/repl/src/outputs.rs | 14 +- crates/repl/src/outputs/plain.rs | 185 +- crates/terminal/Cargo.toml | 1 + crates/terminal/src/alacritty.rs | 1089 ++++++++++++ .../hyperlinks.rs} | 240 ++- crates/terminal/src/mappings/colors.rs | 7 +- crates/terminal/src/mappings/keys.rs | 317 ++-- crates/terminal/src/mappings/mod.rs | 6 +- crates/terminal/src/mappings/mouse.rs | 145 +- crates/terminal/src/pty_info.rs | 54 +- crates/terminal/src/terminal.rs | 1584 ++++++++++------- crates/terminal/src/terminal_settings.rs | 23 - crates/terminal_view/README.md | 20 +- crates/terminal_view/src/terminal_element.rs | 372 ++-- .../src/terminal_path_like_target.rs | 6 +- crates/terminal_view/src/terminal_view.rs | 99 +- 28 files changed, 2839 insertions(+), 1604 deletions(-) create mode 100644 crates/terminal/src/alacritty.rs rename crates/terminal/src/{terminal_hyperlinks.rs => alacritty/hyperlinks.rs} (95%) diff --git a/Cargo.lock b/Cargo.lock index fdf2a71db7..9bb127b905 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4967,7 +4967,6 @@ dependencies = [ name = "debugger_ui" version = "0.1.0" dependencies = [ - "alacritty_terminal", "anyhow", "bitflags 2.10.0", "client", @@ -5005,6 +5004,7 @@ dependencies = [ "sysinfo 0.37.2", "task", "tasks_ui", + "terminal", "terminal_view", "text", "theme", @@ -7717,7 +7717,6 @@ name = "git_ui" version = "0.1.0" dependencies = [ "agent_settings", - "alacritty_terminal", "anyhow", "askpass", "buffer_diff", @@ -7763,6 +7762,7 @@ dependencies = [ "sysinfo 0.37.2", "task", "telemetry", + "terminal", "theme", "theme_settings", "time", @@ -15593,7 +15593,6 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" name = "repl" version = "0.1.0" dependencies = [ - "alacritty_terminal", "anyhow", "async-dispatcher", "async-task", @@ -18835,6 +18834,7 @@ dependencies = [ "urlencoding", "util", "util_macros", + "vte", "windows 0.61.3", ] diff --git a/Cargo.toml b/Cargo.toml index 883e0e0448..a4123fbe67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -801,6 +801,7 @@ unindent = "0.2.0" url = "2.2" urlencoding = "2.1.2" uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] } +vte = { version = "0.15.0", features = ["ansi"] } walkdir = "2.5" wasm-encoder = "0.221" wasmparser = "0.221" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 369040375a..f1025494d6 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -3330,8 +3330,7 @@ mod tests { 0, cx.background_executor(), PathStyle::local(), - ) - .unwrap(); + ); builder.subscribe(cx) }); @@ -3411,8 +3410,7 @@ mod tests { 0, cx.background_executor(), PathStyle::local(), - ) - .unwrap(); + ); builder.subscribe(cx) }); @@ -5026,8 +5024,7 @@ mod tests { 0, cx.background_executor(), PathStyle::local(), - ) - .unwrap(); + ); builder.subscribe(cx) }); @@ -5073,8 +5070,7 @@ mod tests { 0, cx.background_executor(), PathStyle::local(), - ) - .unwrap(); + ); builder.subscribe(cx) }); @@ -5134,8 +5130,7 @@ mod tests { 0, cx.background_executor(), PathStyle::local(), - ) - .unwrap(); + ); builder.subscribe(cx) }); diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index e58a0ce81d..cdd691e8de 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -6,7 +6,15 @@ publish.workspace = true license = "GPL-3.0-or-later" [features] -test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] +test-support = [ + "acp_thread/test-support", + "gpui/test-support", + "project/test-support", + "dep:env_logger", + "client/test-support", + "dep:gpui_tokio", + "reqwest_client/test-support", +] e2e = [] [lints] diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 6a26bef322..3285cc3c26 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -4267,7 +4267,7 @@ fn handle_session_notification( 0, cx.background_executor(), thread.project().read(cx).path_style(cx), - )?; + ); let lower = cx.new(|cx| builder.subscribe(cx)); thread.on_terminal_provider_event( TerminalProviderEvent::Created { @@ -4279,7 +4279,6 @@ fn handle_session_notification( }, cx, ); - anyhow::Ok(()) }) .log_err(); } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index a3a6b46d3f..fed230c48c 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -6263,7 +6263,7 @@ impl AgentPanel { cx.entity_id().as_u64(), cx.background_executor(), path_style, - )?; + ); let terminal = cx.new(|cx| builder.subscribe(cx)); let terminal_view = cx.new(|cx| { TerminalView::new( diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 4860b8b576..b5fca12fc5 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -1675,7 +1675,6 @@ mod tests { cx.background_executor(), PathStyle::local(), ) - .unwrap() .subscribe(cx) }) }); diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index ba98df3e37..195d0d8df9 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -26,7 +26,6 @@ test-support = [ ] [dependencies] -alacritty_terminal.workspace = true anyhow.workspace = true bitflags.workspace = true client.workspace = true @@ -64,6 +63,7 @@ settings.workspace = true sysinfo.workspace = true task.workspace = true tasks_ui.workspace = true +terminal.workspace = true terminal_view.workspace = true text.workspace = true theme.workspace = true diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 746c765722..910637343c 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -2,7 +2,6 @@ use super::{ stack_frame_list::{StackFrameList, StackFrameListEvent}, variable_list::VariableList, }; -use alacritty_terminal::vte::ansi; use anyhow::Result; use collections::HashMap; use dap::{CompletionItem, CompletionItemType, OutputEvent}; @@ -24,7 +23,6 @@ use project::{ search_history::{SearchHistory, SearchHistoryCursor}, }; use settings::Settings; -use std::fmt::Write; use std::{ops::Range, rc::Rc, usize}; use theme::Theme; use theme_settings::ThemeSettings; @@ -176,30 +174,14 @@ impl Console { for event in &events { scratch.clear(); - let mut ansi_handler = ConsoleHandler::default(); - let mut ansi_processor = - ansi::Processor::::default(); - let trimmed_output = event.output.trim_end(); - let _ = writeln!(&mut scratch, "{trimmed_output}"); - ansi_processor.advance(&mut ansi_handler, scratch.as_bytes()); - let output = std::mem::take(&mut ansi_handler.output); + scratch.push_str(trimmed_output); + scratch.push('\n'); + let parsed_output = terminal::parse_ansi_text(scratch.as_bytes()); + let output = parsed_output.text; to_insert.extend(output.chars()); - let mut spans = std::mem::take(&mut ansi_handler.spans); - let mut background_spans = - std::mem::take(&mut ansi_handler.background_spans); - if ansi_handler.current_range_start < output.len() { - spans.push(( - ansi_handler.current_range_start..output.len(), - ansi_handler.current_color, - )); - } - if ansi_handler.current_background_range_start < output.len() { - background_spans.push(( - ansi_handler.current_background_range_start..output.len(), - ansi_handler.current_background_color, - )); - } + let mut spans = parsed_output.foreground_spans; + let mut background_spans = parsed_output.background_spans; for (range, _) in spans.iter_mut() { let start_offset = len + range.start; @@ -253,7 +235,7 @@ impl Console { let start_offset = range.start; let range = buffer.anchor_after(MultiBufferOffset(range.start)) ..buffer.anchor_before(MultiBufferOffset(range.end)); - let color_fn = color_fetcher(color); + let color_fn = background_color_fetcher(color); console.highlight_background( HighlightKey::ConsoleAnsiHighlight(start_offset), &[range], @@ -802,153 +784,16 @@ impl ConsoleQueryBarCompletionProvider { } } -#[derive(Default)] -struct ConsoleHandler { - output: String, - spans: Vec<(Range, Option)>, - background_spans: Vec<(Range, Option)>, - current_range_start: usize, - current_background_range_start: usize, - current_color: Option, - current_background_color: Option, - pos: usize, -} - -impl ConsoleHandler { - fn break_span(&mut self, color: Option) { - self.spans.push(( - self.current_range_start..self.output.len(), - self.current_color, - )); - self.current_color = color; - self.current_range_start = self.pos; - } - - fn break_background_span(&mut self, color: Option) { - self.background_spans.push(( - self.current_background_range_start..self.output.len(), - self.current_background_color, - )); - self.current_background_color = color; - self.current_background_range_start = self.pos; - } -} - -impl ansi::Handler for ConsoleHandler { - fn input(&mut self, c: char) { - self.output.push(c); - self.pos += c.len_utf8(); - } - - fn linefeed(&mut self) { - self.output.push('\n'); - self.pos += 1; - } - - fn put_tab(&mut self, count: u16) { - self.output - .extend(std::iter::repeat('\t').take(count as usize)); - self.pos += count as usize; - } - - fn terminal_attribute(&mut self, attr: ansi::Attr) { - match attr { - ansi::Attr::Foreground(color) => { - self.break_span(Some(color)); - } - ansi::Attr::Background(color) => { - self.break_background_span(Some(color)); - } - ansi::Attr::Reset => { - self.break_span(None); - self.break_background_span(None); - } - _ => {} +fn background_color_fetcher(color: terminal::Color) -> impl Fn(&Theme) -> Hsla { + move |theme| { + if terminal::is_default_background_color(color) { + theme.colors().terminal_background + } else { + terminal_view::terminal_element::convert_color(&color, theme) } } } -fn color_fetcher(color: ansi::Color) -> fn(&Theme) -> Hsla { - let color_fetcher: fn(&Theme) -> Hsla = match color { - // Named and theme defined colors - ansi::Color::Named(n) => match n { - ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black, - ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red, - ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green, - ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow, - ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue, - ansi::NamedColor::Magenta => |theme| theme.colors().terminal_ansi_magenta, - ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan, - ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white, - ansi::NamedColor::BrightBlack => |theme| theme.colors().terminal_ansi_bright_black, - ansi::NamedColor::BrightRed => |theme| theme.colors().terminal_ansi_bright_red, - ansi::NamedColor::BrightGreen => |theme| theme.colors().terminal_ansi_bright_green, - ansi::NamedColor::BrightYellow => |theme| theme.colors().terminal_ansi_bright_yellow, - ansi::NamedColor::BrightBlue => |theme| theme.colors().terminal_ansi_bright_blue, - ansi::NamedColor::BrightMagenta => |theme| theme.colors().terminal_ansi_bright_magenta, - ansi::NamedColor::BrightCyan => |theme| theme.colors().terminal_ansi_bright_cyan, - ansi::NamedColor::BrightWhite => |theme| theme.colors().terminal_ansi_bright_white, - ansi::NamedColor::Foreground => |theme| theme.colors().terminal_foreground, - ansi::NamedColor::Background => |theme| theme.colors().terminal_background, - ansi::NamedColor::Cursor => |theme| theme.players().local().cursor, - ansi::NamedColor::DimBlack => |theme| theme.colors().terminal_ansi_dim_black, - ansi::NamedColor::DimRed => |theme| theme.colors().terminal_ansi_dim_red, - ansi::NamedColor::DimGreen => |theme| theme.colors().terminal_ansi_dim_green, - ansi::NamedColor::DimYellow => |theme| theme.colors().terminal_ansi_dim_yellow, - ansi::NamedColor::DimBlue => |theme| theme.colors().terminal_ansi_dim_blue, - ansi::NamedColor::DimMagenta => |theme| theme.colors().terminal_ansi_dim_magenta, - ansi::NamedColor::DimCyan => |theme| theme.colors().terminal_ansi_dim_cyan, - ansi::NamedColor::DimWhite => |theme| theme.colors().terminal_ansi_dim_white, - ansi::NamedColor::BrightForeground => |theme| theme.colors().terminal_bright_foreground, - ansi::NamedColor::DimForeground => |theme| theme.colors().terminal_dim_foreground, - }, - // 'True' colors - ansi::Color::Spec(_) => |theme| theme.colors().editor_background, - // 8 bit, indexed colors - ansi::Color::Indexed(i) => { - match i { - // 0-15 are the same as the named colors above - 0 => |theme| theme.colors().terminal_ansi_black, - 1 => |theme| theme.colors().terminal_ansi_red, - 2 => |theme| theme.colors().terminal_ansi_green, - 3 => |theme| theme.colors().terminal_ansi_yellow, - 4 => |theme| theme.colors().terminal_ansi_blue, - 5 => |theme| theme.colors().terminal_ansi_magenta, - 6 => |theme| theme.colors().terminal_ansi_cyan, - 7 => |theme| theme.colors().terminal_ansi_white, - 8 => |theme| theme.colors().terminal_ansi_bright_black, - 9 => |theme| theme.colors().terminal_ansi_bright_red, - 10 => |theme| theme.colors().terminal_ansi_bright_green, - 11 => |theme| theme.colors().terminal_ansi_bright_yellow, - 12 => |theme| theme.colors().terminal_ansi_bright_blue, - 13 => |theme| theme.colors().terminal_ansi_bright_magenta, - 14 => |theme| theme.colors().terminal_ansi_bright_cyan, - 15 => |theme| theme.colors().terminal_ansi_bright_white, - // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm. - // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl - // 16..=231 => { - // let (r, g, b) = rgb_for_index(index as u8); - // rgba_color( - // if r == 0 { 0 } else { r * 40 + 55 }, - // if g == 0 { 0 } else { g * 40 + 55 }, - // if b == 0 { 0 } else { b * 40 + 55 }, - // ) - // } - // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238). - // 232..=255 => { - // let i = index as u8 - 232; // Align index to 0..24 - // let value = i * 10 + 8; - // rgba_color(value, value, value) - // } - // For compatibility with the alacritty::Colors interface - // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs - _ => |_| gpui::black(), - } - } - }; - color_fetcher -} - #[cfg(test)] mod tests { use super::*; @@ -996,6 +841,23 @@ mod tests { pretty_assertions::assert_eq!(expect, cx.display_text()); } + #[gpui::test] + fn test_background_color_fetcher_preserves_default_background(cx: &mut TestAppContext) { + init_test(cx); + + cx.update(|cx| { + let mut theme = theme::GlobalTheme::theme(cx).as_ref().clone(); + theme.styles.colors.terminal_background = gpui::red(); + theme.styles.colors.terminal_ansi_background = gpui::blue(); + + let color = background_color_fetcher(terminal::Color::Named( + terminal::NamedColor::Background, + ))(&theme); + + assert_eq!(color, gpui::red()); + }); + } + #[gpui::test] async fn test_determine_completion_replace_range(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 348f41371e..3273277c79 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -17,7 +17,6 @@ test-support = ["multi_buffer/test-support", "remote_connection/test-support"] [dependencies] agent_settings.workspace = true -alacritty_terminal.workspace = true anyhow.workspace = true askpass.workspace = true buffer_diff.workspace = true @@ -59,6 +58,7 @@ smallvec.workspace = true strum.workspace = true sysinfo.workspace = true telemetry.workspace = true +terminal.workspace = true theme.workspace = true theme_settings.workspace = true time.workspace = true @@ -72,6 +72,7 @@ zed_actions.workspace = true zeroize.workspace = true ztracing.workspace = true tracing.workspace = true + [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 84c1cc63f6..b8d0677fa2 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -11,7 +11,6 @@ use crate::{ git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, }; use agent_settings::{AgentSettings, UserAgentsMd}; -use alacritty_terminal::vte::ansi; use anyhow::Context as _; use askpass::AskPassDelegate; use collections::{BTreeMap, HashMap, HashSet}; @@ -7357,10 +7356,7 @@ pub(crate) fn open_output( ) { let operation = operation.into(); - let mut handler = GitOutputHandler::default(); - let mut processor = ansi::Processor::::default(); - processor.advance(&mut handler, output.as_bytes()); - let plain_text = handler.output; + let plain_text = terminal::strip_ansi_text(output.as_bytes()); let buffer = cx.new(|cx| Buffer::local(plain_text.as_str(), cx)); buffer.update(cx, |buffer, cx| { @@ -7378,32 +7374,6 @@ pub(crate) fn open_output( workspace.add_item_to_center(Box::new(editor), window, cx); } -#[derive(Default)] -struct GitOutputHandler { - output: String, - line_start: usize, -} - -impl ansi::Handler for GitOutputHandler { - fn input(&mut self, c: char) { - self.output.push(c); - } - - fn linefeed(&mut self) { - self.output.push('\n'); - self.line_start = self.output.len(); - } - - fn carriage_return(&mut self) { - self.output.truncate(self.line_start); - } - - fn put_tab(&mut self, count: u16) { - self.output - .extend(std::iter::repeat_n('\t', count as usize)); - } -} - pub(crate) fn show_error_toast( workspace: Entity, action: impl Into, @@ -8960,8 +8930,6 @@ mod tests { #[test] fn test_git_output_handler_strips_ansi_codes() { - use alacritty_terminal::vte::ansi; - let cases = [ ("no escape codes here\n", "no escape codes here\n"), ("\x1b[31mhello\x1b[0m", "hello"), @@ -8970,10 +8938,7 @@ mod tests { ]; for (input, expected) in cases { - let mut handler = GitOutputHandler::default(); - let mut processor = ansi::Processor::::default(); - processor.advance(&mut handler, input.as_bytes()); - assert_eq!(handler.output, expected); + assert_eq!(terminal::strip_ansi_text(input.as_bytes()), expected); } } diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml index 25396be336..1c7d52da4d 100644 --- a/crates/repl/Cargo.toml +++ b/crates/repl/Cargo.toml @@ -16,7 +16,6 @@ path = "src/repl.rs" doctest = false [dependencies] -alacritty_terminal.workspace = true anyhow.workspace = true async-dispatcher.workspace = true async-task.workspace = true diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index ad0bd56858..b4fb634d69 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -143,14 +143,14 @@ impl Output { pub fn to_nbformat(&self, cx: &App) -> Option { match self { Output::Stream { content } => { - let text = content.read(cx).full_text(); + let text = content.read(cx).full_text(cx); Some(nbformat::v4::Output::Stream { name: "stdout".to_string(), text: nbformat::v4::MultilineString(text), }) } Output::Plain { content, .. } => { - let text = content.read(cx).full_text(); + let text = content.read(cx).full_text(cx); let mut data = jupyter_protocol::media::Media::default(); data.content.push(jupyter_protocol::MediaType::Plain(text)); Some(nbformat::v4::Output::DisplayData( @@ -161,7 +161,7 @@ impl Output { )) } Output::ErrorOutput(error_view) => { - let traceback_text = error_view.traceback.read(cx).full_text(); + let traceback_text = error_view.traceback.read(cx).full_text(cx); let traceback_lines: Vec = traceback_text.lines().map(|s| s.to_string()).collect(); Some(nbformat::v4::Output::Error(nbformat::v4::ErrorOutput { @@ -319,7 +319,7 @@ impl Output { let ename = err.ename.clone(); let evalue = err.evalue.clone(); let traceback = err.traceback.clone(); - let traceback_text = traceback.read(cx).full_text(); + let traceback_text = traceback.read(cx).full_text(cx); let full_error = format!("{}: {}\n{}", ename, evalue, traceback_text); CopyButton::new("copy-full-error", full_error) @@ -338,7 +338,7 @@ impl Output { let traceback = err.traceback.clone(); move |_, window, cx| { if let Some(workspace) = workspace.upgrade() { - let traceback_text = traceback.read(cx).full_text(); + let traceback_text = traceback.read(cx).full_text(cx); let full_error = format!("{}: {}\n{}", ename, evalue, traceback_text); let buffer = cx.new(|cx| { @@ -698,7 +698,7 @@ impl ExecutionView { _ => return None, }; - let text = content.read(cx).full_text(); + let text = content.read(cx).full_text(cx); let trimmed = text.trim(); let max_length = ReplSettings::get_global(cx).inline_output_max_length; @@ -742,7 +742,7 @@ impl ExecutionView { fn output_as_stream_text(&self, cx: &App) -> Option { self.outputs.iter().find_map(|output| { if let Output::Stream { content } = output { - Some(content.read(cx).full_text()) + Some(content.read(cx).full_text(cx)) } else { None } diff --git a/crates/repl/src/outputs/plain.rs b/crates/repl/src/outputs/plain.rs index bc6d04019c..c127d625e2 100644 --- a/crates/repl/src/outputs/plain.rs +++ b/crates/repl/src/outputs/plain.rs @@ -1,8 +1,8 @@ //! # Plain Text Output //! //! This module provides functionality for rendering plain text output in a terminal-like format. -//! It uses the Alacritty terminal emulator backend to process and display text, supporting -//! ANSI escape sequences for formatting, colors, and other terminal features. +//! It uses Zed's terminal emulator to process and display text, supporting ANSI escape +//! sequences for formatting, colors, and other terminal features. //! //! The main component of this module is the `TerminalOutput` struct, which handles the parsing //! and rendering of text input, simulating a basic terminal environment within REPL output. @@ -15,20 +15,14 @@ //! - Error tracebacks //! -use alacritty_terminal::{ - event::VoidListener, - grid::Dimensions as _, - index::{Column, Line, Point}, - term::Config, - vte::ansi::Processor, -}; use gpui::{Bounds, ClipboardItem, Entity, FontStyle, Pixels, TextStyle, WhiteSpace, canvas, size}; use language::Buffer; use settings::Settings as _; -use terminal::terminal_settings::TerminalSettings; +use terminal::{Terminal, TerminalBuilder, terminal_settings::TerminalSettings}; use terminal_view::terminal_element::TerminalElement; use theme_settings::ThemeSettings; use ui::{IntoElement, prelude::*}; +use util::paths::PathStyle; use crate::outputs::OutputContent; use crate::repl_settings::ReplSettings; @@ -43,15 +37,12 @@ use crate::repl_settings::ReplSettings; /// * text/plain content /// * error tracebacks /// -/// It uses the Alacritty terminal emulator backend to process and render text, +/// It uses Zed's terminal emulator backend to process and render text, /// supporting ANSI escape sequences for text formatting and colors. /// pub struct TerminalOutput { full_buffer: Option>, - /// ANSI escape sequence processor for parsing input text. - parser: Processor, - /// Alacritty terminal instance that manages the terminal state and content. - handler: alacritty_terminal::Term, + terminal: Entity, } /// Returns the default text style for the terminal output. @@ -141,16 +132,21 @@ impl TerminalOutput { /// This method initializes a new terminal emulator with default configuration /// and sets up the necessary components for handling terminal events and rendering. /// - pub fn new(window: &mut Window, cx: &mut App) -> Self { - let term = alacritty_terminal::Term::new( - Config::default(), - &terminal_size(window, cx), - VoidListener, + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let terminal_bounds = terminal_size(window, cx); + let background_executor = cx.background_executor().clone(); + let terminal_builder = TerminalBuilder::new_display_only_with_bounds( + TerminalSettings::get_global(cx).cursor_shape, + TerminalSettings::get_global(cx).alternate_scroll, + None, + 0, + &background_executor, + PathStyle::local(), + terminal_bounds, ); Self { - parser: Processor::new(), - handler: term, + terminal: cx.new(|cx| terminal_builder.subscribe(cx)), full_buffer: None, } } @@ -167,7 +163,7 @@ impl TerminalOutput { /// # Returns /// /// A new instance of `TerminalOutput` containing the provided text. - pub fn from(text: &str, window: &mut Window, cx: &mut App) -> Self { + pub fn from(text: &str, window: &mut Window, cx: &mut Context) -> Self { let mut output = Self::new(window, cx); output.append_text(text, cx); output @@ -199,16 +195,10 @@ impl TerminalOutput { /// # Arguments /// /// * `text` - A string slice containing the text to be appended. - pub fn append_text(&mut self, text: &str, cx: &mut App) { - for byte in text.as_bytes() { - if *byte == b'\n' { - // Dirty (?) hack to move the cursor down - self.parser.advance(&mut self.handler, &[b'\r']); - self.parser.advance(&mut self.handler, &[b'\n']); - } else { - self.parser.advance(&mut self.handler, &[*byte]); - } - } + pub fn append_text(&mut self, text: &str, cx: &mut Context) { + self.terminal.update(cx, |terminal, cx| { + terminal.write_output(text.as_bytes(), cx); + }); // This will keep the buffer up to date, though with some terminal codes it won't be perfect if let Some(buffer) = self.full_buffer.as_ref() { @@ -218,7 +208,11 @@ impl TerminalOutput { } } - pub fn full_text(&self) -> String { + pub fn full_text(&self, cx: &App) -> String { + Self::sanitize_terminal_text(self.terminal.read(cx).get_content()) + } + + fn sanitize_terminal_text(text: String) -> String { fn sanitize(mut line: String) -> Option { line.retain(|ch| ch != '\u{0}' && ch != '\r'); if line.trim().is_empty() { @@ -228,32 +222,10 @@ impl TerminalOutput { Some(trimmed.to_owned()) } - let mut lines = Vec::new(); - - // Get the total number of lines, including history - let total_lines = self.handler.grid().total_lines(); - let visible_lines = self.handler.screen_lines(); - let history_lines = total_lines - visible_lines; - - // Capture history lines in correct order (oldest to newest) - for line in (0..history_lines).rev() { - let line_index = Line(-(line as i32) - 1); - let start = Point::new(line_index, Column(0)); - let end = Point::new(line_index, Column(self.handler.columns() - 1)); - if let Some(cleaned) = sanitize(self.handler.bounds_to_string(start, end)) { - lines.push(cleaned); - } - } - - // Capture visible lines - for line in 0..visible_lines { - let line_index = Line(line as i32); - let start = Point::new(line_index, Column(0)); - let end = Point::new(line_index, Column(self.handler.columns() - 1)); - if let Some(cleaned) = sanitize(self.handler.bounds_to_string(start, end)) { - lines.push(cleaned); - } - } + let lines = text + .lines() + .filter_map(|line| sanitize(line.to_string())) + .collect::>(); if lines.is_empty() { String::new() @@ -311,6 +283,72 @@ mod tests { let expected_f32: f32 = expected.into(); assert!((result_f32 - expected_f32).abs() < 0.01); } + + #[gpui::test] + fn test_append_text_preserves_split_ansi_sequence(cx: &mut TestAppContext) { + let cx = init_test(cx); + let text = cx.update(|window, cx| { + let output = cx.new(|cx| TerminalOutput::new(window, cx)); + output.update(cx, |output, cx| { + output.append_text("\x1b[", cx); + output.append_text("31mred\x1b[0m", cx); + output.full_text(cx) + }) + }); + + assert_eq!(text, "red\n"); + } + + #[gpui::test] + fn test_full_text_reads_terminal_output(cx: &mut TestAppContext) { + let cx = init_test(cx); + cx.update(|window, cx| { + let output = cx.new(|cx| TerminalOutput::new(window, cx)); + output.update(cx, |output, cx| { + output.append_text("hello\n", cx); + assert_eq!(output.full_text(cx), "hello\n"); + }); + }); + } + + #[gpui::test] + fn test_initial_text_uses_repl_terminal_size(cx: &mut TestAppContext) { + let cx = init_test(cx); + let (text, expected) = cx.update(|window, cx| { + let columns = ReplSettings::get_global(cx).max_columns; + let input = format!("\x1b[{columns}Gx"); + let output = cx.new(|cx| TerminalOutput::from(&input, window, cx)); + ( + output.read(cx).full_text(cx), + format!("{}x\n", " ".repeat(columns - 1)), + ) + }); + + assert_eq!(text, expected); + } + + #[gpui::test] + fn test_repl_history_ignores_terminal_scrollback_setting(cx: &mut TestAppContext) { + let cx = init_test(cx); + let (text, expected) = cx.update(|window, cx| { + cx.update_global::(|settings_store, cx| { + settings_store.update_user_settings(cx, |settings| { + settings + .terminal + .get_or_insert_default() + .max_scroll_history_lines = Some(0); + }); + }); + + let input = (0..40) + .map(|line| format!("line-{line}\n")) + .collect::(); + let output = cx.new(|cx| TerminalOutput::from(&input, window, cx)); + (output.read(cx).full_text(cx), input) + }); + + assert_eq!(text, expected); + } } impl Render for TerminalOutput { @@ -320,31 +358,25 @@ impl Render for TerminalOutput { /// the layout of the terminal grid, calculates the dimensions of the output, and /// creates a canvas element that paints the terminal cells and background rectangles. fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let text_style = text_style(window, cx); - let text_system = window.text_system(); + let terminal = self.terminal.clone(); - let grid = self - .handler - .renderable_content() - .display_iter - .map(|ic| terminal::IndexedCell { - point: ic.point, - cell: ic.cell.clone(), - }); + let text_style = text_style(window, cx); let minimum_contrast = TerminalSettings::get_global(cx).minimum_contrast; - let (rects, batched_text_runs) = - TerminalElement::layout_grid(grid, 0, &text_style, None, minimum_contrast, cx); + let (rects, batched_text_runs) = terminal.read(cx).with_renderable_cells(|cells| { + TerminalElement::layout_grid(cells, 0, &text_style, None, minimum_contrast, cx) + }); // lines are 0-indexed, so we must add 1 to get the number of lines let text_line_height = text_style.line_height_in_pixels(window.rem_size()); let num_lines = batched_text_runs .iter() - .map(|b| b.start_point.line) + .map(|b| b.start_point.line()) .max() .unwrap_or(0) + 1; let height = num_lines as f32 * text_line_height; + let text_system = window.text_system(); let font_pixels = text_style.font_size.to_pixels(window.rem_size()); let font_id = text_system.resolve_font(&text_style.font()); @@ -386,12 +418,13 @@ impl Render for TerminalOutput { ) // We must set the height explicitly for the editor block to size itself correctly .h(height) + .into_any_element() } } impl OutputContent for TerminalOutput { fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { - Some(ClipboardItem::new_string(self.full_text())) + Some(ClipboardItem::new_string(self.full_text(_cx))) } fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { @@ -408,8 +441,8 @@ impl OutputContent for TerminalOutput { } let buffer = cx.new(|cx| { - let mut buffer = - Buffer::local(self.full_text(), cx).with_language(language::PLAIN_TEXT.clone(), cx); + let mut buffer = Buffer::local(self.full_text(cx), cx) + .with_language(language::PLAIN_TEXT.clone(), cx); buffer.set_capability(language::Capability::ReadOnly, cx); buffer }); diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index ad1a0753f5..a46453696a 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -43,6 +43,7 @@ thiserror.workspace = true url.workspace = true util.workspace = true urlencoding.workspace = true +vte.workspace = true parking_lot.workspace = true percent-encoding.workspace = true diff --git a/crates/terminal/src/alacritty.rs b/crates/terminal/src/alacritty.rs new file mode 100644 index 0000000000..b37deb6ace --- /dev/null +++ b/crates/terminal/src/alacritty.rs @@ -0,0 +1,1089 @@ +#[cfg(target_os = "windows")] +use std::num::NonZeroU32; +#[cfg(unix)] +use std::os::fd::AsRawFd; +use std::{borrow::Cow, io, ops::RangeInclusive, path::PathBuf, sync::Arc}; + +mod hyperlinks; + +use alacritty_terminal::{ + event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, + event_loop::{EventLoop, Msg, Notifier}, + grid::{Dimensions, Grid, GridIterator, Row, Scroll as AlacScroll}, + index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint}, + selection::{ + Selection as AlacSelection, SelectionRange as AlacSelectionRange, + SelectionType as AlacSelectionType, + }, + sync::FairMutex, + term::{ + Config, Osc52, RenderableCursor, Term, TermMode, + cell::{Cell as AlacCell, Flags, Hyperlink as AlacHyperlink}, + search::{Match, RegexIter, RegexSearch}, + }, + tty, + vi_mode::{ViModeCursor, ViMotion as AlacViMotion}, + vte::ansi::{ + ClearMode, CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle, + NamedPrivateMode, PrivateMode, + }, +}; +use anyhow::{Context as _, Result}; +use futures::channel::mpsc::UnboundedSender; +use util::paths::PathStyle; +use vte::ansi::Handler; +#[cfg(target_os = "windows")] +use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId}; + +use crate::{ + Cell, Color, Content, Cursor, CursorShape, Hyperlink, HyperlinkData, IndexedCell, Modes, Point, + PtyEvent, Range, RenderableCells, Scroll, Search, Selection, SelectionRange, SelectionSide, + SelectionType, TerminalBackendEvent, TerminalBounds, ViMotion, + pty_info::ProcessIdGetter, + terminal_settings::{AlternateScroll, CursorShape as SettingsCursorShape}, +}; + +pub(super) use hyperlinks::{HyperlinkMatch, RegexSearches}; + +pub(super) type AlacrittyPty = tty::Pty; +pub(super) type AlacrittyTerm = Term; +pub(super) type AlacrittyTermConfig = Config; +pub(super) type AlacrittyTermLock = FairMutex; +pub(super) type AlacrittyCell = AlacCell; +pub(super) type AlacrittyGridIterator<'a> = GridIterator<'a, AlacCell>; +pub(super) type AlacrittyHyperlink = AlacHyperlink; + +#[derive(Clone)] +pub(super) struct ZedListener(UnboundedSender); + +#[derive(Clone, Debug)] +pub(super) struct AlacrittySearch { + search: RegexSearch, +} + +#[cfg(unix)] +impl From<&AlacrittyPty> for ProcessIdGetter { + fn from(pty: &AlacrittyPty) -> Self { + Self::new(pty.file().as_raw_fd(), pty.child().id()) + } +} + +#[cfg(windows)] +impl From<&AlacrittyPty> for ProcessIdGetter { + fn from(pty: &AlacrittyPty) -> Self { + let child = pty.child_watcher(); + let handle = child.raw_handle(); + let fallback_pid = child.pid().unwrap_or_else(|| unsafe { + NonZeroU32::new_unchecked(GetProcessId(HANDLE(handle as _))) + }); + + Self::new(handle as i32, u32::from(fallback_pid)) + } +} + +pub(super) struct PtySender { + notifier: Notifier, +} + +impl PtySender { + pub(super) fn notify(&self, input: impl Into>) { + self.notifier.notify(input); + } + + pub(super) fn resize(&self, bounds: TerminalBounds) { + if let Err(error) = self + .notifier + .0 + .send(Msg::Resize(window_size_from_terminal_bounds(bounds))) + { + log::error!("failed to resize alacritty pty: {error}"); + } + } + + pub(super) fn shutdown(&self) { + if let Err(error) = self.notifier.0.send(Msg::Shutdown) { + log::debug!("failed to shut down alacritty pty loop: {error}"); + } + } +} + +fn window_size_from_terminal_bounds(bounds: TerminalBounds) -> WindowSize { + WindowSize { + num_lines: bounds.num_lines() as u16, + num_cols: bounds.num_columns() as u16, + cell_width: f32::from(bounds.cell_width()) as u16, + cell_height: f32::from(bounds.line_height()) as u16, + } +} + +pub(super) fn display_only_term_config( + scrolling_history: usize, + cursor_shape: SettingsCursorShape, +) -> AlacrittyTermConfig { + Config { + scrolling_history, + default_cursor_style: alacritty_cursor_style(cursor_shape), + osc52: Osc52::Disabled, + ..Config::default() + } +} + +pub(super) fn pty_term_config( + scrolling_history: usize, + cursor_shape: SettingsCursorShape, +) -> AlacrittyTermConfig { + Config { + scrolling_history, + default_cursor_style: alacritty_cursor_style(cursor_shape), + ..Config::default() + } +} + +pub(super) fn set_default_cursor_style( + config: &mut AlacrittyTermConfig, + cursor_shape: SettingsCursorShape, +) { + config.default_cursor_style = alacritty_cursor_style(cursor_shape); +} + +pub(super) fn apply_config(term: &AlacrittyTermLock, config: &AlacrittyTermConfig) { + term.lock().set_options(config.clone()); +} + +#[cfg(not(windows))] +pub(super) fn current_child_signal_mask() -> io::Result { + tty::SignalMask::current() +} + +pub(super) fn pty_options( + shell: Option<(String, Vec)>, + working_directory: Option, + env: impl IntoIterator, + #[cfg(not(windows))] child_signal_mask: Option, + #[cfg(windows)] escape_args: bool, +) -> tty::Options { + tty::Options { + shell: shell.map(|(program, args)| tty::Shell::new(program, args)), + working_directory, + drain_on_exit: true, + env: env.into_iter().collect(), + #[cfg(not(windows))] + child_signal_mask, + #[cfg(windows)] + escape_args, + } +} + +pub(super) fn open_pty( + options: &tty::Options, + bounds: TerminalBounds, + window_id: u64, +) -> io::Result { + tty::new(options, window_size_from_terminal_bounds(bounds), window_id) +} + +pub(super) fn new_term( + config: &AlacrittyTermConfig, + bounds: TerminalBounds, + events_tx: UnboundedSender, + alternate_scroll: AlternateScroll, +) -> Arc { + let mut term = Term::new(config.clone(), &bounds, ZedListener(events_tx)); + + if let AlternateScroll::Off = alternate_scroll { + term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); + } + + Arc::new(FairMutex::new(term)) +} + +pub(super) fn spawn_event_loop( + term: Arc, + events_tx: UnboundedSender, + pty: AlacrittyPty, + drain_on_exit: bool, +) -> Result { + let event_loop = EventLoop::new(term, ZedListener(events_tx), pty, drain_on_exit, false) + .context("failed to create event loop")?; + let pty_tx = event_loop.channel(); + let _io_thread = event_loop.spawn(); + + Ok(PtySender { + notifier: Notifier(pty_tx), + }) +} + +pub(super) fn resize(term: &mut AlacrittyTerm, bounds: TerminalBounds) { + term.resize(bounds); +} + +pub(super) fn display_offset(term: &AlacrittyTerm) -> usize { + term.grid().display_offset() +} + +pub(super) fn scroll_display(term: &mut AlacrittyTerm, scroll: Scroll) { + term.scroll_display(scroll.to_alacritty()); +} + +pub(super) fn set_selection(term: &mut AlacrittyTerm, selection: Option<&Selection>) { + term.selection = selection.map(Selection::to_alacritty); +} + +pub(super) fn update_selection( + term: &mut AlacrittyTerm, + point: Point, + side: SelectionSide, +) -> bool { + let Some(mut selection) = term.selection.take() else { + return false; + }; + selection.update(point.to_alacritty(), side.to_alacritty()); + term.selection = Some(selection); + true +} + +pub(super) fn selection_text(term: &AlacrittyTerm) -> Option { + term.selection_to_string() +} + +pub(super) fn scroll_to_point(term: &mut AlacrittyTerm, point: Point) { + term.scroll_to_point(point.to_alacritty()); +} + +pub(super) fn vi_goto_point(term: &mut AlacrittyTerm, point: Point) { + term.vi_goto_point(point.to_alacritty()); +} + +pub(super) fn toggle_vi_mode(term: &mut AlacrittyTerm) { + term.toggle_vi_mode(); +} + +pub(super) fn vi_motion(term: &mut AlacrittyTerm, motion: ViMotion) { + term.vi_motion(motion.to_alacritty()); +} + +fn alacritty_cursor_style(cursor_shape: SettingsCursorShape) -> AlacCursorStyle { + AlacCursorStyle { + shape: alacritty_cursor_shape(cursor_shape), + blinking: false, + } +} + +fn alacritty_cursor_shape(cursor_shape: SettingsCursorShape) -> AlacCursorShape { + match cursor_shape { + SettingsCursorShape::Block => AlacCursorShape::Block, + SettingsCursorShape::Underline => AlacCursorShape::Underline, + SettingsCursorShape::Bar => AlacCursorShape::Beam, + SettingsCursorShape::Hollow => AlacCursorShape::HollowBlock, + } +} + +impl Dimensions for TerminalBounds { + /// Note: this is supposed to be for the back buffer's length, + /// but we exclusively use it to resize the terminal, which does not + /// use this method. We still have to implement it for the trait though, + /// hence, this comment. + fn total_lines(&self) -> usize { + self.screen_lines() + } + + fn screen_lines(&self) -> usize { + self.num_lines() + } + + fn columns(&self) -> usize { + self.num_columns() + } +} + +impl From for TerminalBackendEvent { + fn from(event: AlacTermEvent) -> Self { + match event { + AlacTermEvent::MouseCursorDirty => Self::MouseCursorDirty, + AlacTermEvent::Title(title) => Self::Title(title), + AlacTermEvent::ResetTitle => Self::ResetTitle, + AlacTermEvent::ClipboardStore(_, data) => Self::ClipboardStore(data), + AlacTermEvent::ClipboardLoad(_, format) => Self::ClipboardLoad(format), + AlacTermEvent::ColorRequest(index, format) => Self::ColorRequest(index, format), + AlacTermEvent::PtyWrite(output) => Self::PtyWrite(output), + AlacTermEvent::TextAreaSizeRequest(format) => { + Self::TextAreaSizeRequest(Arc::new(move |bounds| { + format(window_size_from_terminal_bounds(bounds)) + })) + } + AlacTermEvent::CursorBlinkingChange => Self::CursorBlinkingChange, + AlacTermEvent::Wakeup => Self::Wakeup, + AlacTermEvent::Bell => Self::Bell, + AlacTermEvent::Exit => Self::Exit, + AlacTermEvent::ChildExit(status) => Self::ChildExit(status), + } + } +} + +impl EventListener for ZedListener { + fn send_event(&self, event: AlacTermEvent) { + self.0.unbounded_send(PtyEvent::Event(event.into())).ok(); + } +} + +impl Scroll { + fn to_alacritty(self) -> AlacScroll { + match self { + Self::Delta(delta) => AlacScroll::Delta(delta), + Self::PageUp => AlacScroll::PageUp, + Self::PageDown => AlacScroll::PageDown, + Self::Top => AlacScroll::Top, + Self::Bottom => AlacScroll::Bottom, + } + } +} + +impl ViMotion { + fn to_alacritty(self) -> AlacViMotion { + match self { + Self::Up => AlacViMotion::Up, + Self::Down => AlacViMotion::Down, + Self::Left => AlacViMotion::Left, + Self::Right => AlacViMotion::Right, + Self::First => AlacViMotion::First, + Self::Last => AlacViMotion::Last, + Self::FirstOccupied => AlacViMotion::FirstOccupied, + Self::High => AlacViMotion::High, + Self::Middle => AlacViMotion::Middle, + Self::Low => AlacViMotion::Low, + Self::WordLeft => AlacViMotion::WordLeft, + Self::WordRight => AlacViMotion::WordRight, + Self::WordRightEnd => AlacViMotion::WordRightEnd, + Self::Bracket => AlacViMotion::Bracket, + } + } +} + +impl Search { + pub fn new(search: &str) -> Option { + Some(Self { + search: AlacrittySearch { + search: RegexSearch::new(search).ok()?, + }, + }) + } + + fn into_alacritty(self) -> RegexSearch { + self.search.search + } +} + +impl SelectionSide { + fn to_alacritty(self) -> AlacDirection { + match self { + Self::Left => AlacDirection::Left, + Self::Right => AlacDirection::Right, + } + } +} + +impl SelectionType { + fn to_alacritty(self) -> AlacSelectionType { + match self { + Self::Simple => AlacSelectionType::Simple, + Self::Semantic => AlacSelectionType::Semantic, + Self::Lines => AlacSelectionType::Lines, + } + } +} + +impl Selection { + fn to_alacritty(&self) -> AlacSelection { + let mut selection = AlacSelection::new( + self.ty.to_alacritty(), + self.start.point.to_alacritty(), + self.start.side.to_alacritty(), + ); + if self.start.point != self.end.point || self.start.side != self.end.side { + selection.update(self.end.point.to_alacritty(), self.end.side.to_alacritty()); + } + selection + } +} + +impl Hyperlink { + pub fn new(id: Option, uri: String) -> Self { + Self { + data: HyperlinkData::Owned { + id: id.map(|id| Arc::from(id.to_string())), + uri: Arc::from(uri), + }, + } + } + + pub fn id(&self) -> Option<&str> { + match &self.data { + HyperlinkData::Alacritty(hyperlink) => Some(hyperlink.id()), + HyperlinkData::Owned { id, .. } => id.as_deref(), + } + } + + pub fn uri(&self) -> &str { + match &self.data { + HyperlinkData::Alacritty(hyperlink) => hyperlink.uri(), + HyperlinkData::Owned { uri, .. } => uri, + } + } + + fn from_alacritty(hyperlink: AlacHyperlink) -> Self { + Self { + data: HyperlinkData::Alacritty(hyperlink), + } + } +} + +fn terminal_hyperlink_from_alacritty(hyperlink: AlacHyperlink) -> Hyperlink { + Hyperlink::from_alacritty(hyperlink) +} + +impl From for AlacHyperlink { + fn from(hyperlink: Hyperlink) -> Self { + match hyperlink.data { + HyperlinkData::Alacritty(hyperlink) => hyperlink, + HyperlinkData::Owned { id, uri } => Self::new(id.as_deref(), uri.to_string()), + } + } +} + +fn terminal_cell_from_alacritty(cell: &AlacCell) -> Cell { + Cell { cell: cell.clone() } +} + +impl Cell { + #[inline] + pub fn character(&self) -> char { + self.cell.c + } + + #[cfg(test)] + pub(crate) fn set_character(&mut self, character: char) { + self.cell.c = character; + } + + #[inline] + pub fn foreground(&self) -> Color { + self.cell.fg + } + + #[inline] + pub fn background(&self) -> Color { + self.cell.bg + } + + #[inline] + pub fn zerowidth(&self) -> Option<&[char]> { + self.cell.zerowidth() + } + + #[cfg(test)] + pub(crate) fn push_zerowidth(&mut self, character: char) { + self.cell.push_zerowidth(character); + } + + #[inline] + pub fn hyperlink(&self) -> Option { + self.cell.hyperlink().map(terminal_hyperlink_from_alacritty) + } + + #[inline] + pub fn is_inverse(&self) -> bool { + self.cell.flags.contains(Flags::INVERSE) + } + + #[inline] + pub fn is_wide_char_spacer(&self) -> bool { + self.cell.flags.contains(Flags::WIDE_CHAR_SPACER) + } + + #[inline] + pub fn is_dim(&self) -> bool { + self.cell.flags.intersects(Flags::DIM) + } + + #[inline] + pub fn has_underline(&self) -> bool { + self.cell.flags.intersects(Flags::ALL_UNDERLINES) + } + + #[inline] + pub fn has_undercurl(&self) -> bool { + self.cell.flags.contains(Flags::UNDERCURL) + } + + #[inline] + pub fn has_strikeout(&self) -> bool { + self.cell.flags.intersects(Flags::STRIKEOUT) + } + + #[inline] + pub fn is_bold(&self) -> bool { + self.cell.flags.intersects(Flags::BOLD) + } + + #[inline] + pub fn is_italic(&self) -> bool { + self.cell.flags.intersects(Flags::ITALIC) + } + + #[inline] + pub fn has_visible_style_modifier(&self) -> bool { + self.cell + .flags + .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT) + } +} + +impl<'a> RenderableCells<'a> { + pub(super) fn new(cells: GridIterator<'a, AlacCell>) -> Self { + Self { cells } + } +} + +impl Iterator for RenderableCells<'_> { + type Item = IndexedCell; + + fn next(&mut self) -> Option { + self.cells.next().map(|cell| IndexedCell { + point: terminal_point_from_alacritty(cell.point), + cell: terminal_cell_from_alacritty(cell.cell), + }) + } + + fn size_hint(&self) -> (usize, Option) { + self.cells.size_hint() + } +} + +impl Modes { + #[cfg(test)] + fn to_alacritty(self) -> TermMode { + let mut mode = TermMode::empty(); + add_alacritty_mode(&mut mode, self, Self::APP_CURSOR, TermMode::APP_CURSOR); + add_alacritty_mode(&mut mode, self, Self::APP_KEYPAD, TermMode::APP_KEYPAD); + add_alacritty_mode(&mut mode, self, Self::SHOW_CURSOR, TermMode::SHOW_CURSOR); + add_alacritty_mode(&mut mode, self, Self::LINE_WRAP, TermMode::LINE_WRAP); + add_alacritty_mode(&mut mode, self, Self::ORIGIN, TermMode::ORIGIN); + add_alacritty_mode(&mut mode, self, Self::INSERT, TermMode::INSERT); + add_alacritty_mode( + &mut mode, + self, + Self::LINE_FEED_NEW_LINE, + TermMode::LINE_FEED_NEW_LINE, + ); + add_alacritty_mode(&mut mode, self, Self::FOCUS_IN_OUT, TermMode::FOCUS_IN_OUT); + add_alacritty_mode( + &mut mode, + self, + Self::ALTERNATE_SCROLL, + TermMode::ALTERNATE_SCROLL, + ); + add_alacritty_mode( + &mut mode, + self, + Self::BRACKETED_PASTE, + TermMode::BRACKETED_PASTE, + ); + add_alacritty_mode(&mut mode, self, Self::SGR_MOUSE, TermMode::SGR_MOUSE); + add_alacritty_mode(&mut mode, self, Self::UTF8_MOUSE, TermMode::UTF8_MOUSE); + add_alacritty_mode(&mut mode, self, Self::ALT_SCREEN, TermMode::ALT_SCREEN); + add_alacritty_mode( + &mut mode, + self, + Self::MOUSE_REPORT_CLICK, + TermMode::MOUSE_REPORT_CLICK, + ); + add_alacritty_mode(&mut mode, self, Self::MOUSE_DRAG, TermMode::MOUSE_DRAG); + add_alacritty_mode(&mut mode, self, Self::MOUSE_MOTION, TermMode::MOUSE_MOTION); + add_alacritty_mode(&mut mode, self, Self::VI, TermMode::VI); + mode + } +} + +fn terminal_modes_from_alacritty(mode: TermMode) -> Modes { + let mut terminal_modes = Modes::empty(); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::APP_CURSOR, + Modes::APP_CURSOR, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::APP_KEYPAD, + Modes::APP_KEYPAD, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::SHOW_CURSOR, + Modes::SHOW_CURSOR, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::LINE_WRAP, + Modes::LINE_WRAP, + ); + add_terminal_mode(&mut terminal_modes, mode, TermMode::ORIGIN, Modes::ORIGIN); + add_terminal_mode(&mut terminal_modes, mode, TermMode::INSERT, Modes::INSERT); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::LINE_FEED_NEW_LINE, + Modes::LINE_FEED_NEW_LINE, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::FOCUS_IN_OUT, + Modes::FOCUS_IN_OUT, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::ALTERNATE_SCROLL, + Modes::ALTERNATE_SCROLL, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::BRACKETED_PASTE, + Modes::BRACKETED_PASTE, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::SGR_MOUSE, + Modes::SGR_MOUSE, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::UTF8_MOUSE, + Modes::UTF8_MOUSE, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::ALT_SCREEN, + Modes::ALT_SCREEN, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::MOUSE_REPORT_CLICK, + Modes::MOUSE_REPORT_CLICK, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::MOUSE_DRAG, + Modes::MOUSE_DRAG, + ); + add_terminal_mode( + &mut terminal_modes, + mode, + TermMode::MOUSE_MOTION, + Modes::MOUSE_MOTION, + ); + add_terminal_mode(&mut terminal_modes, mode, TermMode::VI, Modes::VI); + terminal_modes +} + +fn add_terminal_mode( + terminal_modes: &mut Modes, + alacritty_modes: TermMode, + alacritty_mode: TermMode, + terminal_mode: Modes, +) { + if alacritty_modes.contains(alacritty_mode) { + terminal_modes.insert(terminal_mode); + } +} + +#[cfg(test)] +fn add_alacritty_mode( + alacritty_modes: &mut TermMode, + terminal_modes: Modes, + terminal_mode: Modes, + alacritty_mode: TermMode, +) { + if terminal_modes.contains(terminal_mode) { + alacritty_modes.insert(alacritty_mode); + } +} + +impl Cursor { + fn from_alacritty(cursor: RenderableCursor) -> Self { + Self { + shape: terminal_cursor_shape_from_alacritty(cursor.shape), + point: terminal_point_from_alacritty(cursor.point), + } + } +} + +fn terminal_cursor_shape_from_alacritty(shape: AlacCursorShape) -> CursorShape { + match shape { + AlacCursorShape::Block => CursorShape::Block, + AlacCursorShape::Underline => CursorShape::Underline, + AlacCursorShape::Beam => CursorShape::Bar, + AlacCursorShape::HollowBlock => CursorShape::HollowBlock, + AlacCursorShape::Hidden => CursorShape::Hidden, + } +} + +impl Point { + fn to_alacritty(self) -> AlacPoint { + AlacPoint::new(Line(self.line), Column(self.column)) + } +} + +fn terminal_point_from_alacritty(point: AlacPoint) -> Point { + Point { + line: point.line.0, + column: point.column.0, + } +} + +impl Range { + #[cfg(test)] + fn to_alacritty(self) -> RangeInclusive { + self.start.to_alacritty()..=self.end.to_alacritty() + } + + fn from_alacritty(range: RangeInclusive) -> Self { + Self { + start: terminal_point_from_alacritty(*range.start()), + end: terminal_point_from_alacritty(*range.end()), + } + } +} + +fn terminal_selection_range_from_alacritty(range: AlacSelectionRange) -> SelectionRange { + SelectionRange { + start: terminal_point_from_alacritty(range.start), + end: terminal_point_from_alacritty(range.end), + is_block: range.is_block, + } +} + +pub(super) fn clear_saved_screen(term: &mut Term) { + term.clear_screen(ClearMode::Saved); + + let cursor = term.grid().cursor.point; + + term.grid_mut().reset_region(..cursor.line); + + let line = term.grid()[cursor.line][..Column(term.grid().columns())] + .iter() + .cloned() + .enumerate() + .collect::>(); + + for (index, cell) in line { + term.grid_mut()[Line(0)][Column(index)] = cell; + } + + term.grid_mut().cursor.point = AlacPoint::new(Line(0), term.grid_mut().cursor.point.column); + let new_cursor = term.grid().cursor.point; + + if (new_cursor.line.0 as usize) < term.screen_lines() - 1 { + term.grid_mut().reset_region((new_cursor.line + 1)..); + } +} + +pub(super) fn make_content(term: &Term, last_content: &Content) -> Content { + let content = term.renderable_content(); + + let estimated_size = content.display_iter.size_hint().0; + let mut cells = Vec::with_capacity(estimated_size); + + cells.extend(content.display_iter.map(|ic| IndexedCell { + point: terminal_point_from_alacritty(ic.point), + cell: terminal_cell_from_alacritty(ic.cell), + })); + + let selection_text = if content.selection.is_some() { + term.selection_to_string() + } else { + None + }; + + Content { + cells, + mode: terminal_modes_from_alacritty(content.mode), + display_offset: content.display_offset, + selection_text, + selection: content + .selection + .map(terminal_selection_range_from_alacritty), + cursor: Cursor::from_alacritty(content.cursor), + cursor_char: term.grid()[content.cursor.point].c, + terminal_bounds: last_content.terminal_bounds, + last_hovered_word: last_content.last_hovered_word.clone(), + scrolled_to_top: content.display_offset == term.history_size(), + scrolled_to_bottom: content.display_offset == 0, + } +} + +pub(super) fn content_text(term: &Term) -> String { + let start = AlacPoint::new(term.topmost_line(), Column(0)); + let end = AlacPoint::new(term.bottommost_line(), term.last_column()); + term.bounds_to_string(start, end) +} + +pub(super) fn total_lines(term: &Term) -> usize { + term.total_lines() +} + +pub(super) fn screen_lines(term: &Term) -> usize { + term.screen_lines() +} + +pub(super) fn full_content_range(term: &Term) -> Range { + let start = AlacPoint::new(term.topmost_line(), Column(0)); + let end = AlacPoint::new(term.bottommost_line(), term.last_column()); + Range::from_alacritty(start..=end) +} + +pub(super) fn last_non_empty_lines(term: &Term, line_count: usize) -> Vec { + let grid = term.grid(); + let mut lines = Vec::new(); + + let mut current_line = grid.bottommost_line().0; + let topmost_line = grid.topmost_line().0; + + while current_line >= topmost_line && lines.len() < line_count { + let (logical_line_start, logical_line) = + logical_line_for_row(grid, current_line, topmost_line); + + if let Some(line) = process_line(logical_line) { + lines.push(line); + } + + current_line = logical_line_start - 1; + } + + lines.reverse(); + lines +} + +pub(super) fn update_vi_cursor_for_scroll(term: &mut Term, scroll: Scroll) { + match scroll { + Scroll::Delta(delta) => { + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, delta); + } + Scroll::PageUp => { + let lines = term.screen_lines() as i32; + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); + } + Scroll::PageDown => { + let lines = -(term.screen_lines() as i32); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); + } + Scroll::Top => { + let point = AlacPoint::new(term.topmost_line(), Column(0)); + term.vi_mode_cursor = ViModeCursor::new(point); + } + Scroll::Bottom => { + let point = AlacPoint::new(term.bottommost_line(), Column(0)); + term.vi_mode_cursor = ViModeCursor::new(point); + } + } +} + +pub(super) fn update_selection_to_vi_cursor(term: &mut Term) -> Option { + let mut selection = term.selection.take()?; + let point = term.vi_mode_cursor.point; + selection.update(point, AlacDirection::Right); + term.selection = Some(selection); + Some(terminal_point_from_alacritty(point)) +} + +pub(super) fn find_from_terminal_point( + term: &AlacrittyTerm, + point: Point, + regex_searches: &mut RegexSearches, + path_style: PathStyle, +) -> Option { + let point = point.to_alacritty().grid_clamp(term, Boundary::Grid); + hyperlinks::find_from_grid_point(term, point, regex_searches, path_style) +} + +fn logical_line_for_row(grid: &Grid, current: i32, topmost: i32) -> (i32, String) { + let start = find_logical_line_start(grid, current, topmost); + let mut line = String::new(); + for row in start..=current { + line.push_str(&row_to_string(&grid[Line(row)])); + } + (start, line) +} + +fn find_logical_line_start(grid: &Grid, current: i32, topmost: i32) -> i32 { + let mut line_start = current; + while line_start > topmost { + let previous_line = Line(line_start - 1); + let last_cell = &grid[previous_line][Column(grid.columns() - 1)]; + if !last_cell.flags.contains(Flags::WRAPLINE) { + break; + } + line_start -= 1; + } + line_start +} + +fn row_to_string(row: &Row) -> String { + row[..Column(row.len())] + .iter() + .map(|cell| cell.c) + .collect::() +} + +fn process_line(line: String) -> Option { + let trimmed = line.trim_end().to_string(); + if !trimmed.is_empty() { + Some(trimmed) + } else { + None + } +} + +/// Appends a stringified task summary to the terminal, after its output. +/// +/// SAFETY: This function should only be called after terminal's PTY is no longer alive. +/// New text being added to the terminal here, uses "less public" APIs, +/// which are not maintaining the entire terminal state intact. +/// +/// +/// The library +/// +/// * does not increment inner grid cursor's _lines_ on `input` calls +/// (but displaying the lines correctly and incrementing cursor's columns) +/// +/// * ignores `\n` and \r` character input, requiring the `newline` call instead +/// +/// * does not alter grid state after `newline` call +/// so its `bottommost_line` is always the same additions, and +/// the cursor's `point` is not updated to the new line and column values +/// +/// * ??? there could be more consequences, and any further "proper" streaming from the PTY might bug and/or panic. +/// Still, subsequent `append_text_to_term` invocations are possible and display the contents correctly. +/// +/// Despite the quirks, this is the simplest approach to appending text to the terminal: its alternative, `grid_mut` manipulations, +/// do not properly set the scrolling state and display odd text after appending; also those manipulations are more tedious and error-prone. +/// The function achieves proper display and scrolling capabilities, at a cost of grid state not properly synchronized. +/// This is enough for printing moderately-sized texts like task summaries, but might break or perform poorly for larger texts. +pub(super) unsafe fn append_text_to_term(term: &mut Term, text_lines: &[&str]) { + term.newline(); + term.grid_mut().cursor.point.column = Column(0); + for line in text_lines { + for character in line.chars() { + term.input(character); + } + term.newline(); + term.grid_mut().cursor.point.column = Column(0); + } +} + +pub(super) fn search_matches(term: &AlacrittyTerm, searcher: Search) -> Vec { + let mut searcher = searcher.into_alacritty(); + all_search_matches(term, &mut searcher) + .map(Range::from_alacritty) + .collect() +} + +fn all_search_matches<'a, T>( + term: &'a Term, + regex: &'a mut RegexSearch, +) -> impl Iterator + 'a { + let start = AlacPoint::new(term.grid().topmost_line(), Column(0)); + let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column()); + RegexIter::new(start, end, AlacDirection::Right, term, regex) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + + #[test] + fn terminal_hyperlink_from_alacritty_keeps_alacritty_storage() { + let hyperlink = AlacHyperlink::new(Some("id"), "https://example.com".to_string()); + let hyperlink = terminal_hyperlink_from_alacritty(hyperlink); + + assert!(matches!(&hyperlink.data, HyperlinkData::Alacritty(_))); + assert_eq!(hyperlink.id(), Some("id")); + assert_eq!(hyperlink.uri(), "https://example.com"); + } + + #[test] + fn terminal_cell_from_alacritty_shares_extra_storage() { + let mut cell = AlacCell::default(); + cell.push_zerowidth('a'); + + let converted = terminal_cell_from_alacritty(&cell); + + match (&cell.extra, &converted.cell.extra) { + (Some(extra), Some(converted_extra)) => assert!(Arc::ptr_eq(extra, converted_extra)), + _ => panic!("expected extra storage on both cells"), + } + } + + #[test] + fn terminal_modes_round_trip_alacritty_flags() { + let alacritty_modes = TermMode::APP_CURSOR + | TermMode::BRACKETED_PASTE + | TermMode::ALT_SCREEN + | TermMode::MOUSE_DRAG + | TermMode::SGR_MOUSE + | TermMode::VI; + + let terminal_modes = terminal_modes_from_alacritty(alacritty_modes); + assert!(terminal_modes.contains(Modes::APP_CURSOR)); + assert!(terminal_modes.contains(Modes::BRACKETED_PASTE)); + assert!(terminal_modes.contains(Modes::ALT_SCREEN)); + assert!(terminal_modes.contains(Modes::MOUSE_DRAG)); + assert!(terminal_modes.intersects(Modes::MOUSE_MODE)); + assert!(terminal_modes.contains(Modes::SGR_MOUSE)); + assert!(terminal_modes.contains(Modes::VI)); + assert!(!terminal_modes.contains(Modes::MOUSE_REPORT_CLICK)); + + let alacritty_modes = terminal_modes.to_alacritty(); + assert!(alacritty_modes.contains(TermMode::APP_CURSOR)); + assert!(alacritty_modes.contains(TermMode::BRACKETED_PASTE)); + assert!(alacritty_modes.contains(TermMode::ALT_SCREEN)); + assert!(alacritty_modes.contains(TermMode::MOUSE_DRAG)); + assert!(alacritty_modes.contains(TermMode::SGR_MOUSE)); + assert!(alacritty_modes.contains(TermMode::VI)); + assert!(!alacritty_modes.contains(TermMode::MOUSE_REPORT_CLICK)); + } + + #[test] + fn terminal_selection_range_round_trip_alacritty_range() { + let alacritty_range = AlacSelectionRange { + start: AlacPoint::new(Line(-2), Column(3)), + end: AlacPoint::new(Line(4), Column(8)), + is_block: true, + }; + + let terminal_range = terminal_selection_range_from_alacritty(alacritty_range); + assert_eq!( + terminal_range, + SelectionRange { + start: Point { + line: -2, + column: 3 + }, + end: Point { line: 4, column: 8 }, + is_block: true, + } + ); + } +} diff --git a/crates/terminal/src/terminal_hyperlinks.rs b/crates/terminal/src/alacritty/hyperlinks.rs similarity index 95% rename from crates/terminal/src/terminal_hyperlinks.rs rename to crates/terminal/src/alacritty/hyperlinks.rs index 65b1f5d82f..15acb69d5d 100644 --- a/crates/terminal/src/terminal_hyperlinks.rs +++ b/crates/terminal/src/alacritty/hyperlinks.rs @@ -11,67 +11,88 @@ use alacritty_terminal::{ use log::{info, warn}; use regex::Regex; use std::{ - ops::{Index, Range}, + ops::{Index, Range as StdRange}, time::{Duration, Instant}, }; use url::Url; use util::paths::{PathStyle, UrlExt}; +use crate::Range; + const URL_REGEX: &str = r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`']+"#; const WIDE_CHAR_SPACERS: Flags = Flags::from_bits(Flags::LEADING_WIDE_CHAR_SPACER.bits() | Flags::WIDE_CHAR_SPACER.bits()) .unwrap(); -pub(super) struct RegexSearches { - url_regex: RegexSearch, +pub(crate) struct RegexSearches { + url_regex: Option, path_hyperlink_regexes: Vec, path_hyperlink_timeout: Duration, } -impl Default for RegexSearches { - fn default() -> Self { - Self { - url_regex: RegexSearch::new(URL_REGEX).unwrap(), - path_hyperlink_regexes: Vec::default(), - path_hyperlink_timeout: Duration::default(), - } - } +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct HyperlinkMatch { + pub(crate) text: String, + pub(crate) is_url: bool, + pub(crate) range: Range, } -impl RegexSearches { - pub(super) fn new( - path_hyperlink_regexes: impl IntoIterator>, - path_hyperlink_timeout_ms: u64, - ) -> Self { + +impl From<(String, bool, Match)> for HyperlinkMatch { + fn from((text, is_url, range): (String, bool, Match)) -> Self { Self { - url_regex: RegexSearch::new(URL_REGEX).unwrap(), - path_hyperlink_regexes: path_hyperlink_regexes - .into_iter() - .filter_map(|regex| { - Regex::new(regex.as_ref()) - .inspect_err(|error| { - warn!( - concat!( - "Ignoring path hyperlink regex specified in ", - "`terminal.path_hyperlink_regexes`:\n\n\t{}\n\nError: {}", - ), - regex.as_ref(), - error - ); - }) - .ok() - }) - .collect(), - path_hyperlink_timeout: Duration::from_millis(path_hyperlink_timeout_ms), + text, + is_url, + range: Range::from_alacritty(range), } } } -pub(super) fn find_from_grid_point( +impl Default for RegexSearches { + fn default() -> Self { + Self::new(Vec::::new(), 0) + } +} +impl RegexSearches { + pub(crate) fn new( + path_hyperlink_regexes: impl IntoIterator>, + path_hyperlink_timeout_ms: u64, + ) -> Self { + Self { + url_regex: RegexSearch::new(URL_REGEX).ok(), + path_hyperlink_regexes: Self::path_hyperlink_regexes(path_hyperlink_regexes), + path_hyperlink_timeout: Duration::from_millis(path_hyperlink_timeout_ms), + } + } + + fn path_hyperlink_regexes( + path_hyperlink_regexes: impl IntoIterator>, + ) -> Vec { + path_hyperlink_regexes + .into_iter() + .filter_map(|regex| { + Regex::new(regex.as_ref()) + .inspect_err(|error| { + warn!( + concat!( + "Ignoring path hyperlink regex specified in ", + "`terminal.path_hyperlink_regexes`:\n\n\t{}\n\nError: {}", + ), + regex.as_ref(), + error + ); + }) + .ok() + }) + .collect() + } +} + +pub(crate) fn find_from_grid_point( term: &Term, point: AlacPoint, regex_searches: &mut RegexSearches, path_style: PathStyle, -) -> Option<(String, bool, Match)> { +) -> Option { let grid = term.grid(); let link = grid.index(point).hyperlink(); let found_word = if let Some(ref url) = link { @@ -101,18 +122,16 @@ pub(super) fn find_from_grid_point( Some((url, true, url_match)) } else { let (line_start, line_end) = (term.line_search_left(point), term.line_search_right(point)); - if let Some((url, url_match)) = RegexIter::new( - line_start, - line_end, - AlacDirection::Right, - term, - &mut regex_searches.url_regex, - ) - .find(|rm| rm.contains(&point)) - .map(|url_match| { - let url = term.bounds_to_string(*url_match.start(), *url_match.end()); - sanitize_url_punctuation(url, url_match, term) - }) { + let url_match = regex_searches.url_regex.as_mut().and_then(|url_regex| { + RegexIter::new(line_start, line_end, AlacDirection::Right, term, url_regex) + .find(|rm| rm.contains(&point)) + .map(|url_match| { + let url = term.bounds_to_string(*url_match.start(), *url_match.end()); + sanitize_url_punctuation(url, url_match, term) + }) + }); + + if let Some((url, url_match)) = url_match { Some((url, true, url_match)) } else { path_match( @@ -127,35 +146,75 @@ pub(super) fn find_from_grid_point( } }; - found_word.map(|(maybe_url_or_path, is_url, word_match)| { - if is_url { - // Treat "file://" IRIs like file paths to ensure - // that line numbers at the end of the path are - // handled correctly. - // Use Url::to_file_path() to properly handle Windows drive letters - // (e.g., file:///C:/path -> C:\path) - if maybe_url_or_path.starts_with("file://") { - if let Ok(url) = Url::parse(&maybe_url_or_path) { - if let Ok(path) = url.to_file_path_ext(path_style) { - return (path.to_string_lossy().into_owned(), false, word_match); - } else if let Some(path) = try_osc8_url_to_path(url) - && path_style.is_posix() - { - return (path, false, word_match); - } + found_word.map(|found_word| normalize_found_word(found_word, path_style)) +} + +fn normalize_found_word( + found_word: (String, bool, Match), + path_style: PathStyle, +) -> HyperlinkMatch { + let (maybe_url_or_path, is_url, word_match) = found_word; + normalize_hyperlink_match( + maybe_url_or_path, + is_url, + Range::from_alacritty(word_match), + path_style, + ) +} + +fn normalize_hyperlink_match( + maybe_url_or_path: String, + is_url: bool, + range: Range, + path_style: PathStyle, +) -> HyperlinkMatch { + if is_url { + // Treat "file://" IRIs like file paths to ensure + // that line numbers at the end of the path are + // handled correctly. + // Use Url::to_file_path() to properly handle Windows drive letters + // (e.g., file:///C:/path -> C:\path) + if maybe_url_or_path.starts_with("file://") { + if let Ok(url) = Url::parse(&maybe_url_or_path) { + if let Ok(path) = url.to_file_path_ext(path_style) { + return HyperlinkMatch { + text: path.to_string_lossy().into_owned(), + is_url: false, + range, + }; + } else if let Some(path) = try_osc8_url_to_path(url) + && path_style.is_posix() + { + return HyperlinkMatch { + text: path, + is_url: false, + range, + }; } - // Fallback: strip file:// prefix if URL parsing fails - let path = maybe_url_or_path - .strip_prefix("file://") - .unwrap_or(&maybe_url_or_path); - (path.to_string(), false, word_match) - } else { - (maybe_url_or_path, true, word_match) + } + // Fallback: strip file:// prefix if URL parsing fails + let path = maybe_url_or_path + .strip_prefix("file://") + .unwrap_or(&maybe_url_or_path); + HyperlinkMatch { + text: path.to_string(), + is_url: false, + range, } } else { - (maybe_url_or_path, false, word_match) + HyperlinkMatch { + text: maybe_url_or_path, + is_url: true, + range, + } } - }) + } else { + HyperlinkMatch { + text: maybe_url_or_path, + is_url: false, + range, + } + } } // OSC 8 mandates that file:// URIs must be encoded as file://{host}{path} @@ -312,8 +371,8 @@ fn path_match( if line.len() <= hovered_point_byte_offset { return None; } - let found_from_range = |path_range: Range, - link_range: Range, + let found_from_range = |path_range: StdRange, + link_range: StdRange, position: Option<(u32, Option)>| { let advance_point_by_str = |mut point: AlacPoint, s: &str| { for _ in s.chars() { @@ -572,7 +631,7 @@ mod tests { macro_rules! test_hyperlink { ($($lines:expr),+; $hyperlink_kind:ident) => { { - use crate::terminal_hyperlinks::tests::line_cells_count; + use crate::alacritty::hyperlinks::tests::line_cells_count; use std::cmp; let test_lines = vec![$($lines),+]; @@ -598,7 +657,7 @@ mod tests { } }; ($columns:expr; $total_cells:expr; $lines:expr; $hyperlink_kind:ident) => { { - use crate::terminal_hyperlinks::tests::{ test_hyperlink, HyperlinkKind }; + use crate::alacritty::hyperlinks::tests::{ test_hyperlink, HyperlinkKind }; let source_location = format!("{}:{}", std::file!(), std::line!()); for columns in $columns { @@ -1080,8 +1139,7 @@ mod tests { event::VoidListener, grid::Scroll, index::{Column, Point as AlacPoint}, - term::test::mock_term, - term::{Term, search::Match}, + term::{Term, test::mock_term}, }; use settings::{self, Settings, SettingsContent}; use std::{cell::RefCell, rc::Rc}; @@ -1114,7 +1172,7 @@ mod tests { TEST_TERM_AND_POINT.with(|(term, point)| { assert_eq!( find_from_grid_point_bench(term, *point) - .map(|(path, ..)| path) + .map(|hyperlink| hyperlink.text) .unwrap_or_default(), "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal", "Hyperlink should have been found" @@ -1132,7 +1190,7 @@ mod tests { TEST_TERM_AND_POINT.with(|(term, point)| { assert_eq!( find_from_grid_point_bench(term, *point) - .map(|(path, ..)| path) + .map(|hyperlink| hyperlink.text) .unwrap_or_default(), "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42", "Hyperlink should have been found" @@ -1150,7 +1208,7 @@ mod tests { TEST_TERM_AND_POINT.with(|(term, point)| { assert_eq!( find_from_grid_point_bench(term, *point) - .map(|(path, ..)| path) + .map(|hyperlink| hyperlink.text) .unwrap_or_default(), "rust-toolchain.toml", "Hyperlink should have been found" @@ -1213,7 +1271,7 @@ mod tests { TEST_TERM_AND_POINT.with(|(term, point)| { assert_eq!( find_from_grid_point_bench(term, *point) - .map(|(path, ..)| path) + .map(|hyperlink| hyperlink.text) .unwrap_or_default(), "392", "Hyperlink should have been found" @@ -1247,7 +1305,7 @@ mod tests { TEST_TERM_AND_POINT.with(|(term, point)| { assert_eq!( find_from_grid_point_bench(term, *point) - .map(|(path, ..)| path) + .map(|hyperlink| hyperlink.text) .unwrap_or_default(), LINE.trim_end_matches(['.', '\r', '\n']), "Hyperlink should have been found" @@ -1258,7 +1316,7 @@ mod tests { pub fn find_from_grid_point_bench( term: &Term, point: AlacPoint, - ) -> Option<(String, bool, Match)> { + ) -> Option { const PATH_HYPERLINK_TIMEOUT_MS: u64 = 1000; thread_local! { @@ -1891,14 +1949,16 @@ mod tests { let check_hyperlink_match = CheckHyperlinkMatch::new(&term, &expected_hyperlink, source_location); match hyperlink_found { - Some((hyperlink_word, false, hyperlink_match)) => { + Some(hyperlink) if !hyperlink.is_url => { + let hyperlink_match = hyperlink.range.to_alacritty(); check_hyperlink_match.check_path_with_position_and_match( - PathWithPosition::parse_str(&hyperlink_word), + PathWithPosition::parse_str(&hyperlink.text), &hyperlink_match, ); } - Some((hyperlink_word, true, hyperlink_match)) => { - check_hyperlink_match.check_iri_and_match(hyperlink_word, &hyperlink_match); + Some(hyperlink) => { + let hyperlink_match = hyperlink.range.to_alacritty(); + check_hyperlink_match.check_iri_and_match(hyperlink.text, &hyperlink_match); } None => { if expected_hyperlink.hyperlink_match.start() diff --git a/crates/terminal/src/mappings/colors.rs b/crates/terminal/src/mappings/colors.rs index 876e366606..62f45a83cb 100644 --- a/crates/terminal/src/mappings/colors.rs +++ b/crates/terminal/src/mappings/colors.rs @@ -1,11 +1,10 @@ -use alacritty_terminal::vte::ansi::Rgb as AlacRgb; use gpui::Rgba; +use vte::ansi::Rgb as VteRgb; -//Convenience method to convert from a GPUI color to an alacritty Rgb -pub fn to_alac_rgb(color: impl Into) -> AlacRgb { +pub(crate) fn to_vte_rgb(color: impl Into) -> VteRgb { let color = color.into(); let r = ((color.r * color.a) * 255.) as u8; let g = ((color.g * color.a) * 255.) as u8; let b = ((color.b * color.a) * 255.) as u8; - AlacRgb { r, g, b } + VteRgb { r, g, b } } diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index 5983ce5d63..5e453e9500 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -1,11 +1,12 @@ use std::borrow::Cow; /// The mappings defined in this file where created from reading the alacritty source -use alacritty_terminal::term::TermMode; use gpui::Keystroke; +use crate::Modes; + #[derive(Debug, PartialEq, Eq)] -enum AlacModifiers { +enum TerminalModifiers { None, Alt, Ctrl, @@ -14,7 +15,7 @@ enum AlacModifiers { Other, } -impl AlacModifiers { +impl TerminalModifiers { fn new(ks: &Keystroke) -> Self { match ( ks.modifiers.alt, @@ -22,147 +23,147 @@ impl AlacModifiers { ks.modifiers.shift, ks.modifiers.platform, ) { - (false, false, false, false) => AlacModifiers::None, - (true, false, false, false) => AlacModifiers::Alt, - (false, true, false, false) => AlacModifiers::Ctrl, - (false, false, true, false) => AlacModifiers::Shift, - (false, true, true, false) => AlacModifiers::CtrlShift, - _ => AlacModifiers::Other, + (false, false, false, false) => TerminalModifiers::None, + (true, false, false, false) => TerminalModifiers::Alt, + (false, true, false, false) => TerminalModifiers::Ctrl, + (false, false, true, false) => TerminalModifiers::Shift, + (false, true, true, false) => TerminalModifiers::CtrlShift, + _ => TerminalModifiers::Other, } } fn any(&self) -> bool { match &self { - AlacModifiers::None => false, - AlacModifiers::Alt => true, - AlacModifiers::Ctrl => true, - AlacModifiers::Shift => true, - AlacModifiers::CtrlShift => true, - AlacModifiers::Other => true, + TerminalModifiers::None => false, + TerminalModifiers::Alt => true, + TerminalModifiers::Ctrl => true, + TerminalModifiers::Shift => true, + TerminalModifiers::CtrlShift => true, + TerminalModifiers::Other => true, } } } -pub fn to_esc_str( +pub(crate) fn to_esc_str( keystroke: &Keystroke, - mode: &TermMode, + mode: Modes, option_as_meta: bool, ) -> Option> { - let modifiers = AlacModifiers::new(keystroke); + let modifiers = TerminalModifiers::new(keystroke); // Manual Bindings including modifiers let manual_esc_str: Option<&'static str> = match (keystroke.key.as_ref(), &modifiers) { //Basic special keys - ("tab", AlacModifiers::None) => Some("\x09"), - ("escape", AlacModifiers::None) => Some("\x1b"), - ("enter", AlacModifiers::None) => Some("\x0d"), - ("enter", AlacModifiers::Shift) => Some("\x0a"), - ("enter", AlacModifiers::Alt) => Some("\x1b\x0d"), - ("backspace", AlacModifiers::None) => Some("\x7f"), + ("tab", TerminalModifiers::None) => Some("\x09"), + ("escape", TerminalModifiers::None) => Some("\x1b"), + ("enter", TerminalModifiers::None) => Some("\x0d"), + ("enter", TerminalModifiers::Shift) => Some("\x0a"), + ("enter", TerminalModifiers::Alt) => Some("\x1b\x0d"), + ("backspace", TerminalModifiers::None) => Some("\x7f"), //Interesting escape codes - ("tab", AlacModifiers::Shift) => Some("\x1b[Z"), - ("backspace", AlacModifiers::Ctrl) => Some("\x08"), - ("backspace", AlacModifiers::Alt) => Some("\x1b\x7f"), - ("backspace", AlacModifiers::Shift) => Some("\x7f"), - ("space", AlacModifiers::Ctrl) => Some("\x00"), - ("home", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOH"), - ("home", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[H"), - ("end", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOF"), - ("end", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[F"), - ("up", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOA"), - ("up", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[A"), - ("down", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOB"), - ("down", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[B"), - ("right", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOC"), - ("right", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[C"), - ("left", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOD"), - ("left", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[D"), - ("back", AlacModifiers::None) => Some("\x7f"), - ("insert", AlacModifiers::None) => Some("\x1b[2~"), - ("delete", AlacModifiers::None) => Some("\x1b[3~"), - ("pageup", AlacModifiers::None) => Some("\x1b[5~"), - ("pagedown", AlacModifiers::None) => Some("\x1b[6~"), - ("f1", AlacModifiers::None) => Some("\x1bOP"), - ("f2", AlacModifiers::None) => Some("\x1bOQ"), - ("f3", AlacModifiers::None) => Some("\x1bOR"), - ("f4", AlacModifiers::None) => Some("\x1bOS"), - ("f5", AlacModifiers::None) => Some("\x1b[15~"), - ("f6", AlacModifiers::None) => Some("\x1b[17~"), - ("f7", AlacModifiers::None) => Some("\x1b[18~"), - ("f8", AlacModifiers::None) => Some("\x1b[19~"), - ("f9", AlacModifiers::None) => Some("\x1b[20~"), - ("f10", AlacModifiers::None) => Some("\x1b[21~"), - ("f11", AlacModifiers::None) => Some("\x1b[23~"), - ("f12", AlacModifiers::None) => Some("\x1b[24~"), - ("f13", AlacModifiers::None) => Some("\x1b[25~"), - ("f14", AlacModifiers::None) => Some("\x1b[26~"), - ("f15", AlacModifiers::None) => Some("\x1b[28~"), - ("f16", AlacModifiers::None) => Some("\x1b[29~"), - ("f17", AlacModifiers::None) => Some("\x1b[31~"), - ("f18", AlacModifiers::None) => Some("\x1b[32~"), - ("f19", AlacModifiers::None) => Some("\x1b[33~"), - ("f20", AlacModifiers::None) => Some("\x1b[34~"), + ("tab", TerminalModifiers::Shift) => Some("\x1b[Z"), + ("backspace", TerminalModifiers::Ctrl) => Some("\x08"), + ("backspace", TerminalModifiers::Alt) => Some("\x1b\x7f"), + ("backspace", TerminalModifiers::Shift) => Some("\x7f"), + ("space", TerminalModifiers::Ctrl) => Some("\x00"), + ("home", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOH"), + ("home", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[H"), + ("end", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOF"), + ("end", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[F"), + ("up", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOA"), + ("up", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[A"), + ("down", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOB"), + ("down", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[B"), + ("right", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOC"), + ("right", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[C"), + ("left", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOD"), + ("left", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[D"), + ("back", TerminalModifiers::None) => Some("\x7f"), + ("insert", TerminalModifiers::None) => Some("\x1b[2~"), + ("delete", TerminalModifiers::None) => Some("\x1b[3~"), + ("pageup", TerminalModifiers::None) => Some("\x1b[5~"), + ("pagedown", TerminalModifiers::None) => Some("\x1b[6~"), + ("f1", TerminalModifiers::None) => Some("\x1bOP"), + ("f2", TerminalModifiers::None) => Some("\x1bOQ"), + ("f3", TerminalModifiers::None) => Some("\x1bOR"), + ("f4", TerminalModifiers::None) => Some("\x1bOS"), + ("f5", TerminalModifiers::None) => Some("\x1b[15~"), + ("f6", TerminalModifiers::None) => Some("\x1b[17~"), + ("f7", TerminalModifiers::None) => Some("\x1b[18~"), + ("f8", TerminalModifiers::None) => Some("\x1b[19~"), + ("f9", TerminalModifiers::None) => Some("\x1b[20~"), + ("f10", TerminalModifiers::None) => Some("\x1b[21~"), + ("f11", TerminalModifiers::None) => Some("\x1b[23~"), + ("f12", TerminalModifiers::None) => Some("\x1b[24~"), + ("f13", TerminalModifiers::None) => Some("\x1b[25~"), + ("f14", TerminalModifiers::None) => Some("\x1b[26~"), + ("f15", TerminalModifiers::None) => Some("\x1b[28~"), + ("f16", TerminalModifiers::None) => Some("\x1b[29~"), + ("f17", TerminalModifiers::None) => Some("\x1b[31~"), + ("f18", TerminalModifiers::None) => Some("\x1b[32~"), + ("f19", TerminalModifiers::None) => Some("\x1b[33~"), + ("f20", TerminalModifiers::None) => Some("\x1b[34~"), // NumpadEnter, Action::Esc("\n".into()); //Mappings for caret notation keys - ("a", AlacModifiers::Ctrl) => Some("\x01"), //1 - ("A", AlacModifiers::CtrlShift) => Some("\x01"), //1 - ("b", AlacModifiers::Ctrl) => Some("\x02"), //2 - ("B", AlacModifiers::CtrlShift) => Some("\x02"), //2 - ("c", AlacModifiers::Ctrl) => Some("\x03"), //3 - ("C", AlacModifiers::CtrlShift) => Some("\x03"), //3 - ("d", AlacModifiers::Ctrl) => Some("\x04"), //4 - ("D", AlacModifiers::CtrlShift) => Some("\x04"), //4 - ("e", AlacModifiers::Ctrl) => Some("\x05"), //5 - ("E", AlacModifiers::CtrlShift) => Some("\x05"), //5 - ("f", AlacModifiers::Ctrl) => Some("\x06"), //6 - ("F", AlacModifiers::CtrlShift) => Some("\x06"), //6 - ("g", AlacModifiers::Ctrl) => Some("\x07"), //7 - ("G", AlacModifiers::CtrlShift) => Some("\x07"), //7 - ("h", AlacModifiers::Ctrl) => Some("\x08"), //8 - ("H", AlacModifiers::CtrlShift) => Some("\x08"), //8 - ("i", AlacModifiers::Ctrl) => Some("\x09"), //9 - ("I", AlacModifiers::CtrlShift) => Some("\x09"), //9 - ("j", AlacModifiers::Ctrl) => Some("\x0a"), //10 - ("J", AlacModifiers::CtrlShift) => Some("\x0a"), //10 - ("k", AlacModifiers::Ctrl) => Some("\x0b"), //11 - ("K", AlacModifiers::CtrlShift) => Some("\x0b"), //11 - ("l", AlacModifiers::Ctrl) => Some("\x0c"), //12 - ("L", AlacModifiers::CtrlShift) => Some("\x0c"), //12 - ("m", AlacModifiers::Ctrl) => Some("\x0d"), //13 - ("M", AlacModifiers::CtrlShift) => Some("\x0d"), //13 - ("n", AlacModifiers::Ctrl) => Some("\x0e"), //14 - ("N", AlacModifiers::CtrlShift) => Some("\x0e"), //14 - ("o", AlacModifiers::Ctrl) => Some("\x0f"), //15 - ("O", AlacModifiers::CtrlShift) => Some("\x0f"), //15 - ("p", AlacModifiers::Ctrl) => Some("\x10"), //16 - ("P", AlacModifiers::CtrlShift) => Some("\x10"), //16 - ("q", AlacModifiers::Ctrl) => Some("\x11"), //17 - ("Q", AlacModifiers::CtrlShift) => Some("\x11"), //17 - ("r", AlacModifiers::Ctrl) => Some("\x12"), //18 - ("R", AlacModifiers::CtrlShift) => Some("\x12"), //18 - ("s", AlacModifiers::Ctrl) => Some("\x13"), //19 - ("S", AlacModifiers::CtrlShift) => Some("\x13"), //19 - ("t", AlacModifiers::Ctrl) => Some("\x14"), //20 - ("T", AlacModifiers::CtrlShift) => Some("\x14"), //20 - ("u", AlacModifiers::Ctrl) => Some("\x15"), //21 - ("U", AlacModifiers::CtrlShift) => Some("\x15"), //21 - ("v", AlacModifiers::Ctrl) => Some("\x16"), //22 - ("V", AlacModifiers::CtrlShift) => Some("\x16"), //22 - ("w", AlacModifiers::Ctrl) => Some("\x17"), //23 - ("W", AlacModifiers::CtrlShift) => Some("\x17"), //23 - ("x", AlacModifiers::Ctrl) => Some("\x18"), //24 - ("X", AlacModifiers::CtrlShift) => Some("\x18"), //24 - ("y", AlacModifiers::Ctrl) => Some("\x19"), //25 - ("Y", AlacModifiers::CtrlShift) => Some("\x19"), //25 - ("z", AlacModifiers::Ctrl) => Some("\x1a"), //26 - ("Z", AlacModifiers::CtrlShift) => Some("\x1a"), //26 - ("@", AlacModifiers::Ctrl) => Some("\x00"), //0 - ("[", AlacModifiers::Ctrl) => Some("\x1b"), //27 - ("\\", AlacModifiers::Ctrl) => Some("\x1c"), //28 - ("]", AlacModifiers::Ctrl) => Some("\x1d"), //29 - ("^", AlacModifiers::Ctrl) => Some("\x1e"), //30 - ("_", AlacModifiers::Ctrl) => Some("\x1f"), //31 - ("?", AlacModifiers::Ctrl) => Some("\x7f"), //127 + ("a", TerminalModifiers::Ctrl) => Some("\x01"), //1 + ("A", TerminalModifiers::CtrlShift) => Some("\x01"), //1 + ("b", TerminalModifiers::Ctrl) => Some("\x02"), //2 + ("B", TerminalModifiers::CtrlShift) => Some("\x02"), //2 + ("c", TerminalModifiers::Ctrl) => Some("\x03"), //3 + ("C", TerminalModifiers::CtrlShift) => Some("\x03"), //3 + ("d", TerminalModifiers::Ctrl) => Some("\x04"), //4 + ("D", TerminalModifiers::CtrlShift) => Some("\x04"), //4 + ("e", TerminalModifiers::Ctrl) => Some("\x05"), //5 + ("E", TerminalModifiers::CtrlShift) => Some("\x05"), //5 + ("f", TerminalModifiers::Ctrl) => Some("\x06"), //6 + ("F", TerminalModifiers::CtrlShift) => Some("\x06"), //6 + ("g", TerminalModifiers::Ctrl) => Some("\x07"), //7 + ("G", TerminalModifiers::CtrlShift) => Some("\x07"), //7 + ("h", TerminalModifiers::Ctrl) => Some("\x08"), //8 + ("H", TerminalModifiers::CtrlShift) => Some("\x08"), //8 + ("i", TerminalModifiers::Ctrl) => Some("\x09"), //9 + ("I", TerminalModifiers::CtrlShift) => Some("\x09"), //9 + ("j", TerminalModifiers::Ctrl) => Some("\x0a"), //10 + ("J", TerminalModifiers::CtrlShift) => Some("\x0a"), //10 + ("k", TerminalModifiers::Ctrl) => Some("\x0b"), //11 + ("K", TerminalModifiers::CtrlShift) => Some("\x0b"), //11 + ("l", TerminalModifiers::Ctrl) => Some("\x0c"), //12 + ("L", TerminalModifiers::CtrlShift) => Some("\x0c"), //12 + ("m", TerminalModifiers::Ctrl) => Some("\x0d"), //13 + ("M", TerminalModifiers::CtrlShift) => Some("\x0d"), //13 + ("n", TerminalModifiers::Ctrl) => Some("\x0e"), //14 + ("N", TerminalModifiers::CtrlShift) => Some("\x0e"), //14 + ("o", TerminalModifiers::Ctrl) => Some("\x0f"), //15 + ("O", TerminalModifiers::CtrlShift) => Some("\x0f"), //15 + ("p", TerminalModifiers::Ctrl) => Some("\x10"), //16 + ("P", TerminalModifiers::CtrlShift) => Some("\x10"), //16 + ("q", TerminalModifiers::Ctrl) => Some("\x11"), //17 + ("Q", TerminalModifiers::CtrlShift) => Some("\x11"), //17 + ("r", TerminalModifiers::Ctrl) => Some("\x12"), //18 + ("R", TerminalModifiers::CtrlShift) => Some("\x12"), //18 + ("s", TerminalModifiers::Ctrl) => Some("\x13"), //19 + ("S", TerminalModifiers::CtrlShift) => Some("\x13"), //19 + ("t", TerminalModifiers::Ctrl) => Some("\x14"), //20 + ("T", TerminalModifiers::CtrlShift) => Some("\x14"), //20 + ("u", TerminalModifiers::Ctrl) => Some("\x15"), //21 + ("U", TerminalModifiers::CtrlShift) => Some("\x15"), //21 + ("v", TerminalModifiers::Ctrl) => Some("\x16"), //22 + ("V", TerminalModifiers::CtrlShift) => Some("\x16"), //22 + ("w", TerminalModifiers::Ctrl) => Some("\x17"), //23 + ("W", TerminalModifiers::CtrlShift) => Some("\x17"), //23 + ("x", TerminalModifiers::Ctrl) => Some("\x18"), //24 + ("X", TerminalModifiers::CtrlShift) => Some("\x18"), //24 + ("y", TerminalModifiers::Ctrl) => Some("\x19"), //25 + ("Y", TerminalModifiers::CtrlShift) => Some("\x19"), //25 + ("z", TerminalModifiers::Ctrl) => Some("\x1a"), //26 + ("Z", TerminalModifiers::CtrlShift) => Some("\x1a"), //26 + ("@", TerminalModifiers::Ctrl) => Some("\x00"), //0 + ("[", TerminalModifiers::Ctrl) => Some("\x1b"), //27 + ("\\", TerminalModifiers::Ctrl) => Some("\x1c"), //28 + ("]", TerminalModifiers::Ctrl) => Some("\x1d"), //29 + ("^", TerminalModifiers::Ctrl) => Some("\x1e"), //30 + ("_", TerminalModifiers::Ctrl) => Some("\x1f"), //31 + ("?", TerminalModifiers::Ctrl) => Some("\x7f"), //127 _ => None, }; if let Some(esc_str) = manual_esc_str { @@ -210,7 +211,8 @@ pub fn to_esc_str( } if !cfg!(target_os = "macos") || option_as_meta { - let is_alt_lowercase_ascii = modifiers == AlacModifiers::Alt && keystroke.key.is_ascii(); + let is_alt_lowercase_ascii = + modifiers == TerminalModifiers::Alt && keystroke.key.is_ascii(); let is_alt_uppercase_ascii = keystroke.modifiers.alt && keystroke.modifiers.shift && keystroke.key.is_ascii(); if is_alt_lowercase_ascii || is_alt_uppercase_ascii { @@ -270,57 +272,51 @@ mod test { key: "🖖🏻".to_string(), //2 char string key_char: None, }; - assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None); + assert_eq!(to_esc_str(&ks, Modes::NONE, false), None); } #[test] fn test_application_mode() { - let app_cursor = TermMode::APP_CURSOR; - let none = TermMode::NONE; + let app_cursor = Modes::APP_CURSOR; + let none = Modes::NONE; let up = Keystroke::parse("up").unwrap(); let down = Keystroke::parse("down").unwrap(); let left = Keystroke::parse("left").unwrap(); let right = Keystroke::parse("right").unwrap(); - assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".into())); - assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".into())); - assert_eq!(to_esc_str(&right, &none, false), Some("\x1b[C".into())); - assert_eq!(to_esc_str(&left, &none, false), Some("\x1b[D".into())); + assert_eq!(to_esc_str(&up, none, false), Some("\x1b[A".into())); + assert_eq!(to_esc_str(&down, none, false), Some("\x1b[B".into())); + assert_eq!(to_esc_str(&right, none, false), Some("\x1b[C".into())); + assert_eq!(to_esc_str(&left, none, false), Some("\x1b[D".into())); - assert_eq!(to_esc_str(&up, &app_cursor, false), Some("\x1bOA".into())); - assert_eq!(to_esc_str(&down, &app_cursor, false), Some("\x1bOB".into())); - assert_eq!( - to_esc_str(&right, &app_cursor, false), - Some("\x1bOC".into()) - ); - assert_eq!(to_esc_str(&left, &app_cursor, false), Some("\x1bOD".into())); + assert_eq!(to_esc_str(&up, app_cursor, false), Some("\x1bOA".into())); + assert_eq!(to_esc_str(&down, app_cursor, false), Some("\x1bOB".into())); + assert_eq!(to_esc_str(&right, app_cursor, false), Some("\x1bOC".into())); + assert_eq!(to_esc_str(&left, app_cursor, false), Some("\x1bOD".into())); let home = Keystroke::parse("home").unwrap(); let end = Keystroke::parse("end").unwrap(); - assert_eq!(to_esc_str(&home, &none, false), Some("\x1b[H".into())); - assert_eq!(to_esc_str(&end, &none, false), Some("\x1b[F".into())); - assert_eq!(to_esc_str(&home, &app_cursor, false), Some("\x1bOH".into())); - assert_eq!(to_esc_str(&end, &app_cursor, false), Some("\x1bOF".into())); + assert_eq!(to_esc_str(&home, none, false), Some("\x1b[H".into())); + assert_eq!(to_esc_str(&end, none, false), Some("\x1b[F".into())); + assert_eq!(to_esc_str(&home, app_cursor, false), Some("\x1bOH".into())); + assert_eq!(to_esc_str(&end, app_cursor, false), Some("\x1bOF".into())); let shift_up = Keystroke::parse("shift-up").unwrap(); let shift_down = Keystroke::parse("shift-down").unwrap(); let shift_home = Keystroke::parse("shift-home").unwrap(); let shift_end = Keystroke::parse("shift-end").unwrap(); + assert_eq!(to_esc_str(&shift_up, none, false), Some("\x1b[1;2A".into())); assert_eq!( - to_esc_str(&shift_up, &none, false), - Some("\x1b[1;2A".into()) - ); - assert_eq!( - to_esc_str(&shift_down, &none, false), + to_esc_str(&shift_down, none, false), Some("\x1b[1;2B".into()) ); assert_eq!( - to_esc_str(&shift_home, &none, false), + to_esc_str(&shift_home, none, false), Some("\x1b[1;2H".into()) ); assert_eq!( - to_esc_str(&shift_end, &none, false), + to_esc_str(&shift_end, none, false), Some("\x1b[1;2F".into()) ); } @@ -329,18 +325,18 @@ mod test { fn test_ctrl_codes() { let letters_lower = 'a'..='z'; let letters_upper = 'A'..='Z'; - let mode = TermMode::ANY; + let mode = Modes::APP_CURSOR; for (lower, upper) in letters_lower.zip(letters_upper) { assert_eq!( to_esc_str( &Keystroke::parse(&format!("ctrl-shift-{}", lower)).unwrap(), - &mode, + mode, false ), to_esc_str( &Keystroke::parse(&format!("ctrl-{}", upper)).unwrap(), - &mode, + mode, false ), "On letter: {}/{}", @@ -357,7 +353,7 @@ mod test { assert_eq!( to_esc_str( &Keystroke::parse(&format!("alt-{}", character)).unwrap(), - &TermMode::NONE, + Modes::NONE, true ) .unwrap(), @@ -375,7 +371,7 @@ mod test { assert_ne!( to_esc_str( &Keystroke::parse(&format!("alt-{}", key)).unwrap(), - &TermMode::NONE, + Modes::NONE, true ) .unwrap(), @@ -388,16 +384,13 @@ mod test { fn test_shift_enter_newline() { let shift_enter = Keystroke::parse("shift-enter").unwrap(); let regular_enter = Keystroke::parse("enter").unwrap(); - let mode = TermMode::NONE; + let mode = Modes::NONE; // Shift-enter should send line feed (newline) - assert_eq!(to_esc_str(&shift_enter, &mode, false), Some("\x0a".into())); + assert_eq!(to_esc_str(&shift_enter, mode, false), Some("\x0a".into())); // Regular enter should still send carriage return - assert_eq!( - to_esc_str(®ular_enter, &mode, false), - Some("\x0d".into()) - ); + assert_eq!(to_esc_str(®ular_enter, mode, false), Some("\x0d".into())); } #[test] diff --git a/crates/terminal/src/mappings/mod.rs b/crates/terminal/src/mappings/mod.rs index d58dd27f96..8926b7e0f2 100644 --- a/crates/terminal/src/mappings/mod.rs +++ b/crates/terminal/src/mappings/mod.rs @@ -1,3 +1,3 @@ -pub mod colors; -pub mod keys; -pub mod mouse; +pub(crate) mod colors; +pub(crate) mod keys; +pub(crate) mod mouse; diff --git a/crates/terminal/src/mappings/mouse.rs b/crates/terminal/src/mappings/mouse.rs index ffd60a83aa..7a88303475 100644 --- a/crates/terminal/src/mappings/mouse.rs +++ b/crates/terminal/src/mappings/mouse.rs @@ -1,14 +1,11 @@ use std::cmp::{self, min}; use std::iter::repeat; -use alacritty_terminal::grid::Dimensions; /// Most of the code, and specifically the constants, in this are copied from Alacritty, /// with modifications for our circumstances -use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point as AlacPoint, Side}; -use alacritty_terminal::term::TermMode; -use gpui::{Modifiers, MouseButton, Pixels, Point, ScrollWheelEvent, px}; +use gpui::{Modifiers, MouseButton, Pixels, Point as GpuiPoint, ScrollWheelEvent, px}; -use crate::TerminalBounds; +use crate::{Modes, Point, SelectionSide, TerminalBounds}; enum MouseFormat { Sgr, @@ -16,10 +13,10 @@ enum MouseFormat { } impl MouseFormat { - fn from_mode(mode: TermMode) -> Self { - if mode.contains(TermMode::SGR_MOUSE) { + fn from_mode(mode: Modes) -> Self { + if mode.contains(Modes::SGR_MOUSE) { MouseFormat::Sgr - } else if mode.contains(TermMode::UTF8_MOUSE) { + } else if mode.contains(Modes::UTF8_MOUSE) { MouseFormat::Normal(true) } else { MouseFormat::Normal(false) @@ -28,7 +25,7 @@ impl MouseFormat { } #[derive(Debug)] -enum AlacMouseButton { +enum MouseButtonCode { LeftButton = 0, MiddleButton = 1, RightButton = 2, @@ -41,23 +38,23 @@ enum AlacMouseButton { Other = 99, } -impl AlacMouseButton { +impl MouseButtonCode { fn from_move_button(e: Option) -> Self { match e { - Some(gpui::MouseButton::Left) => AlacMouseButton::LeftMove, - Some(gpui::MouseButton::Middle) => AlacMouseButton::MiddleMove, - Some(gpui::MouseButton::Right) => AlacMouseButton::RightMove, - Some(gpui::MouseButton::Navigate(_)) => AlacMouseButton::Other, - None => AlacMouseButton::NoneMove, + Some(gpui::MouseButton::Left) => MouseButtonCode::LeftMove, + Some(gpui::MouseButton::Middle) => MouseButtonCode::MiddleMove, + Some(gpui::MouseButton::Right) => MouseButtonCode::RightMove, + Some(gpui::MouseButton::Navigate(_)) => MouseButtonCode::Other, + None => MouseButtonCode::NoneMove, } } fn from_button(e: MouseButton) -> Self { match e { - gpui::MouseButton::Left => AlacMouseButton::LeftButton, - gpui::MouseButton::Right => AlacMouseButton::MiddleButton, - gpui::MouseButton::Middle => AlacMouseButton::RightButton, - gpui::MouseButton::Navigate(_) => AlacMouseButton::Other, + gpui::MouseButton::Left => MouseButtonCode::LeftButton, + gpui::MouseButton::Right => MouseButtonCode::MiddleButton, + gpui::MouseButton::Middle => MouseButtonCode::RightButton, + gpui::MouseButton::Navigate(_) => MouseButtonCode::Other, } } @@ -68,27 +65,27 @@ impl AlacMouseButton { }; if is_positive { - AlacMouseButton::ScrollUp + MouseButtonCode::ScrollUp } else { - AlacMouseButton::ScrollDown + MouseButtonCode::ScrollDown } } fn is_other(&self) -> bool { - matches!(self, AlacMouseButton::Other) + matches!(self, MouseButtonCode::Other) } } -pub fn scroll_report( - point: AlacPoint, +pub(crate) fn scroll_report( + point: Point, scroll_lines: i32, e: &ScrollWheelEvent, - mode: TermMode, + mode: Modes, ) -> Option>> { - if mode.intersects(TermMode::MOUSE_MODE) { + if mode.intersects(Modes::MOUSE_MODE) { mouse_report( point, - AlacMouseButton::from_scroll(e), + MouseButtonCode::from_scroll(e), true, e.modifiers, MouseFormat::from_mode(mode), @@ -106,7 +103,7 @@ mod tests { #[test] fn scroll_report_repeats_for_negative_scroll_lines() { - let grid_point = AlacPoint::new(GridLine(0), GridCol(0)); + let grid_point = Point::new(0, 0); let scroll_event = ScrollWheelEvent { delta: ScrollDelta::Lines(point(0., -1.)), @@ -114,7 +111,7 @@ mod tests { ..Default::default() }; - let mode = TermMode::MOUSE_MODE; + let mode = Modes::MOUSE_MODE; let reports: Vec> = scroll_report(grid_point, -3, &scroll_event, mode) .expect("mouse mode should produce a scroll report") .collect(); @@ -124,7 +121,7 @@ mod tests { #[test] fn scroll_report_repeats_for_positive_scroll_lines() { - let grid_point = AlacPoint::new(GridLine(0), GridCol(0)); + let grid_point = Point::new(0, 0); let scroll_event = ScrollWheelEvent { delta: ScrollDelta::Lines(point(0., 1.)), @@ -132,7 +129,7 @@ mod tests { ..Default::default() }; - let mode = TermMode::MOUSE_MODE; + let mode = Modes::MOUSE_MODE; let reports: Vec> = scroll_report(grid_point, 3, &scroll_event, mode) .expect("mouse mode should produce a scroll report") .collect(); @@ -141,7 +138,7 @@ mod tests { } } -pub fn alt_scroll(scroll_lines: i32) -> Vec { +pub(crate) fn alt_scroll(scroll_lines: i32) -> Vec { let cmd = if scroll_lines > 0 { b'A' } else { b'B' }; let mut content = Vec::with_capacity(scroll_lines.unsigned_abs() as usize * 3); @@ -153,15 +150,15 @@ pub fn alt_scroll(scroll_lines: i32) -> Vec { content } -pub fn mouse_button_report( - point: AlacPoint, +pub(crate) fn mouse_button_report( + point: Point, button: gpui::MouseButton, modifiers: Modifiers, pressed: bool, - mode: TermMode, + mode: Modes, ) -> Option> { - let button = AlacMouseButton::from_button(button); - if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) { + let button = MouseButtonCode::from_button(button); + if !button.is_other() && mode.intersects(Modes::MOUSE_MODE) { mouse_report( point, button, @@ -174,17 +171,17 @@ pub fn mouse_button_report( } } -pub fn mouse_moved_report( - point: AlacPoint, +pub(crate) fn mouse_moved_report( + point: Point, button: Option, modifiers: Modifiers, - mode: TermMode, + mode: Modes, ) -> Option> { - let button = AlacMouseButton::from_move_button(button); + let button = MouseButtonCode::from_move_button(button); - if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) { + if !button.is_other() && mode.intersects(Modes::MOUSE_MOTION | Modes::MOUSE_DRAG) { //Only drags are reported in drag mode, so block NoneMove. - if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, AlacMouseButton::NoneMove) { + if mode.contains(Modes::MOUSE_DRAG) && matches!(button, MouseButtonCode::NoneMove) { None } else { mouse_report(point, button, true, modifiers, MouseFormat::from_mode(mode)) @@ -194,51 +191,54 @@ pub fn mouse_moved_report( } } -pub fn grid_point( - pos: Point, +pub(crate) fn grid_point( + pos: GpuiPoint, cur_size: TerminalBounds, display_offset: usize, -) -> AlacPoint { +) -> Point { grid_point_and_side(pos, cur_size, display_offset).0 } -pub fn grid_point_and_side( - pos: Point, +pub(crate) fn grid_point_and_side( + pos: GpuiPoint, cur_size: TerminalBounds, display_offset: usize, -) -> (AlacPoint, Side) { - let mut col = GridCol((pos.x / cur_size.cell_width) as usize); +) -> (Point, SelectionSide) { + let mut column = (pos.x / cur_size.cell_width) as usize; let cell_x = cmp::max(px(0.), pos.x) % cur_size.cell_width; let half_cell_width = cur_size.cell_width / 2.0; let mut side = if cell_x > half_cell_width { - Side::Right + SelectionSide::Right } else { - Side::Left + SelectionSide::Left }; - if col > cur_size.last_column() { - col = cur_size.last_column(); - side = Side::Right; + let last_column = cur_size.num_columns().saturating_sub(1); + if column > last_column { + column = last_column; + side = SelectionSide::Right; } - let col = min(col, cur_size.last_column()); + let column = min(column, last_column); let mut line = (pos.y / cur_size.line_height) as i32; - if line > cur_size.bottommost_line() { - line = cur_size.bottommost_line().0; - side = Side::Right; + let bottommost_line = i32::try_from(cur_size.num_lines().saturating_sub(1)).unwrap_or(i32::MAX); + if line > bottommost_line { + line = bottommost_line; + side = SelectionSide::Right; } else if line < 0 { - side = Side::Left; + side = SelectionSide::Left; } + let display_offset = i32::try_from(display_offset).unwrap_or(i32::MAX); ( - AlacPoint::new(GridLine(line - display_offset as i32), col), + Point::new(line.saturating_sub(display_offset), column), side, ) } ///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode fn mouse_report( - point: AlacPoint, - button: AlacMouseButton, + point: Point, + button: MouseButtonCode, pressed: bool, modifiers: Modifiers, format: MouseFormat, @@ -272,11 +272,10 @@ fn mouse_report( } } -fn normal_mouse_report(point: AlacPoint, button: u8, utf8: bool) -> Option> { - let AlacPoint { line, column } = point; +fn normal_mouse_report(point: Point, button: u8, utf8: bool) -> Option> { let max_point = if utf8 { 2015 } else { 223 }; - if line >= max_point || column >= max_point { + if point.line >= max_point || point.column >= max_point as usize { return None; } @@ -289,22 +288,22 @@ fn normal_mouse_report(point: AlacPoint, button: u8, utf8: bool) -> Option= 95 { - msg.append(&mut mouse_pos_encode(column.0)); + if utf8 && point.column >= 95 { + msg.append(&mut mouse_pos_encode(point.column)); } else { - msg.push(32 + 1 + column.0 as u8); + msg.push(32 + 1 + point.column as u8); } - if utf8 && line >= 95 { - msg.append(&mut mouse_pos_encode(line.0 as usize)); + if utf8 && point.line >= 95 { + msg.append(&mut mouse_pos_encode(point.line as usize)); } else { - msg.push(32 + 1 + line.0 as u8); + msg.push(32 + 1 + point.line as u8); } Some(msg) } -fn sgr_mouse_report(point: AlacPoint, button: u8, pressed: bool) -> String { +fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String { let c = if pressed { 'M' } else { 'm' }; let msg = format!( diff --git a/crates/terminal/src/pty_info.rs b/crates/terminal/src/pty_info.rs index 560a24ca54..576a833f01 100644 --- a/crates/terminal/src/pty_info.rs +++ b/crates/terminal/src/pty_info.rs @@ -1,10 +1,5 @@ -use alacritty_terminal::tty::Pty; use gpui::{Context, Task}; use parking_lot::{MappedRwLockReadGuard, Mutex, RwLock, RwLockReadGuard}; -#[cfg(target_os = "windows")] -use std::num::NonZeroU32; -#[cfg(unix)] -use std::os::fd::AsRawFd; use std::{path::PathBuf, sync::Arc}; #[cfg(target_os = "windows")] @@ -21,6 +16,13 @@ pub struct ProcessIdGetter { } impl ProcessIdGetter { + pub(crate) fn new(handle: i32, fallback_pid: u32) -> ProcessIdGetter { + ProcessIdGetter { + handle, + fallback_pid, + } + } + pub fn fallback_pid(&self) -> Pid { Pid::from_u32(self.fallback_pid) } @@ -28,13 +30,6 @@ impl ProcessIdGetter { #[cfg(unix)] impl ProcessIdGetter { - fn new(pty: &Pty) -> ProcessIdGetter { - ProcessIdGetter { - handle: pty.file().as_raw_fd(), - fallback_pid: pty.child().id(), - } - } - fn pid(&self) -> Option { // Negative pid means error. // Zero pid means no foreground process group is set on the PTY yet. @@ -54,19 +49,6 @@ impl ProcessIdGetter { #[cfg(windows)] impl ProcessIdGetter { - fn new(pty: &Pty) -> ProcessIdGetter { - let child = pty.child_watcher(); - let handle = child.raw_handle(); - let fallback_pid = child.pid().unwrap_or_else(|| unsafe { - NonZeroU32::new_unchecked(GetProcessId(HANDLE(handle as _))) - }); - - ProcessIdGetter { - handle: handle as i32, - fallback_pid: u32::from(fallback_pid), - } - } - fn pid(&self) -> Option { let pid = unsafe { GetProcessId(HANDLE(self.handle as _)) }; // the GetProcessId may fail and returns zero, which will lead to a stack overflow issue @@ -84,23 +66,23 @@ impl ProcessIdGetter { } #[derive(Clone, Debug)] -pub struct ProcessInfo { - pub name: String, - pub cwd: PathBuf, - pub argv: Vec, +pub(crate) struct ProcessInfo { + pub(crate) name: String, + pub(crate) cwd: PathBuf, + pub(crate) argv: Vec, } /// Fetches Zed-relevant Pseudo-Terminal (PTY) process information -pub struct PtyProcessInfo { +pub(crate) struct PtyProcessInfo { system: RwLock, refresh_kind: ProcessRefreshKind, pid_getter: ProcessIdGetter, - pub current: RwLock>, + pub(crate) current: RwLock>, task: Mutex>>, } impl PtyProcessInfo { - pub fn new(pty: &Pty) -> PtyProcessInfo { + pub(crate) fn new(pid_getter: ProcessIdGetter) -> PtyProcessInfo { let process_refresh_kind = ProcessRefreshKind::nothing() .with_cmd(UpdateKind::Always) .with_cwd(UpdateKind::Always) @@ -111,13 +93,13 @@ impl PtyProcessInfo { PtyProcessInfo { system: RwLock::new(system), refresh_kind: process_refresh_kind, - pid_getter: ProcessIdGetter::new(pty), + pid_getter, current: RwLock::new(None), task: Mutex::new(None), } } - pub fn pid_getter(&self) -> &ProcessIdGetter { + pub(crate) fn pid_getter(&self) -> &ProcessIdGetter { &self.pid_getter } @@ -191,7 +173,7 @@ impl PtyProcessInfo { } /// Updates the cached process info, emitting a [`Event::TitleChanged`] event if the Zed-relevant info has changed - pub fn emit_title_changed_if_changed(self: &Arc, cx: &mut Context<'_, Terminal>) { + pub(crate) fn emit_title_changed_if_changed(self: &Arc, cx: &mut Context<'_, Terminal>) { if self.task.lock().is_some() { return; } @@ -220,7 +202,7 @@ impl PtyProcessInfo { })); } - pub fn pid(&self) -> Option { + pub(crate) fn pid(&self) -> Option { self.pid_getter.pid() } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 7d22a86d7d..eeb31d7854 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,37 +1,18 @@ -pub mod mappings; - -pub use alacritty_terminal; +mod mappings; +mod alacritty; mod pty_info; -mod terminal_hyperlinks; pub mod terminal_settings; -use alacritty_terminal::{ - Term, - event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, - event_loop::{EventLoop, Msg, Notifier}, - grid::{Dimensions, Grid, Row, Scroll as AlacScroll}, - index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint}, - selection::{Selection, SelectionRange, SelectionType}, - sync::FairMutex, - term::{ - Config, RenderableCursor, TermMode, - cell::{Cell, Flags}, - search::{Match, RegexIter, RegexSearch}, - }, - tty::{self}, - vi_mode::{ViModeCursor, ViMotion}, - vte::ansi::{ - ClearMode, CursorStyle as AlacCursorStyle, Handler, NamedPrivateMode, PrivateMode, - }, -}; -use anyhow::{Context as _, Result, bail}; +#[cfg(not(windows))] +use anyhow::Context as _; +use anyhow::{Result, bail}; use futures_lite::future::yield_now; use log::trace; use futures::{ FutureExt, - channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}, + channel::mpsc::{UnboundedReceiver, unbounded}, }; use itertools::Itertools as _; @@ -47,8 +28,7 @@ use pty_info::{ProcessIdGetter, PtyProcessInfo}; use serde::{Deserialize, Serialize}; use settings::Settings; use task::{HideStrategy, Shell, SpawnInTerminal}; -use terminal_hyperlinks::RegexSearches; -use terminal_settings::{AlternateScroll, CursorShape, TerminalSettings}; +use terminal_settings::{AlternateScroll, CursorShape as SettingsCursorShape, TerminalSettings}; use theme::{ActiveTheme, Theme}; use urlencoding; use util::{paths::PathStyle, truncate_and_trailoff}; @@ -58,22 +38,534 @@ use std::os::unix::process::ExitStatusExt; use std::{ borrow::Cow, cmp::{self, min}, - fmt::Display, - ops::{Deref, RangeInclusive}, + fmt::{self, Display, Formatter}, + ops::{BitOr, BitOrAssign, Deref, Range as StdRange}, path::{Path, PathBuf}, process::ExitStatus, sync::Arc, time::{Duration, Instant}, }; use thiserror::Error; +use vte::ansi::{Attr, Handler, Processor, StdSyncHandler}; +pub use vte::ansi::{Color, NamedColor, Rgb}; use gpui::{ App, AppContext as _, BackgroundExecutor, Bounds, ClipboardItem, Context, EventEmitter, Hsla, - Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, - Rgba, ScrollWheelEvent, Size, Task, TouchPhase, Window, actions, black, px, + Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, + Point as GpuiPoint, Rgba, ScrollWheelEvent, Size, Task, TouchPhase, Window, actions, black, px, }; -use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; +#[cfg(not(windows))] +use crate::alacritty::current_child_signal_mask; +use crate::alacritty::{ + AlacrittyCell, AlacrittyGridIterator, AlacrittyHyperlink, AlacrittySearch, AlacrittyTerm, + AlacrittyTermConfig, AlacrittyTermLock, HyperlinkMatch, PtySender, RegexSearches, + append_text_to_term, apply_config, clear_saved_screen, content_text, display_offset, + display_only_term_config, find_from_terminal_point, full_content_range, last_non_empty_lines, + make_content, new_term, open_pty, pty_options, pty_term_config, resize, screen_lines, + scroll_display, scroll_to_point, search_matches, selection_text, set_default_cursor_style, + set_selection as set_term_selection, spawn_event_loop, toggle_vi_mode as toggle_term_vi_mode, + total_lines, update_selection as update_term_selection, update_selection_to_vi_cursor, + update_vi_cursor_for_scroll, vi_goto_point, vi_motion, +}; +use crate::mappings::colors::to_vte_rgb; +use crate::mappings::keys::to_esc_str; + +#[derive(Clone, Copy, Debug)] +enum Scroll { + Delta(i32), + PageUp, + PageDown, + Top, + Bottom, +} + +#[derive(Clone, Copy, Debug)] +enum ViMotion { + Up, + Down, + Left, + Right, + First, + Last, + FirstOccupied, + High, + Middle, + Low, + WordLeft, + WordRight, + WordRightEnd, + Bracket, +} + +#[derive(Clone, Debug)] +pub struct Search { + search: AlacrittySearch, +} + +#[derive(Clone, Debug)] +struct Selection { + ty: SelectionType, + start: SelectionAnchor, + end: SelectionAnchor, + head: Point, +} + +#[derive(Clone, Copy, Debug)] +struct SelectionAnchor { + point: Point, + side: SelectionSide, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum SelectionSide { + Left, + Right, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SelectionType { + Simple, + Semantic, + Lines, +} + +impl Selection { + fn new(selection_type: SelectionType, point: Point, side: SelectionSide) -> Self { + let anchor = SelectionAnchor { point, side }; + Self { + ty: selection_type, + start: anchor, + end: anchor, + head: point, + } + } + + fn simple_range(range: Range) -> Self { + let mut selection = Self::new(SelectionType::Simple, range.start(), SelectionSide::Left); + selection.update(range.end(), SelectionSide::Right); + selection + } + + fn update(&mut self, point: Point, side: SelectionSide) { + self.end = SelectionAnchor { point, side }; + self.head = point; + } +} + +pub fn is_default_background_color(color: Color) -> bool { + matches!(color, Color::Named(NamedColor::Background)) +} + +pub fn is_app_chosen_exact_color(color: Color) -> bool { + matches!(color, Color::Spec(_) | Color::Indexed(16..=255)) +} + +pub type AnsiSpans = Vec<(StdRange, Option)>; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ParsedAnsiText { + pub text: String, + pub foreground_spans: AnsiSpans, + pub background_spans: AnsiSpans, +} + +pub fn parse_ansi_text(input: &[u8]) -> ParsedAnsiText { + let mut handler = StyledAnsiTextHandler::default(); + let mut processor = Processor::::default(); + processor.advance(&mut handler, input); + handler.finish() +} + +pub fn strip_ansi_text(input: &[u8]) -> String { + let mut handler = PlainAnsiTextHandler::default(); + let mut processor = Processor::::default(); + processor.advance(&mut handler, input); + handler.text +} + +#[derive(Default)] +struct StyledAnsiTextHandler { + text: String, + foreground_spans: AnsiSpans, + background_spans: AnsiSpans, + current_foreground_range_start: usize, + current_background_range_start: usize, + current_foreground_color: Option, + current_background_color: Option, +} + +impl StyledAnsiTextHandler { + fn finish(mut self) -> ParsedAnsiText { + if self.current_foreground_range_start < self.text.len() { + self.foreground_spans.push(( + self.current_foreground_range_start..self.text.len(), + self.current_foreground_color, + )); + } + + if self.current_background_range_start < self.text.len() { + self.background_spans.push(( + self.current_background_range_start..self.text.len(), + self.current_background_color, + )); + } + + ParsedAnsiText { + text: self.text, + foreground_spans: self.foreground_spans, + background_spans: self.background_spans, + } + } + + fn break_foreground_span(&mut self, color: Option) { + self.foreground_spans.push(( + self.current_foreground_range_start..self.text.len(), + self.current_foreground_color, + )); + self.current_foreground_color = color; + self.current_foreground_range_start = self.text.len(); + } + + fn break_background_span(&mut self, color: Option) { + self.background_spans.push(( + self.current_background_range_start..self.text.len(), + self.current_background_color, + )); + self.current_background_color = color; + self.current_background_range_start = self.text.len(); + } +} + +impl Handler for StyledAnsiTextHandler { + fn input(&mut self, c: char) { + self.text.push(c); + } + + fn linefeed(&mut self) { + self.text.push('\n'); + } + + fn put_tab(&mut self, count: u16) { + self.text.extend(std::iter::repeat_n('\t', count as usize)); + } + + fn terminal_attribute(&mut self, attr: Attr) { + match attr { + Attr::Foreground(color) => { + self.break_foreground_span(Some(color)); + } + Attr::Background(color) => { + self.break_background_span(Some(color)); + } + Attr::Reset => { + self.break_foreground_span(None); + self.break_background_span(None); + } + _ => {} + } + } +} + +#[derive(Default)] +struct PlainAnsiTextHandler { + text: String, + line_start: usize, +} + +impl Handler for PlainAnsiTextHandler { + fn input(&mut self, c: char) { + self.text.push(c); + } + + fn linefeed(&mut self) { + self.text.push('\n'); + self.line_start = self.text.len(); + } + + fn carriage_return(&mut self) { + self.text.truncate(self.line_start); + } + + fn put_tab(&mut self, count: u16) { + self.text.extend(std::iter::repeat_n('\t', count as usize)); + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Hyperlink { + data: HyperlinkData, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +enum HyperlinkData { + Alacritty(AlacrittyHyperlink), + Owned { id: Option>, uri: Arc }, +} + +#[derive(Default, Debug, Clone, Eq, PartialEq)] +pub struct Cell { + cell: AlacrittyCell, +} + +pub struct RenderableCells<'a> { + cells: AlacrittyGridIterator<'a>, +} + +#[derive(Debug, Clone)] +pub struct IndexedCell { + pub point: Point, + pub cell: Cell, +} + +impl Deref for IndexedCell { + type Target = Cell; + + #[inline] + fn deref(&self) -> &Cell { + &self.cell + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct Modes(u32); + +impl Modes { + pub const NONE: Self = Self(0); + pub const APP_CURSOR: Self = Self(1 << 0); + pub const APP_KEYPAD: Self = Self(1 << 1); + pub const SHOW_CURSOR: Self = Self(1 << 2); + pub const LINE_WRAP: Self = Self(1 << 3); + pub const ORIGIN: Self = Self(1 << 4); + pub const INSERT: Self = Self(1 << 5); + pub const LINE_FEED_NEW_LINE: Self = Self(1 << 6); + pub const FOCUS_IN_OUT: Self = Self(1 << 7); + pub const ALTERNATE_SCROLL: Self = Self(1 << 8); + pub const BRACKETED_PASTE: Self = Self(1 << 9); + pub const SGR_MOUSE: Self = Self(1 << 10); + pub const UTF8_MOUSE: Self = Self(1 << 11); + pub const ALT_SCREEN: Self = Self(1 << 12); + pub const MOUSE_REPORT_CLICK: Self = Self(1 << 13); + pub const MOUSE_DRAG: Self = Self(1 << 14); + pub const MOUSE_MOTION: Self = Self(1 << 15); + pub const VI: Self = Self(1 << 16); + pub const MOUSE_MODE: Self = + Self(Self::MOUSE_REPORT_CLICK.0 | Self::MOUSE_DRAG.0 | Self::MOUSE_MOTION.0); + + pub const fn empty() -> Self { + Self::NONE + } + + pub const fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } + + pub const fn intersects(self, other: Self) -> bool { + self.0 & other.0 != 0 + } + + pub fn insert(&mut self, other: Self) { + self.0 |= other.0; + } + + pub fn remove(&mut self, other: Self) { + self.0 &= !other.0; + } +} + +impl BitOr for Modes { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl BitOrAssign for Modes { + fn bitor_assign(&mut self, rhs: Self) { + self.insert(rhs); + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Cursor { + pub shape: CursorShape, + pub point: Point, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CursorShape { + Block, + Underline, + Bar, + HollowBlock, + Hidden, +} + +impl From for CursorShape { + fn from(shape: SettingsCursorShape) -> Self { + match shape { + SettingsCursorShape::Block => Self::Block, + SettingsCursorShape::Underline => Self::Underline, + SettingsCursorShape::Bar => Self::Bar, + SettingsCursorShape::Hollow => Self::HollowBlock, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Point { + pub line: i32, + pub column: usize, +} + +impl Point { + pub fn new(line: i32, column: usize) -> Self { + Self { line, column } + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Range { + start: Point, + end: Point, +} + +impl Range { + pub fn new(start: Point, end: Point) -> Self { + Self { start, end } + } + + pub fn start(&self) -> Point { + self.start + } + + pub fn end(&self) -> Point { + self.end + } + + pub fn contains(&self, point: Point) -> bool { + self.start <= point && point <= self.end + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SelectionRange { + pub start: Point, + pub end: Point, + pub is_block: bool, +} + +impl SelectionRange { + pub fn point_range(self) -> Range { + Range::new(self.start, self.end) + } +} + +// TODO: Un-pub +#[derive(Clone)] +pub struct Content { + pub cells: Vec, + pub mode: Modes, + pub display_offset: usize, + pub selection_text: Option, + pub selection: Option, + pub cursor: Cursor, + pub cursor_char: char, + pub terminal_bounds: TerminalBounds, + pub last_hovered_word: Option, + pub scrolled_to_top: bool, + pub scrolled_to_bottom: bool, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct HoveredWord { + pub word: String, + pub word_match: Range, + pub id: usize, +} + +impl Default for Content { + fn default() -> Self { + Content { + cells: Default::default(), + mode: Default::default(), + display_offset: Default::default(), + selection_text: Default::default(), + selection: Default::default(), + cursor: Cursor { + shape: CursorShape::Block, + point: Point::new(0, 0), + }, + cursor_char: Default::default(), + terminal_bounds: Default::default(), + last_hovered_word: None, + scrolled_to_top: false, + scrolled_to_bottom: false, + } + } +} + +#[derive(PartialEq, Eq)] +enum SelectionPhase { + Selecting, + Ended, +} + +#[cfg(test)] +mod domain_tests { + use super::*; + + #[test] + fn strip_ansi_text_removes_ansi_and_handles_carriage_returns() { + let cases = [ + ("no escape codes here\n", "no escape codes here\n"), + ("\x1b[31mhello\x1b[0m", "hello"), + ("\x1b[1;32mfoo\x1b[0m bar", "foo bar"), + ("progress 10%\rprogress 100%\n", "progress 100%\n"), + ]; + + for (input, expected) in cases { + assert_eq!(strip_ansi_text(input.as_bytes()), expected); + } + } + + #[test] + fn parse_ansi_text_records_foreground_and_background_spans() { + let parsed = parse_ansi_text(b"\x1b[31mred\x1b[44mblue-bg\x1b[0mplain"); + + assert_eq!(parsed.text, "redblue-bgplain"); + assert_eq!( + parsed.foreground_spans, + vec![ + (0..0, None), + (0..10, Some(Color::Named(NamedColor::Red))), + (10..15, None), + ] + ); + assert_eq!( + parsed.background_spans, + vec![ + (0..3, None), + (3..10, Some(Color::Named(NamedColor::Blue))), + (10..15, None), + ] + ); + } + + #[test] + fn terminal_cell_clone_shares_extra_storage() { + let mut cell = Cell::default(); + cell.push_zerowidth('a'); + + let clone = cell.clone(); + + match (&cell.cell.extra, &clone.cell.extra) { + (Some(extra), Some(clone_extra)) => assert!(Arc::ptr_eq(extra, clone_extra)), + _ => panic!("expected extra storage on both cells"), + } + } +} actions!( terminal, @@ -169,30 +661,65 @@ enum InternalEvent { Resize(TerminalBounds), Clear, // FocusNextMatch, - Scroll(AlacScroll), - ScrollToAlacPoint(AlacPoint), - SetSelection(Option<(Selection, AlacPoint)>), - UpdateSelection(Point), - FindHyperlink(Point, bool), - ProcessHyperlink((String, bool, Match), bool), + Scroll(Scroll), + ScrollToPoint(Point), + SetSelection(Option), + UpdateSelection(GpuiPoint), + FindHyperlink(GpuiPoint, bool), + ProcessHyperlink(HyperlinkMatch, bool), // Whether keep selection when copy Copy(Option), // Vi mode events ToggleViMode, ViMotion(ViMotion), - MoveViCursorToAlacPoint(AlacPoint), + MoveViCursorToPoint(Point), } -///A translation struct for Alacritty to communicate with us from their event loop -#[derive(Clone)] -pub struct ZedListener(pub UnboundedSender); +type ClipboardFormatter = Arc String + Sync + Send + 'static>; +type ColorFormatter = Arc String + Sync + Send + 'static>; +type TextAreaSizeFormatter = Arc String + Sync + Send + 'static>; -impl EventListener for ZedListener { - fn send_event(&self, event: AlacTermEvent) { - self.0.unbounded_send(event).ok(); +#[derive(Clone)] +pub(crate) enum TerminalBackendEvent { + MouseCursorDirty, + Title(String), + ResetTitle, + ClipboardStore(String), + ClipboardLoad(ClipboardFormatter), + ColorRequest(usize, ColorFormatter), + PtyWrite(String), + TextAreaSizeRequest(TextAreaSizeFormatter), + CursorBlinkingChange, + Wakeup, + Bell, + Exit, + ChildExit(ExitStatus), +} + +impl fmt::Debug for TerminalBackendEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::MouseCursorDirty => f.write_str("MouseCursorDirty"), + Self::Title(title) => write!(f, "Title({title})"), + Self::ResetTitle => f.write_str("ResetTitle"), + Self::ClipboardStore(data) => write!(f, "ClipboardStore({data})"), + Self::ClipboardLoad(_) => f.write_str("ClipboardLoad"), + Self::ColorRequest(index, _) => write!(f, "ColorRequest({index})"), + Self::PtyWrite(output) => write!(f, "PtyWrite({output})"), + Self::TextAreaSizeRequest(_) => f.write_str("TextAreaSizeRequest"), + Self::CursorBlinkingChange => f.write_str("CursorBlinkingChange"), + Self::Wakeup => f.write_str("Wakeup"), + Self::Bell => f.write_str("Bell"), + Self::Exit => f.write_str("Exit"), + Self::ChildExit(status) => write!(f, "ChildExit({status})"), + } } } +enum PtyEvent { + Event(TerminalBackendEvent), +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct TerminalBounds { pub cell_width: Pixels, @@ -245,7 +772,7 @@ impl Default for TerminalBounds { DEBUG_LINE_HEIGHT, DEBUG_CELL_WIDTH, Bounds { - origin: Point::default(), + origin: GpuiPoint::default(), size: Size { width: DEBUG_TERMINAL_WIDTH, height: DEBUG_TERMINAL_HEIGHT, @@ -255,33 +782,10 @@ impl Default for TerminalBounds { } } -impl From for WindowSize { - fn from(val: TerminalBounds) -> Self { - WindowSize { - num_lines: val.num_lines() as u16, - num_cols: val.num_columns() as u16, - cell_width: f32::from(val.cell_width()) as u16, - cell_height: f32::from(val.line_height()) as u16, - } - } -} - -impl Dimensions for TerminalBounds { - /// Note: this is supposed to be for the back buffer's length, - /// but we exclusively use it to resize the terminal, which does not - /// use this method. We still have to implement it for the trait though, - /// hence, this comment. - fn total_lines(&self) -> usize { - self.screen_lines() - } - - fn screen_lines(&self) -> usize { - self.num_lines() - } - - fn columns(&self) -> usize { - self.num_columns() - } +fn normalize_terminal_bounds(mut bounds: TerminalBounds) -> TerminalBounds { + bounds.bounds.size.height = cmp::max(bounds.line_height, bounds.height()); + bounds.bounds.size.width = cmp::max(bounds.cell_width, bounds.width()); + bounds } #[derive(Error, Debug)] @@ -294,7 +798,7 @@ pub struct TerminalError { } impl TerminalError { - pub fn fmt_directory(&self) -> String { + fn fmt_directory(&self) -> String { self.directory .clone() .map(|path| { @@ -310,7 +814,7 @@ impl TerminalError { .unwrap_or_else(|| "".to_string()) } - pub fn fmt_shell(&self) -> String { + fn fmt_shell(&self) -> String { if let Some(title_override) = &self.title_override { format!( "{} {} ({})", @@ -347,41 +851,47 @@ pub const MAX_SCROLL_HISTORY_LINES: usize = 100_000; pub struct TerminalBuilder { terminal: Terminal, - events_rx: UnboundedReceiver, + events_rx: UnboundedReceiver, } impl TerminalBuilder { pub fn new_display_only( - cursor_shape: CursorShape, + cursor_shape: SettingsCursorShape, alternate_scroll: AlternateScroll, max_scroll_history_lines: Option, window_id: u64, background_executor: &BackgroundExecutor, path_style: PathStyle, - ) -> Result { - // Create a display-only terminal (no actual PTY). - let default_cursor_style = AlacCursorStyle::from(cursor_shape); + ) -> TerminalBuilder { + Self::new_display_only_with_bounds( + cursor_shape, + alternate_scroll, + max_scroll_history_lines, + window_id, + background_executor, + path_style, + TerminalBounds::default(), + ) + } + + pub fn new_display_only_with_bounds( + cursor_shape: SettingsCursorShape, + alternate_scroll: AlternateScroll, + max_scroll_history_lines: Option, + window_id: u64, + background_executor: &BackgroundExecutor, + path_style: PathStyle, + terminal_bounds: TerminalBounds, + ) -> TerminalBuilder { + let terminal_bounds = normalize_terminal_bounds(terminal_bounds); + let scrolling_history = max_scroll_history_lines .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) .min(MAX_SCROLL_HISTORY_LINES); - let config = Config { - scrolling_history, - default_cursor_style, - ..Config::default() - }; + let config = display_only_term_config(scrolling_history, cursor_shape); let (events_tx, events_rx) = unbounded(); - let mut term = Term::new( - config.clone(), - &TerminalBounds::default(), - ZedListener(events_tx), - ); - - if let AlternateScroll::Off = alternate_scroll { - term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); - } - - let term = Arc::new(FairMutex::new(term)); + let term = new_term(&config, terminal_bounds, events_tx, alternate_scroll); let terminal = Terminal { task: None, @@ -389,9 +899,13 @@ impl TerminalBuilder { completion_tx: None, term, term_config: config, + output_processor: Processor::::new(), title_override: None, events: VecDeque::with_capacity(10), - last_content: Default::default(), + last_content: Content { + terminal_bounds, + ..Default::default() + }, last_mouse: None, matches: Vec::new(), @@ -428,10 +942,10 @@ impl TerminalBuilder { input_log: Vec::new(), }; - Ok(TerminalBuilder { + TerminalBuilder { terminal, events_rx, - }) + } } pub fn new( @@ -439,7 +953,7 @@ impl TerminalBuilder { task: Option, shell: Shell, mut env: HashMap, - cursor_shape: CursorShape, + cursor_shape: SettingsCursorShape, alternate_scroll: AlternateScroll, max_scroll_history_lines: Option, path_hyperlink_regexes: Vec, @@ -454,7 +968,7 @@ impl TerminalBuilder { let version = release_channel::AppVersion::global(cx); let background_executor = cx.background_executor().clone(); #[cfg(not(windows))] - let child_signal_mask = match tty::SignalMask::current() + let child_signal_mask = match current_child_signal_mask() .context("failed to capture terminal child signal mask") { Ok(signal_mask) => Some(signal_mask), @@ -536,31 +1050,26 @@ impl TerminalBuilder { // supported remoting into windows. let shell_kind = shell.shell_kind(cfg!(windows)); - let pty_options = { - let alac_shell = shell_params.as_ref().map(|params| { - alacritty_terminal::tty::Shell::new( - params.program.clone(), - params.args.clone().unwrap_or_default(), - ) - }); + let alacritty_shell = shell_params.as_ref().map(|params| { + ( + params.program.clone(), + params.args.clone().unwrap_or_default(), + ) + }); + let pty_options = pty_options( + alacritty_shell, + working_directory.clone(), + env.clone(), + // We pass in the foreground thread's signal mask to the child process via pty_options, + // so terminal construction can run on a background thread without breaking Ctrl-C and other signals + // otherwise the terminal would inherit the background executor's signal mask which blocks + // some terminal signals + #[cfg(not(windows))] + child_signal_mask, + #[cfg(windows)] + shell_kind.tty_escape_args(), + ); - alacritty_terminal::tty::Options { - shell: alac_shell, - working_directory: working_directory.clone(), - drain_on_exit: true, - env: env.clone().into_iter().collect(), - // We pass in the foreground thread's signal mask to the child process via pty_options, - // so terminal construction can run on a background thread without breaking Ctrl-C and other signals - // otherwise the terminal would inherit the background executor's signal mask which blocks - // some terminal signals - #[cfg(not(windows))] - child_signal_mask, - #[cfg(windows)] - escape_args: shell_kind.tty_escape_args(), - } - }; - - let default_cursor_style = AlacCursorStyle::from(cursor_shape); let scrolling_history = if task.is_some() { // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. // After the task finishes, we do not allow appending to that terminal, so small tasks output should not @@ -571,14 +1080,10 @@ impl TerminalBuilder { .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) .min(MAX_SCROLL_HISTORY_LINES) }; - let config = Config { - scrolling_history, - default_cursor_style, - ..Config::default() - }; + let config = pty_term_config(scrolling_history, cursor_shape); //Setup the pty... - let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) { + let pty = match open_pty(&pty_options, TerminalBounds::default(), window_id) { Ok(pty) => pty, Err(error) => { bail!(TerminalError { @@ -595,44 +1100,29 @@ impl TerminalBuilder { //TODO: Remove with a bounded sender which can be dispatched on &self let (events_tx, events_rx) = unbounded(); //Set up the terminal... - let mut term = Term::new( - config.clone(), - &TerminalBounds::default(), - ZedListener(events_tx.clone()), + let term = new_term( + &config, + TerminalBounds::default(), + events_tx.clone(), + alternate_scroll, ); - //Alacritty defaults to alternate scrolling being on, so we just need to turn it off. - if let AlternateScroll::Off = alternate_scroll { - term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); - } - - let term = Arc::new(FairMutex::new(term)); - - let pty_info = PtyProcessInfo::new(&pty); + let pty_info = PtyProcessInfo::new(ProcessIdGetter::from(&pty)); //And connect them together - let event_loop = EventLoop::new( - term.clone(), - ZedListener(events_tx), - pty, - pty_options.drain_on_exit, - false, - ) - .context("failed to create event loop")?; - - let pty_tx = event_loop.channel(); - let _io_thread = event_loop.spawn(); // DANGER + let pty_tx = spawn_event_loop(term.clone(), events_tx, pty, pty_options.drain_on_exit)?; let no_task = task.is_none(); let terminal = Terminal { task, terminal_type: TerminalType::Pty { - pty_tx: Notifier(pty_tx), + pty_tx, info: Arc::new(pty_info), }, completion_tx, term, term_config: config, + output_processor: Processor::::new(), title_override: terminal_title_override, events: VecDeque::with_capacity(10), //Should never get this high. last_content: Default::default(), @@ -710,7 +1200,7 @@ impl TerminalBuilder { while let Some(event) = self.events_rx.next().await { terminal.update(cx, |terminal, cx| { //Process the first event immediately for lowered latency - terminal.process_event(event, cx); + terminal.process_pty_event(event, cx); })?; 'outer: loop { @@ -730,7 +1220,8 @@ impl TerminalBuilder { _ = timer => break, event = self.events_rx.next() => { if let Some(event) = event { - if matches!(event, AlacTermEvent::Wakeup) { + if matches!(event, PtyEvent::Event(TerminalBackendEvent::Wakeup)) + { wakeup = true; } else { events.push(event); @@ -753,11 +1244,11 @@ impl TerminalBuilder { terminal.update(cx, |this, cx| { if wakeup { - this.process_event(AlacTermEvent::Wakeup, cx); + this.process_event(TerminalBackendEvent::Wakeup, cx); } for event in events { - this.process_event(event, cx); + this.process_pty_event(event, cx); } })?; yield_now().await; @@ -787,74 +1278,9 @@ impl TerminalBuilder { } } -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct IndexedCell { - pub point: AlacPoint, - pub cell: Cell, -} - -impl Deref for IndexedCell { - type Target = Cell; - - #[inline] - fn deref(&self) -> &Cell { - &self.cell - } -} - -// TODO: Un-pub -#[derive(Clone)] -pub struct TerminalContent { - pub cells: Vec, - pub mode: TermMode, - pub display_offset: usize, - pub selection_text: Option, - pub selection: Option, - pub cursor: RenderableCursor, - pub cursor_char: char, - pub terminal_bounds: TerminalBounds, - pub last_hovered_word: Option, - pub scrolled_to_top: bool, - pub scrolled_to_bottom: bool, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct HoveredWord { - pub word: String, - pub word_match: RangeInclusive, - pub id: usize, -} - -impl Default for TerminalContent { - fn default() -> Self { - TerminalContent { - cells: Default::default(), - mode: Default::default(), - display_offset: Default::default(), - selection_text: Default::default(), - selection: Default::default(), - cursor: RenderableCursor { - shape: alacritty_terminal::vte::ansi::CursorShape::Block, - point: AlacPoint::new(Line(0), Column(0)), - }, - cursor_char: Default::default(), - terminal_bounds: Default::default(), - last_hovered_word: None, - scrolled_to_top: false, - scrolled_to_bottom: false, - } - } -} - -#[derive(PartialEq, Eq)] -pub enum SelectionPhase { - Selecting, - Ended, -} - enum TerminalType { Pty { - pty_tx: Notifier, + pty_tx: PtySender, info: Arc, }, DisplayOnly, @@ -863,14 +1289,15 @@ enum TerminalType { pub struct Terminal { terminal_type: TerminalType, completion_tx: Option>>, - term: Arc>>, - term_config: Config, + term: Arc, + term_config: AlacrittyTermConfig, + output_processor: Processor, events: VecDeque, /// This is only used for mouse mode cell change detection - last_mouse: Option<(AlacPoint, AlacDirection)>, - pub matches: Vec>, - pub last_content: TerminalContent, - pub selection_head: Option, + last_mouse: Option<(Point, SelectionSide)>, + pub matches: Vec, + pub last_content: Content, + pub selection_head: Option, pub breadcrumb_text: String, title_override: Option, @@ -882,8 +1309,8 @@ pub struct Terminal { vi_mode_enabled: bool, is_remote_terminal: bool, last_mouse_move_time: Instant, - last_hyperlink_search_position: Option>, - mouse_down_hyperlink: Option<(String, bool, Match)>, + last_hyperlink_search_position: Option>, + mouse_down_hyperlink: Option, #[cfg(windows)] shell_program: Option, template: CopyTemplate, @@ -900,7 +1327,7 @@ pub struct Terminal { struct CopyTemplate { shell: Shell, env: HashMap, - cursor_shape: CursorShape, + cursor_shape: SettingsCursorShape, alternate_scroll: AlternateScroll, max_scroll_history_lines: Option, path_hyperlink_regexes: Vec, @@ -944,34 +1371,38 @@ impl TaskStatus { const FIND_HYPERLINK_THROTTLE_PX: Pixels = px(5.0); impl Terminal { - fn process_event(&mut self, event: AlacTermEvent, cx: &mut Context) { + fn process_pty_event(&mut self, event: PtyEvent, cx: &mut Context) { match event { - AlacTermEvent::Title(title) => { + PtyEvent::Event(event) => self.process_event(event, cx), + } + } + + fn process_event(&mut self, event: TerminalBackendEvent, cx: &mut Context) { + match event { + TerminalBackendEvent::Title(title) => { // ignore default shell program title change as windows always sends those events // and it would end up showing the shell executable path in breadcrumbs #[cfg(windows)] + if self + .shell_program + .as_ref() + .map(|e| *e == title) + .unwrap_or(false) { - if self - .shell_program - .as_ref() - .map(|e| *e == title) - .unwrap_or(false) - { - return; - } + return; } self.breadcrumb_text = title; cx.emit(Event::BreadcrumbsChanged); } - AlacTermEvent::ResetTitle => { + TerminalBackendEvent::ResetTitle => { self.breadcrumb_text = String::new(); cx.emit(Event::BreadcrumbsChanged); } - AlacTermEvent::ClipboardStore(_, data) => { + TerminalBackendEvent::ClipboardStore(data) => { cx.write_to_clipboard(ClipboardItem::new_string(data)) } - AlacTermEvent::ClipboardLoad(_, format) => { + TerminalBackendEvent::ClipboardLoad(format) => { self.write_to_pty( match &cx.read_from_clipboard().and_then(|item| item.text()) { // The terminal only supports pasting strings, not images. @@ -981,30 +1412,30 @@ impl Terminal { .into_bytes(), ) } - AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.into_bytes()), - AlacTermEvent::TextAreaSizeRequest(format) => { - self.write_to_pty(format(self.last_content.terminal_bounds.into()).into_bytes()) + TerminalBackendEvent::PtyWrite(out) => self.write_to_pty(out.into_bytes()), + TerminalBackendEvent::TextAreaSizeRequest(format) => { + self.write_to_pty(format(self.last_content.terminal_bounds).into_bytes()) } - AlacTermEvent::CursorBlinkingChange => { + TerminalBackendEvent::CursorBlinkingChange => { let terminal = self.term.lock(); let blinking = terminal.cursor_style().blinking; cx.emit(Event::BlinkChanged(blinking)); } - AlacTermEvent::Bell => { + TerminalBackendEvent::Bell => { cx.emit(Event::Bell); } - AlacTermEvent::Exit => self.register_task_finished(None, cx), - AlacTermEvent::MouseCursorDirty => { + TerminalBackendEvent::Exit => self.register_task_finished(None, cx), + TerminalBackendEvent::MouseCursorDirty => { //NOOP, Handled in render } - AlacTermEvent::Wakeup => { + TerminalBackendEvent::Wakeup => { cx.emit(Event::Wakeup); if let TerminalType::Pty { info, .. } = &self.terminal_type { info.emit_title_changed_if_changed(cx); } } - AlacTermEvent::ColorRequest(index, format) => { + TerminalBackendEvent::ColorRequest(index, format) => { // It's important that the color request is processed here to retain relative order // with other PTY writes. Otherwise applications might witness out-of-order // responses to requests. For example: An application sending `OSC 11 ; ? ST` @@ -1013,11 +1444,12 @@ impl Terminal { // Instead of locking, we could store the colors in `self.last_content`. But then // we might respond with out of date value if a "set color" sequence is immediately // followed by a color request sequence. + let color = self.term.lock().colors()[index] - .unwrap_or_else(|| to_alac_rgb(get_color_at_index(index, cx.theme().as_ref()))); + .unwrap_or_else(|| to_vte_rgb(get_color_at_index(index, cx.theme().as_ref()))); self.write_to_pty(format(color).into_bytes()); } - AlacTermEvent::ChildExit(exit_status) => { + TerminalBackendEvent::ChildExit(exit_status) => { self.register_task_finished(Some(exit_status), cx); } } @@ -1030,24 +1462,22 @@ impl Terminal { fn process_terminal_event( &mut self, event: &InternalEvent, - term: &mut Term, + term: &mut AlacrittyTerm, window: &mut Window, cx: &mut Context, ) { match event { - &InternalEvent::Resize(mut new_bounds) => { + &InternalEvent::Resize(new_bounds) => { + let new_bounds = normalize_terminal_bounds(new_bounds); trace!("Resizing: new_bounds={new_bounds:?}"); - new_bounds.bounds.size.height = - cmp::max(new_bounds.line_height, new_bounds.height()); - new_bounds.bounds.size.width = cmp::max(new_bounds.cell_width, new_bounds.width()); self.last_content.terminal_bounds = new_bounds; if let TerminalType::Pty { pty_tx, .. } = &self.terminal_type { - pty_tx.0.send(Msg::Resize(new_bounds.into())).ok(); + pty_tx.resize(new_bounds); } - term.resize(new_bounds); + resize(term, new_bounds); // If there are matches we need to emit a wake up event to // invalidate the matches and recalculate their locations // in the new terminal layout @@ -1057,107 +1487,52 @@ impl Terminal { } InternalEvent::Clear => { trace!("Clearing"); - // Clear back buffer - term.clear_screen(ClearMode::Saved); - - let cursor = term.grid().cursor.point; - - // Clear the lines above - term.grid_mut().reset_region(..cursor.line); - - // Copy the current line up - let line = term.grid()[cursor.line][..Column(term.grid().columns())] - .iter() - .cloned() - .enumerate() - .collect::>(); - - for (i, cell) in line { - term.grid_mut()[Line(0)][Column(i)] = cell; - } - - // Reset the cursor - term.grid_mut().cursor.point = - AlacPoint::new(Line(0), term.grid_mut().cursor.point.column); - let new_cursor = term.grid().cursor.point; - - // Clear the lines below the new cursor - if (new_cursor.line.0 as usize) < term.screen_lines() - 1 { - term.grid_mut().reset_region((new_cursor.line + 1)..); - } - + clear_saved_screen(term); cx.emit(Event::Wakeup); } InternalEvent::Scroll(scroll) => { trace!("Scrolling: scroll={scroll:?}"); - term.scroll_display(*scroll); + scroll_display(term, *scroll); self.refresh_hovered_word(window); if self.vi_mode_enabled { - match *scroll { - AlacScroll::Delta(delta) => { - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, delta); - } - AlacScroll::PageUp => { - let lines = term.screen_lines() as i32; - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); - } - AlacScroll::PageDown => { - let lines = -(term.screen_lines() as i32); - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); - } - AlacScroll::Top => { - let point = AlacPoint::new(term.topmost_line(), Column(0)); - term.vi_mode_cursor = ViModeCursor::new(point); - } - AlacScroll::Bottom => { - let point = AlacPoint::new(term.bottommost_line(), Column(0)); - term.vi_mode_cursor = ViModeCursor::new(point); - } - } - if let Some(mut selection) = term.selection.take() { - let point = term.vi_mode_cursor.point; - selection.update(point, AlacDirection::Right); - term.selection = Some(selection); - + update_vi_cursor_for_scroll(term, *scroll); + if let Some(selection_head) = update_selection_to_vi_cursor(term) { #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if let Some(selection_text) = term.selection_to_string() { + if let Some(selection_text) = selection_text(term) { cx.write_to_primary(ClipboardItem::new_string(selection_text)); } - self.selection_head = Some(point); + self.selection_head = Some(selection_head); cx.emit(Event::SelectionsChanged) } } } InternalEvent::SetSelection(selection) => { trace!("Setting selection: selection={selection:?}"); - term.selection = selection.as_ref().map(|(sel, _)| sel.clone()); + set_term_selection(term, selection.as_ref()); #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if let Some(selection_text) = term.selection_to_string() { + if let Some(selection_text) = selection_text(term) { cx.write_to_primary(ClipboardItem::new_string(selection_text)); } - if let Some((_, head)) = selection { - self.selection_head = Some(*head); + if let Some(selection) = selection { + self.selection_head = Some(selection.head); } cx.emit(Event::SelectionsChanged) } InternalEvent::UpdateSelection(position) => { trace!("Updating selection: position={position:?}"); - if let Some(mut selection) = term.selection.take() { - let (point, side) = grid_point_and_side( - *position, - self.last_content.terminal_bounds, - term.grid().display_offset(), - ); - - selection.update(point, side); - term.selection = Some(selection); + let (point, side) = grid_point_and_side( + *position, + self.last_content.terminal_bounds, + display_offset(term), + ); + if update_term_selection(term, point, side) { #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if let Some(selection_text) = term.selection_to_string() { + if let Some(selection_text) = selection_text(term) { cx.write_to_primary(ClipboardItem::new_string(selection_text)); } @@ -1168,7 +1543,7 @@ impl Terminal { InternalEvent::Copy(keep_selection) => { trace!("Copying selection: keep_selection={keep_selection:?}"); - if let Some(txt) = term.selection_to_string() { + if let Some(txt) = selection_text(term) { cx.write_to_clipboard(ClipboardItem::new_string(txt)); if !keep_selection.unwrap_or_else(|| { let settings = TerminalSettings::get_global(cx); @@ -1178,24 +1553,24 @@ impl Terminal { } } } - InternalEvent::ScrollToAlacPoint(point) => { + InternalEvent::ScrollToPoint(point) => { trace!("Scrolling to point: point={point:?}"); - term.scroll_to_point(*point); + scroll_to_point(term, *point); self.refresh_hovered_word(window); } - InternalEvent::MoveViCursorToAlacPoint(point) => { + InternalEvent::MoveViCursorToPoint(point) => { trace!("Move vi cursor to point: point={point:?}"); - term.vi_goto_point(*point); + vi_goto_point(term, *point); self.refresh_hovered_word(window); } InternalEvent::ToggleViMode => { trace!("Toggling vi mode"); self.vi_mode_enabled = !self.vi_mode_enabled; - term.toggle_vi_mode(); + toggle_term_vi_mode(term); } InternalEvent::ViMotion(motion) => { trace!("Performing vi motion: motion={motion:?}"); - term.vi_motion(*motion); + vi_motion(term, *motion); } InternalEvent::FindHyperlink(position, open) => { trace!("Finding hyperlink at position: position={position:?}, open={open:?}"); @@ -1203,11 +1578,10 @@ impl Terminal { let point = grid_point( *position, self.last_content.terminal_bounds, - term.grid().display_offset(), - ) - .grid_clamp(term, Boundary::Grid); + display_offset(term), + ); - match terminal_hyperlinks::find_from_grid_point( + match find_from_terminal_point( term, point, &mut self.hyperlink_regex_searches, @@ -1228,13 +1602,12 @@ impl Terminal { } } - fn process_hyperlink( - &mut self, - hyperlink: (String, bool, Match), - open: bool, - cx: &mut Context, - ) { - let (maybe_url_or_path, is_url, url_match) = hyperlink; + fn process_hyperlink(&mut self, hyperlink: HyperlinkMatch, open: bool, cx: &mut Context) { + let HyperlinkMatch { + text: maybe_url_or_path, + is_url, + range, + } = hyperlink; let prev_hovered_word = self.last_content.last_hovered_word.take(); let target = if is_url { @@ -1260,14 +1633,24 @@ impl Terminal { if open { cx.emit(Event::Open(target)); } else { - self.update_selected_word(prev_hovered_word, url_match, maybe_url_or_path, target, cx); + self.update_selected_word(prev_hovered_word, range, maybe_url_or_path, target, cx); } } + fn find_hyperlink_at_point(&mut self, point: Point) -> Option { + let term_lock = self.term.lock(); + find_from_terminal_point( + &term_lock, + point, + &mut self.hyperlink_regex_searches, + self.path_style, + ) + } + fn update_selected_word( &mut self, prev_word: Option, - word_match: RangeInclusive, + word_match: Range, word: String, navigation_target: MaybeNavigationTarget, cx: &mut Context, @@ -1299,13 +1682,13 @@ impl Terminal { res } - pub fn last_content(&self) -> &TerminalContent { + pub fn last_content(&self) -> &Content { &self.last_content } - pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape) { - self.term_config.default_cursor_style = cursor_shape.into(); - self.term.lock().set_options(self.term_config.clone()); + pub fn set_cursor_shape(&mut self, cursor_shape: SettingsCursorShape) { + set_default_cursor_style(&mut self.term_config, cursor_shape); + apply_config(&self.term, &self.term_config); } pub fn write_output(&mut self, bytes: &[u8], cx: &mut Context) { @@ -1329,22 +1712,17 @@ impl Terminal { prev_byte = byte; } - let mut processor = alacritty_terminal::vte::ansi::Processor::< - alacritty_terminal::vte::ansi::StdSyncHandler, - >::new(); - { - let mut term = self.term.lock(); - processor.advance(&mut *term, &converted); - } + let mut term = self.term.lock(); + self.output_processor.advance(&mut *term, &converted); cx.emit(Event::Wakeup); } pub fn total_lines(&self) -> usize { - self.term.lock_unfair().total_lines() + total_lines(&self.term.lock_unfair()) } pub fn viewport_lines(&self) -> usize { - self.term.lock_unfair().screen_lines() + screen_lines(&self.term.lock_unfair()) } //To test: @@ -1353,18 +1731,18 @@ impl Terminal { pub fn activate_match(&mut self, index: usize) { if let Some(search_match) = self.matches.get(index).cloned() { - self.set_selection(Some((make_selection(&search_match), *search_match.end()))); + self.set_selection(Some(Selection::simple_range(search_match))); if self.vi_mode_enabled { self.events - .push_back(InternalEvent::MoveViCursorToAlacPoint(*search_match.end())); + .push_back(InternalEvent::MoveViCursorToPoint(search_match.end())); } else { self.events - .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start())); + .push_back(InternalEvent::ScrollToPoint(search_match.start())); } } } - pub fn select_matches(&mut self, matches: &[RangeInclusive]) { + pub fn select_matches(&mut self, matches: &[Range]) { let matches_to_select = self .matches .iter() @@ -1372,22 +1750,18 @@ impl Terminal { .cloned() .collect::>(); for match_to_select in matches_to_select { - self.set_selection(Some(( - make_selection(&match_to_select), - *match_to_select.end(), - ))); + self.set_selection(Some(Selection::simple_range(match_to_select))); } } pub fn select_all(&mut self) { let term = self.term.lock(); - let start = AlacPoint::new(term.topmost_line(), Column(0)); - let end = AlacPoint::new(term.bottommost_line(), term.last_column()); + let range = full_content_range(&term); drop(term); - self.set_selection(Some((make_selection(&(start..=end)), end))); + self.set_selection(Some(Selection::simple_range(range))); } - fn set_selection(&mut self, selection: Option<(Selection, AlacPoint)>) { + fn set_selection(&mut self, selection: Option) { self.events .push_back(InternalEvent::SetSelection(selection)); } @@ -1402,42 +1776,39 @@ impl Terminal { pub fn scroll_line_up(&mut self) { self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(1))); + .push_back(InternalEvent::Scroll(Scroll::Delta(1))); } pub fn scroll_up_by(&mut self, lines: usize) { self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(lines as i32))); + .push_back(InternalEvent::Scroll(Scroll::Delta(lines as i32))); } pub fn scroll_line_down(&mut self) { self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(-1))); + .push_back(InternalEvent::Scroll(Scroll::Delta(-1))); } pub fn scroll_down_by(&mut self, lines: usize) { self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(-(lines as i32)))); + .push_back(InternalEvent::Scroll(Scroll::Delta(-(lines as i32)))); } pub fn scroll_page_up(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::PageUp)); + self.events.push_back(InternalEvent::Scroll(Scroll::PageUp)); } pub fn scroll_page_down(&mut self) { self.events - .push_back(InternalEvent::Scroll(AlacScroll::PageDown)); + .push_back(InternalEvent::Scroll(Scroll::PageDown)); } pub fn scroll_to_top(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Top)); + self.events.push_back(InternalEvent::Scroll(Scroll::Top)); } pub fn scroll_to_bottom(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); + self.events.push_back(InternalEvent::Scroll(Scroll::Bottom)); } pub fn scrolled_to_top(&self) -> bool { @@ -1450,9 +1821,7 @@ impl Terminal { ///Resize the terminal and the PTY. pub fn set_size(&mut self, new_bounds: TerminalBounds) { - let mut new_bounds = new_bounds; - new_bounds.bounds.size.height = cmp::max(new_bounds.line_height, new_bounds.height()); - new_bounds.bounds.size.width = cmp::max(new_bounds.cell_width, new_bounds.width()); + let new_bounds = normalize_terminal_bounds(new_bounds); let old_bounds = self.last_content.terminal_bounds; self.last_content.terminal_bounds = new_bounds; @@ -1491,8 +1860,7 @@ impl Terminal { } pub fn input(&mut self, input: impl Into>) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); + self.events.push_back(InternalEvent::Scroll(Scroll::Bottom)); self.events.push_back(InternalEvent::SetSelection(None)); self.keyboard_input_sent = true; @@ -1543,9 +1911,9 @@ impl Terminal { if let Some(motion) = motion { let cursor = self.last_content.cursor.point; - let cursor_pos = Point { - x: cursor.column.0 as f32 * self.last_content.terminal_bounds.cell_width, - y: cursor.line.0 as f32 * self.last_content.terminal_bounds.line_height, + let cursor_pos = GpuiPoint { + x: cursor.column as f32 * self.last_content.terminal_bounds.cell_width, + y: cursor.line as f32 * self.last_content.terminal_bounds.line_height, }; self.events .push_back(InternalEvent::UpdateSelection(cursor_pos)); @@ -1554,17 +1922,17 @@ impl Terminal { } let scroll_motion = match key.as_ref() { - "g" => Some(AlacScroll::Top), - "G" => Some(AlacScroll::Bottom), - "b" if keystroke.modifiers.control => Some(AlacScroll::PageUp), - "f" if keystroke.modifiers.control => Some(AlacScroll::PageDown), + "g" => Some(Scroll::Top), + "G" => Some(Scroll::Bottom), + "b" if keystroke.modifiers.control => Some(Scroll::PageUp), + "f" if keystroke.modifiers.control => Some(Scroll::PageDown), "d" if keystroke.modifiers.control => { let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2; - Some(AlacScroll::Delta(-amount)) + Some(Scroll::Delta(-amount)) } "u" if keystroke.modifiers.control => { let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2; - Some(AlacScroll::Delta(amount)) + Some(Scroll::Delta(amount)) } _ => None, }; @@ -1578,10 +1946,10 @@ impl Terminal { "v" => { let point = self.last_content.cursor.point; let selection_type = SelectionType::Simple; - let side = AlacDirection::Right; + let side = SelectionSide::Right; let selection = Selection::new(selection_type, point, side); self.events - .push_back(InternalEvent::SetSelection(Some((selection, point)))); + .push_back(InternalEvent::SetSelection(Some(selection))); } "escape" => { @@ -1607,7 +1975,7 @@ impl Terminal { } // Keep default terminal behavior - let esc = to_esc_str(keystroke, &self.last_content.mode, option_as_meta); + let esc = to_esc_str(keystroke, self.last_content.mode, option_as_meta); if let Some(esc) = esc { match esc { Cow::Borrowed(string) => self.input(string.as_bytes()), @@ -1639,7 +2007,7 @@ impl Terminal { ///Paste text into the terminal pub fn paste(&mut self, text: &str) { - let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) { + let paste_text = if self.last_content.mode.contains(Modes::BRACKETED_PASTE) { format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~") } else { text.replace("\r\n", "\r").replace('\n', "\r") @@ -1656,118 +2024,38 @@ impl Terminal { self.process_terminal_event(&e, &mut terminal, window, cx) } - self.last_content = Self::make_content(&terminal, &self.last_content); + self.last_content = make_content(&terminal, &self.last_content); } - fn make_content(term: &Term, last_content: &TerminalContent) -> TerminalContent { + pub fn with_renderable_cells(&self, f: impl for<'a> FnOnce(RenderableCells<'a>) -> R) -> R { + let term = self.term.lock_unfair(); let content = term.renderable_content(); - - // Pre-allocate with estimated size to reduce reallocations - let estimated_size = content.display_iter.size_hint().0; - let mut cells = Vec::with_capacity(estimated_size); - - cells.extend(content.display_iter.map(|ic| IndexedCell { - point: ic.point, - cell: ic.cell.clone(), - })); - - let selection_text = if content.selection.is_some() { - term.selection_to_string() - } else { - None - }; - - TerminalContent { - cells, - mode: content.mode, - display_offset: content.display_offset, - selection_text, - selection: content.selection, - cursor: content.cursor, - cursor_char: term.grid()[content.cursor.point].c, - terminal_bounds: last_content.terminal_bounds, - last_hovered_word: last_content.last_hovered_word.clone(), - scrolled_to_top: content.display_offset == term.history_size(), - scrolled_to_bottom: content.display_offset == 0, - } + f(RenderableCells::new(content.display_iter)) } pub fn get_content(&self) -> String { let term = self.term.lock_unfair(); - let start = AlacPoint::new(term.topmost_line(), Column(0)); - let end = AlacPoint::new(term.bottommost_line(), term.last_column()); - term.bounds_to_string(start, end) + content_text(&term) } pub fn last_n_non_empty_lines(&self, n: usize) -> Vec { - let term = self.term.clone(); - let terminal = term.lock_unfair(); - let grid = terminal.grid(); - let mut lines = Vec::new(); - - let mut current_line = grid.bottommost_line().0; - let topmost_line = grid.topmost_line().0; - - while current_line >= topmost_line && lines.len() < n { - let logical_line_start = self.find_logical_line_start(grid, current_line, topmost_line); - let logical_line = self.construct_logical_line(grid, logical_line_start, current_line); - - if let Some(line) = self.process_line(logical_line) { - lines.push(line); - } - - // Move to the line above the start of the current logical line - current_line = logical_line_start - 1; - } - - lines.reverse(); - lines - } - - fn find_logical_line_start(&self, grid: &Grid, current: i32, topmost: i32) -> i32 { - let mut line_start = current; - while line_start > topmost { - let prev_line = Line(line_start - 1); - let last_cell = &grid[prev_line][Column(grid.columns() - 1)]; - if !last_cell.flags.contains(Flags::WRAPLINE) { - break; - } - line_start -= 1; - } - line_start - } - - fn construct_logical_line(&self, grid: &Grid, start: i32, end: i32) -> String { - let mut logical_line = String::new(); - for row in start..=end { - let grid_row = &grid[Line(row)]; - logical_line.push_str(&row_to_string(grid_row)); - } - logical_line - } - - fn process_line(&self, line: String) -> Option { - let trimmed = line.trim_end().to_string(); - if !trimmed.is_empty() { - Some(trimmed) - } else { - None - } + let terminal = self.term.lock_unfair(); + last_non_empty_lines(&terminal, n) } pub fn focus_in(&self) { - if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) { + if self.last_content.mode.contains(Modes::FOCUS_IN_OUT) { self.write_to_pty("\x1b[I".as_bytes()); } } pub fn focus_out(&mut self) { - if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) { + if self.last_content.mode.contains(Modes::FOCUS_IN_OUT) { self.write_to_pty("\x1b[O".as_bytes()); } } - pub fn mouse_changed(&mut self, point: AlacPoint, side: AlacDirection) -> bool { + fn mouse_changed(&mut self, point: Point, side: SelectionSide) -> bool { match self.last_mouse { Some((old_point, old_side)) => { if old_point == point && old_side == side { @@ -1785,7 +2073,7 @@ impl Terminal { } pub fn mouse_mode(&self, shift: bool) -> bool { - self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift + self.last_content.mode.intersects(Modes::MOUSE_MODE) && !shift } pub fn mouse_move(&mut self, e: &MouseMoveEvent, cx: &mut Context) { @@ -1797,11 +2085,17 @@ impl Terminal { self.last_content.display_offset, ); - if self.mouse_changed(point, side) - && let Some(bytes) = - mouse_moved_report(point, e.pressed_button, e.modifiers, self.last_content.mode) - { - self.write_to_pty(bytes); + if self.mouse_changed(point, side) { + let bytes = mouse_moved_report( + point, + e.pressed_button, + e.modifiers, + self.last_content.mode, + ); + + if let Some(bytes) = bytes { + self.write_to_pty(bytes); + } } } else { self.schedule_find_hyperlink(e.modifiers, e.position); @@ -1809,7 +2103,7 @@ impl Terminal { cx.notify(); } - fn schedule_find_hyperlink(&mut self, modifiers: Modifiers, position: Point) { + fn schedule_find_hyperlink(&mut self, modifiers: Modifiers, position: GpuiPoint) { if self.selection_phase == SelectionPhase::Selecting || !modifiers.secondary() || !self.last_content.terminal_bounds.bounds.contains(&position) @@ -1849,7 +2143,7 @@ impl Terminal { ); let selection = Selection::new(SelectionType::Semantic, point, side); self.events - .push_back(InternalEvent::SetSelection(Some((selection, point)))); + .push_back(InternalEvent::SetSelection(Some(selection))); } pub fn mouse_drag( @@ -1860,14 +2154,14 @@ impl Terminal { ) { let position = e.position - self.last_content.terminal_bounds.bounds.origin; if !self.mouse_mode(e.modifiers.shift) { - if let Some((.., hyperlink_range)) = &self.mouse_down_hyperlink { + if let Some(hyperlink) = &self.mouse_down_hyperlink { let point = grid_point( position, self.last_content.terminal_bounds, self.last_content.display_offset, ); - if !hyperlink_range.contains(&point) { + if !hyperlink.range.contains(point) { self.mouse_down_hyperlink = None; } else { return; @@ -1881,14 +2175,14 @@ impl Terminal { .push_back(InternalEvent::UpdateSelection(position)); // Doesn't make sense to scroll the alt screen - if !self.last_content.mode.contains(TermMode::ALT_SCREEN) { + if !self.last_content.mode.contains(Modes::ALT_SCREEN) { let scroll_lines = match self.drag_line_delta(e, region) { Some(value) => value, None => return, }; self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines))); + .push_back(InternalEvent::Scroll(Scroll::Delta(scroll_lines))); } cx.notify(); @@ -1924,14 +2218,7 @@ impl Terminal { && e.modifiers.secondary() && !self.mouse_mode(e.modifiers.shift) { - let term_lock = self.term.lock(); - self.mouse_down_hyperlink = terminal_hyperlinks::find_from_grid_point( - &term_lock, - point, - &mut self.hyperlink_regex_searches, - self.path_style, - ); - drop(term_lock); + self.mouse_down_hyperlink = self.find_hyperlink_at_point(point); if self.mouse_down_hyperlink.is_some() { return; @@ -1939,9 +2226,10 @@ impl Terminal { } if self.mouse_mode(e.modifiers.shift) { - if let Some(bytes) = - mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode) - { + let bytes = + mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode); + + if let Some(bytes) = bytes { self.write_to_pty(bytes); } } else { @@ -1970,9 +2258,9 @@ impl Terminal { let selection = selection_type .map(|selection_type| Selection::new(selection_type, point, side)); - if let Some(sel) = selection { + if let Some(selection) = selection { self.events - .push_back(InternalEvent::SetSelection(Some((sel, point)))); + .push_back(InternalEvent::SetSelection(Some(selection))); } } #[cfg(any(target_os = "linux", target_os = "freebsd"))] @@ -1998,9 +2286,10 @@ impl Terminal { self.last_content.display_offset, ); - if let Some(bytes) = - mouse_button_report(point, e.button, e.modifiers, false, self.last_content.mode) - { + let bytes = + mouse_button_report(point, e.button, e.modifiers, false, self.last_content.mode); + + if let Some(bytes) = bytes { self.write_to_pty(bytes); } } else { @@ -2015,15 +2304,7 @@ impl Terminal { self.last_content.display_offset, ); - if let Some(mouse_up_hyperlink) = { - let term_lock = self.term.lock(); - terminal_hyperlinks::find_from_grid_point( - &term_lock, - point, - &mut self.hyperlink_regex_searches, - self.path_style, - ) - } { + if let Some(mouse_up_hyperlink) = self.find_hyperlink_at_point(point) { if mouse_down_hyperlink == mouse_up_hyperlink { self.events .push_back(InternalEvent::ProcessHyperlink(mouse_up_hyperlink, true)); @@ -2038,7 +2319,12 @@ impl Terminal { if self.selection_phase == SelectionPhase::Ended { let mouse_cell_index = content_index_for_mouse(position, &self.last_content.terminal_bounds); - if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { + if let Some(link) = self + .last_content + .cells + .get(mouse_cell_index) + .and_then(|cell| cell.hyperlink()) + { cx.open_url(link.uri()); } else if e.modifiers.secondary() { self.events @@ -2075,14 +2361,13 @@ impl Terminal { } else if self .last_content .mode - .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL) + .contains(Modes::ALT_SCREEN | Modes::ALTERNATE_SCROLL) && !e.shift { self.write_to_pty(alt_scroll(scroll_lines)); } else { - let scroll = AlacScroll::Delta(scroll_lines); - - self.events.push_back(InternalEvent::Scroll(scroll)); + self.events + .push_back(InternalEvent::Scroll(Scroll::Delta(scroll_lines))); } } } @@ -2121,16 +2406,11 @@ impl Terminal { } } - pub fn find_matches( - &self, - mut searcher: RegexSearch, - cx: &Context, - ) -> Task>> { + pub fn find_matches(&self, searcher: Search, cx: &Context) -> Task> { let term = self.term.clone(); cx.background_spawn(async move { let term = term.lock(); - - all_search_matches(&term, &mut searcher).collect() + search_matches(&term, searcher) }) } @@ -2320,6 +2600,7 @@ impl Terminal { if task.spawned_task.show_command { lines_to_show.push(command_line.as_str()); } + let hide = task.spawned_task.hide; if !lines_to_show.is_empty() { // SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once, @@ -2329,7 +2610,7 @@ impl Terminal { unsafe { append_text_to_term(&mut self.term.lock(), &lines_to_show) }; } - match task.spawned_task.hide { + match hide { HideStrategy::Never => {} HideStrategy::Always => { cx.emit(Event::CloseTerminal); @@ -2368,14 +2649,6 @@ impl Terminal { } } -// Helper function to convert a grid row to a string -pub fn row_to_string(row: &Row) -> String { - row[..Column(row.len())] - .iter() - .map(|cell| cell.c) - .collect::() -} - const TASK_DELIMITER: &str = "⏵ "; fn task_summary(task: &TaskState, exit_status: Option) -> (bool, String, String) { let escaped_full_label = task @@ -2416,49 +2689,12 @@ fn task_summary(task: &TaskState, exit_status: Option) -> (bool, Str (success, task_line, command_line) } -/// Appends a stringified task summary to the terminal, after its output. -/// -/// SAFETY: This function should only be called after terminal's PTY is no longer alive. -/// New text being added to the terminal here, uses "less public" APIs, -/// which are not maintaining the entire terminal state intact. -/// -/// -/// The library -/// -/// * does not increment inner grid cursor's _lines_ on `input` calls -/// (but displaying the lines correctly and incrementing cursor's columns) -/// -/// * ignores `\n` and \r` character input, requiring the `newline` call instead -/// -/// * does not alter grid state after `newline` call -/// so its `bottommost_line` is always the same additions, and -/// the cursor's `point` is not updated to the new line and column values -/// -/// * ??? there could be more consequences, and any further "proper" streaming from the PTY might bug and/or panic. -/// Still, subsequent `append_text_to_term` invocations are possible and display the contents correctly. -/// -/// Despite the quirks, this is the simplest approach to appending text to the terminal: its alternative, `grid_mut` manipulations, -/// do not properly set the scrolling state and display odd text after appending; also those manipulations are more tedious and error-prone. -/// The function achieves proper display and scrolling capabilities, at a cost of grid state not properly synchronized. -/// This is enough for printing moderately-sized texts like task summaries, but might break or perform poorly for larger texts. -unsafe fn append_text_to_term(term: &mut Term, text_lines: &[&str]) { - term.newline(); - term.grid_mut().cursor.point.column = Column(0); - for line in text_lines { - for c in line.chars() { - term.input(c); - } - term.newline(); - term.grid_mut().cursor.point.column = Column(0); - } -} - impl Drop for Terminal { fn drop(&mut self) { if let TerminalType::Pty { pty_tx, info } = std::mem::replace(&mut self.terminal_type, TerminalType::DisplayOnly) { - pty_tx.0.send(Msg::Shutdown).ok(); + pty_tx.shutdown(); info.terminate_child_process(); let timer = self.background_executor.timer(Duration::from_millis(100)); @@ -2545,27 +2781,12 @@ fn normalize_script_command_name(argument: &str) -> Option { .and_then(normalize_path_command_name) } -fn make_selection(range: &RangeInclusive) -> Selection { - let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left); - selection.update(*range.end(), AlacDirection::Right); - selection -} - -fn all_search_matches<'a, T>( - term: &'a Term, - regex: &'a mut RegexSearch, -) -> impl Iterator + 'a { - let start = AlacPoint::new(term.grid().topmost_line(), Column(0)); - let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column()); - RegexIter::new(start, end, AlacDirection::Right, term, regex) -} - -fn content_index_for_mouse(pos: Point, terminal_bounds: &TerminalBounds) -> usize { +fn content_index_for_mouse(pos: GpuiPoint, terminal_bounds: &TerminalBounds) -> usize { let col = (pos.x / terminal_bounds.cell_width()).round() as usize; - let clamped_col = min(col, terminal_bounds.columns() - 1); + let clamped_col = min(col, terminal_bounds.num_columns().saturating_sub(1)); let row = (pos.y / terminal_bounds.line_height()).round() as usize; - let clamped_row = min(row, terminal_bounds.screen_lines() - 1); - clamped_row * terminal_bounds.columns() + clamped_col + let clamped_row = min(row, terminal_bounds.num_lines().saturating_sub(1)); + clamped_row * terminal_bounds.num_columns() + clamped_col } /// Converts an 8 bit ANSI color to its GPUI equivalent. @@ -2664,18 +2885,15 @@ mod tests { use super::*; use crate::{ - IndexedCell, TerminalBounds, TerminalBuilder, TerminalContent, content_index_for_mouse, + Cell, Content, IndexedCell, TerminalBounds, TerminalBuilder, content_index_for_mouse, rgb_for_index, }; - use alacritty_terminal::{ - index::{Column, Line, Point as AlacPoint}, - term::cell::Cell, - }; use async_channel::Receiver; use collections::HashMap; + use gpui::MouseMoveEvent; use gpui::{ - Entity, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - Point, TestAppContext, bounds, point, size, + ClipboardItem, Entity, Modifiers, MouseButton, MouseDownEvent, MouseUpEvent, Pixels, + TestAppContext, bounds, point, size, }; use parking_lot::Mutex; use rand::{Rng, distr, rngs::StdRng}; @@ -2768,7 +2986,7 @@ mod tests { title_override: None, }, HashMap::default(), - CursorShape::default(), + SettingsCursorShape::default(), AlternateScroll::On, None, vec![], @@ -2795,14 +3013,13 @@ mod tests { let terminal = cx.new(|cx| { TerminalBuilder::new_display_only( - CursorShape::default(), + SettingsCursorShape::default(), AlternateScroll::On, None, 0, cx.background_executor(), PathStyle::local(), ) - .unwrap() .subscribe(cx) }); @@ -2814,7 +3031,7 @@ mod tests { terminal.update(cx, |terminal, _cx| { let term_lock = terminal.term.lock(); - terminal.last_content = Terminal::make_content(&term_lock, &terminal.last_content); + terminal.last_content = make_content(&term_lock, &terminal.last_content); drop(term_lock); let terminal_bounds = TerminalBounds::new( @@ -2831,7 +3048,7 @@ mod tests { fn ctrl_mouse_down_at( terminal: &mut Terminal, - position: Point, + position: GpuiPoint, cx: &mut Context, ) { let mouse_down = MouseDownEvent { @@ -2846,7 +3063,7 @@ mod tests { fn ctrl_mouse_move_to( terminal: &mut Terminal, - position: Point, + position: GpuiPoint, cx: &mut Context, ) { let terminal_bounds = terminal.last_content.terminal_bounds.bounds; @@ -2860,7 +3077,7 @@ mod tests { fn ctrl_mouse_up_at( terminal: &mut Terminal, - position: Point, + position: GpuiPoint, cx: &mut Context, ) { let mouse_up = MouseUpEvent { @@ -2928,7 +3145,7 @@ mod tests { None, task::Shell::System, HashMap::default(), - CursorShape::default(), + SettingsCursorShape::default(), AlternateScroll::On, None, vec![], @@ -2964,10 +3181,6 @@ mod tests { let first_event = event_rx.recv().await.expect("No wakeup event received"); - terminal.update(cx, |terminal, _| { - let success = terminal.try_keystroke(&Keystroke::parse("ctrl-c").unwrap(), false); - assert!(success, "Should have registered ctrl-c sequence"); - }); terminal.update(cx, |terminal, _| { let success = terminal.try_keystroke(&Keystroke::parse("ctrl-d").unwrap(), false); assert!(success, "Should have registered ctrl-d sequence"); @@ -3000,7 +3213,7 @@ mod tests { None, task::Shell::System, HashMap::default(), - CursorShape::default(), + SettingsCursorShape::default(), AlternateScroll::On, None, vec![], @@ -3066,7 +3279,7 @@ mod tests { title_override: None, }, HashMap::default(), - CursorShape::default(), + SettingsCursorShape::default(), AlternateScroll::On, None, Vec::new(), @@ -3144,7 +3357,7 @@ mod tests { cell_width: Pixels::from(cell_size), line_height: Pixels::from(cell_size), bounds: bounds( - Point::default(), + GpuiPoint::default(), size( Pixels::from(cell_size * (viewport_cells as f32)), Pixels::from(cell_size * (viewport_cells as f32)), @@ -3170,7 +3383,7 @@ mod tests { let content_index = content_index_for_mouse(mouse_pos, &content.terminal_bounds); - let mouse_cell = content.cells[content_index].c; + let mouse_cell = content.cells[content_index].character(); let real_cell = cells[row][col]; assert_eq!(mouse_cell, real_cell); @@ -3185,7 +3398,7 @@ mod tests { cell_width: Pixels::from(10.), line_height: Pixels::from(10.), bounds: bounds( - Point::default(), + GpuiPoint::default(), size(Pixels::from(100.), Pixels::from(100.)), ), }; @@ -3198,7 +3411,7 @@ mod tests { point(Pixels::from(-10.), Pixels::from(-10.)), &content.terminal_bounds, )] - .c, + .character(), cells[0][0] ); assert_eq!( @@ -3206,7 +3419,7 @@ mod tests { point(Pixels::from(1000.), Pixels::from(1000.)), &content.terminal_bounds, )] - .c, + .character(), cells[9][9] ); } @@ -3215,14 +3428,13 @@ mod tests { async fn test_set_size_coalesces_pixel_only_changes(cx: &mut TestAppContext) { let builder = cx.update(|cx| { TerminalBuilder::new_display_only( - CursorShape::Block, + SettingsCursorShape::Block, AlternateScroll::On, None, 0, cx.background_executor(), PathStyle::local(), ) - .unwrap() }); let mut terminal = builder.terminal; @@ -3230,7 +3442,7 @@ mod tests { cell_width: Pixels::from(10.), line_height: Pixels::from(10.), bounds: bounds( - Point::default(), + GpuiPoint::default(), size(Pixels::from(100.), Pixels::from(100.)), ), }; @@ -3271,25 +3483,21 @@ mod tests { cells } - fn convert_cells_to_content( - terminal_bounds: TerminalBounds, - cells: &[Vec], - ) -> TerminalContent { + fn convert_cells_to_content(terminal_bounds: TerminalBounds, cells: &[Vec]) -> Content { let mut ic = Vec::new(); for (index, row) in cells.iter().enumerate() { for (cell_index, cell_char) in row.iter().enumerate() { + let mut cell = Cell::default(); + cell.set_character(*cell_char); ic.push(IndexedCell { - point: AlacPoint::new(Line(index as i32), Column(cell_index)), - cell: Cell { - c: *cell_char, - ..Default::default() - }, + point: Point::new(index as i32, cell_index), + cell, }); } } - TerminalContent { + Content { cells: ic, terminal_bounds, ..Default::default() @@ -3300,14 +3508,13 @@ mod tests { async fn test_write_output_converts_lf_to_crlf(cx: &mut TestAppContext) { let terminal = cx.new(|cx| { TerminalBuilder::new_display_only( - CursorShape::default(), + SettingsCursorShape::default(), AlternateScroll::On, None, 0, cx.background_executor(), PathStyle::local(), ) - .unwrap() .subscribe(cx) }); @@ -3319,7 +3526,7 @@ mod tests { // Get the content by directly accessing the term let content = terminal.update(cx, |terminal, _cx| { let term = terminal.term.lock_unfair(); - Terminal::make_content(&term, &terminal.last_content) + make_content(&term, &terminal.last_content) }); // If LF is properly converted to CRLF, each line should start at column 0 @@ -3331,10 +3538,10 @@ mod tests { let mut line2_col0 = false; for cell in cells { - if cell.c == 'l' && cell.point.column.0 == 0 { - if cell.point.line.0 == 0 && !line1_col0 { + if cell.character() == 'l' && cell.point.column == 0 { + if cell.point.line == 0 && !line1_col0 { line1_col0 = true; - } else if cell.point.line.0 == 1 && !line2_col0 { + } else if cell.point.line == 1 && !line2_col0 { line2_col0 = true; } } @@ -3348,14 +3555,13 @@ mod tests { async fn test_write_output_preserves_existing_crlf(cx: &mut TestAppContext) { let terminal = cx.new(|cx| { TerminalBuilder::new_display_only( - CursorShape::default(), + SettingsCursorShape::default(), AlternateScroll::On, None, 0, cx.background_executor(), PathStyle::local(), ) - .unwrap() .subscribe(cx) }); @@ -3367,7 +3573,7 @@ mod tests { // Get the content by directly accessing the term let content = terminal.update(cx, |terminal, _cx| { let term = terminal.term.lock_unfair(); - Terminal::make_content(&term, &terminal.last_content) + make_content(&term, &terminal.last_content) }); let cells = &content.cells; @@ -3375,7 +3581,7 @@ mod tests { // Check that both lines start at column 0 let mut found_lines_at_column_0 = 0; for cell in cells { - if cell.c == 'l' && cell.point.column.0 == 0 { + if cell.character() == 'l' && cell.point.column == 0 { found_lines_at_column_0 += 1; } } @@ -3390,14 +3596,13 @@ mod tests { async fn test_write_output_preserves_bare_cr(cx: &mut TestAppContext) { let terminal = cx.new(|cx| { TerminalBuilder::new_display_only( - CursorShape::default(), + SettingsCursorShape::default(), AlternateScroll::On, None, 0, cx.background_executor(), PathStyle::local(), ) - .unwrap() .subscribe(cx) }); @@ -3409,7 +3614,7 @@ mod tests { // Get the content by directly accessing the term let content = terminal.update(cx, |terminal, _cx| { let term = terminal.term.lock_unfair(); - Terminal::make_content(&term, &terminal.last_content) + make_content(&term, &terminal.last_content) }); let cells = &content.cells; @@ -3417,8 +3622,8 @@ mod tests { // Check that we have "world" at the beginning of the line let mut text = String::new(); for cell in cells.iter().take(5) { - if cell.point.line.0 == 0 { - text.push(cell.c); + if cell.point.line == 0 { + text.push(cell.character()); } } @@ -3429,6 +3634,35 @@ mod tests { ); } + #[gpui::test] + async fn test_display_only_write_output_ignores_osc52(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = settings::SettingsStore::test(cx); + cx.set_global(settings_store); + cx.write_to_clipboard(ClipboardItem::new_string("original".to_string())); + }); + + let terminal = cx.new(|cx| { + TerminalBuilder::new_display_only( + SettingsCursorShape::default(), + AlternateScroll::On, + None, + 0, + cx.background_executor(), + PathStyle::local(), + ) + .subscribe(cx) + }); + + terminal.update(cx, |terminal, cx| { + terminal.write_output(b"\x1b]52;c;b3ZlcndyaXR0ZW4=\x07", cx); + }); + cx.run_until_parked(); + + let clipboard_text = cx.update(|cx| cx.read_from_clipboard().and_then(|item| item.text())); + assert_eq!(clipboard_text.as_deref(), Some("original")); + } + #[gpui::test] async fn test_hyperlink_ctrl_click_same_position(cx: &mut TestAppContext) { let terminal = init_ctrl_click_hyperlink_test(cx, b"Visit https://zed.dev/ for more\r\n"); @@ -3626,7 +3860,7 @@ mod tests { mod perf { use super::super::*; use gpui::{ - Entity, Point, ScrollDelta, ScrollWheelEvent, TestAppContext, VisualContext, + Entity, ScrollDelta, ScrollWheelEvent, TestAppContext, VisualContext, VisualTestContext, point, }; use util::default; @@ -3652,7 +3886,7 @@ mod tests { None, task::Shell::System, HashMap::default(), - CursorShape::default(), + SettingsCursorShape::default(), AlternateScroll::On, None, settings.path_hyperlink_regexes.clone(), @@ -3698,7 +3932,7 @@ mod tests { terminal.scroll_wheel( &ScrollWheelEvent { position, - delta: ScrollDelta::Lines(Point::new(0.0, lines as f32)), + delta: ScrollDelta::Lines(GpuiPoint::new(0.0, lines as f32)), ..default() }, 1.0, @@ -3733,7 +3967,7 @@ mod tests { px(line_height), px(8.0), Bounds { - origin: Point::default(), + origin: GpuiPoint::default(), size: Size { width: px(800.0), height: px(height), @@ -3759,7 +3993,7 @@ mod tests { px(20.0), px(cell_width), Bounds { - origin: Point::default(), + origin: GpuiPoint::default(), size: Size { width: px(width), height: px(400.0), diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 86666b06ee..8a31745d67 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -1,6 +1,3 @@ -use alacritty_terminal::vte::ansi::{ - CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle, -}; use collections::HashMap; use gpui::{FontFallbacks, FontFeatures, FontWeight, Pixels, px}; use schemars::JsonSchema; @@ -163,23 +160,3 @@ impl From for CursorShape { } } } - -impl From for AlacCursorShape { - fn from(value: CursorShape) -> Self { - match value { - CursorShape::Block => AlacCursorShape::Block, - CursorShape::Underline => AlacCursorShape::Underline, - CursorShape::Bar => AlacCursorShape::Beam, - CursorShape::Hollow => AlacCursorShape::HollowBlock, - } - } -} - -impl From for AlacCursorStyle { - fn from(value: CursorShape) -> Self { - AlacCursorStyle { - shape: value.into(), - blinking: false, - } - } -} diff --git a/crates/terminal_view/README.md b/crates/terminal_view/README.md index ca48f54542..2f037246d5 100644 --- a/crates/terminal_view/README.md +++ b/crates/terminal_view/README.md @@ -1,14 +1,28 @@ -Design notes: +# Terminal View + +## Design Notes This crate is split into two conceptual halves: -- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here. +- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with terminal emulator backends and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here. - Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file. ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context. The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors. -#Input +## Backend Boundary + +`terminal.rs` exposes backend-neutral domain types such as terminal content, cells, modes, points, ranges, scroll commands, vi motions, hyperlinks, and search matches. UI code should depend on those types instead of importing backend-specific terminal types directly. + +The current implementation is still Alacritty-backed, but the abstraction boundary keeps backend details concentrated in the terminal crate: + +- `terminal_view` renders `TerminalContent` and dispatches backend-neutral actions. +- Panels and tools use terminal-domain types for mode, cursor, range, hyperlink, and search behavior. +- Backend-specific conversions stay near the terminal event loop and render snapshot code. + +This keeps the user-facing terminal behavior unchanged while making future backend experiments reviewable as backend implementations instead of UI-wide refactors. + +## Input There are currently many distinct paths for getting keystrokes to the terminal: diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 99918d4784..cd0ed9241f 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -4,26 +4,18 @@ use gpui::{ Element, ElementId, Entity, FocusHandle, Font, FontFeatures, FontStyle, FontWeight, GlobalElementId, HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, IntoElement, LayoutId, Length, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, - Point, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, + Point as GpuiPoint, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UTF16Selection, UnderlineStyle, WeakEntity, WhiteSpace, Window, div, fill, point, px, relative, size, }; use itertools::Itertools; -use language::CursorShape; +use language::CursorShape as EditorCursorShape; use settings::Settings; use std::time::Instant; use terminal::{ - IndexedCell, Terminal, TerminalBounds, TerminalContent, - alacritty_terminal::{ - grid::Dimensions, - index::Point as AlacPoint, - term::{TermMode, cell::Flags}, - vte::ansi::{ - Color::{self as AnsiColor, Named}, - CursorShape as AlacCursorShape, NamedColor, - }, - }, - terminal_settings::TerminalSettings, + Cell, Color, Content, CursorShape, IndexedCell, Modes, NamedColor, Point, Range, Terminal, + TerminalBounds, 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_settings::ThemeSettings; @@ -33,7 +25,7 @@ use util::ResultExt; use workspace::Workspace; use std::mem; -use std::{fmt::Debug, ops::RangeInclusive, rc::Rc}; +use std::{fmt::Debug, rc::Rc}; use crate::{BlockContext, BlockProperties, ContentMode, TerminalMode, TerminalView}; @@ -42,12 +34,12 @@ pub struct LayoutState { hitbox: Hitbox, batched_text_runs: Vec, rects: Vec, - relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, + relative_highlighted_ranges: Vec<(Range, Hsla)>, cursor: Option, ime_cursor_bounds: Option>, background_color: Hsla, dimensions: TerminalBounds, - mode: TermMode, + mode: Modes, display_offset: usize, hyperlink_tooltip: Option, block_below_cursor_element: Option, @@ -55,7 +47,7 @@ pub struct LayoutState { content_mode: ContentMode, } -/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points. +/// Helper struct for converting terminal cursor points to displayed cursor points. #[derive(Copy, Clone)] struct DisplayCursor { line: i32, @@ -63,10 +55,10 @@ struct DisplayCursor { } impl DisplayCursor { - fn from(cursor_point: AlacPoint, display_offset: usize) -> Self { + fn from(cursor_point: Point, display_offset: usize) -> Self { Self { - line: cursor_point.line.0 + display_offset as i32, - col: cursor_point.column.0, + line: cursor_point.line + display_offset as i32, + col: cursor_point.column, } } @@ -79,10 +71,30 @@ impl DisplayCursor { } } +#[derive(Copy, Clone, Debug, Default)] +pub struct LayoutPoint { + line: i32, + column: i32, +} + +impl LayoutPoint { + fn new(line: i32, column: i32) -> Self { + Self { line, column } + } + + pub fn line(&self) -> i32 { + self.line + } + + pub fn column(&self) -> i32 { + self.column + } +} + /// A batched text run that combines multiple adjacent cells with the same style #[derive(Debug)] pub struct BatchedTextRun { - pub start_point: AlacPoint, + pub start_point: LayoutPoint, pub text: String, pub cell_count: usize, pub style: TextRun, @@ -91,7 +103,7 @@ pub struct BatchedTextRun { impl BatchedTextRun { fn new_from_char( - start_point: AlacPoint, + start_point: LayoutPoint, c: char, style: TextRun, font_size: AbsoluteLength, @@ -135,17 +147,17 @@ impl BatchedTextRun { pub fn paint( &self, - origin: Point, + origin: GpuiPoint, dimensions: &TerminalBounds, window: &mut Window, cx: &mut App, ) { - let pos = Point::new( + let pos = GpuiPoint::new( origin.x + self.start_point.column as f32 * dimensions.cell_width, origin.y + self.start_point.line as f32 * dimensions.line_height, ); - let _ = window + window .text_system() .shape_line( self.text.clone().into(), @@ -160,19 +172,20 @@ impl BatchedTextRun { None, window, cx, - ); + ) + .log_err(); } } #[derive(Clone, Debug, Default)] pub struct LayoutRect { - point: AlacPoint, + point: LayoutPoint, num_of_cells: usize, color: Hsla, } impl LayoutRect { - fn new(point: AlacPoint, num_of_cells: usize, color: Hsla) -> LayoutRect { + fn new(point: LayoutPoint, num_of_cells: usize, color: Hsla) -> LayoutRect { LayoutRect { point, num_of_cells, @@ -180,12 +193,17 @@ impl LayoutRect { } } - pub fn paint(&self, origin: Point, dimensions: &TerminalBounds, window: &mut Window) { + pub fn paint( + &self, + origin: GpuiPoint, + dimensions: &TerminalBounds, + window: &mut Window, + ) { let position = { - let alac_point = self.point; + let layout_point = self.point; point( - (origin.x + alac_point.column as f32 * dimensions.cell_width).floor(), - origin.y + alac_point.line as f32 * dimensions.line_height, + (origin.x + layout_point.column as f32 * dimensions.cell_width).floor(), + origin.y + layout_point.line as f32 * dimensions.line_height, ) }; let size = point( @@ -247,6 +265,31 @@ impl BackgroundRegion { } } +pub trait TerminalLayoutCell { + fn point(&self) -> Point; + fn cell(&self) -> &Cell; +} + +impl TerminalLayoutCell for IndexedCell { + fn point(&self) -> Point { + self.point + } + + fn cell(&self) -> &Cell { + &self.cell + } +} + +impl TerminalLayoutCell for &IndexedCell { + fn point(&self) -> Point { + self.point + } + + fn cell(&self) -> &Cell { + &self.cell + } +} + /// Merge background regions to minimize the number of rectangles fn merge_background_regions(regions: Vec) -> Vec { if regions.is_empty() { @@ -326,13 +369,11 @@ impl TerminalElement { .track_focus(&focus) } - //Vec> -> Clip out the parts of the ranges - - pub fn layout_grid( - grid: impl Iterator, + pub fn layout_grid( + grid: impl Iterator, start_line_offset: i32, text_style: &TextStyle, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, + hyperlink: Option<(HighlightStyle, &Range)>, minimum_contrast: f32, cx: &App, ) -> (Vec, Vec) { @@ -352,9 +393,9 @@ impl TerminalElement { let mut current_batch: Option = None; // First pass: collect all cells and their backgrounds - let linegroups = grid.into_iter().chunk_by(|i| i.point.line); + let linegroups = grid.into_iter().chunk_by(|cell| cell.point().line); for (line_index, (_, line)) in linegroups.into_iter().enumerate() { - let alac_line = start_line_offset + line_index as i32; + let display_line = start_line_offset + line_index as i32; // Flush any existing batch at line boundaries if let Some(batch) = current_batch.take() { @@ -364,36 +405,38 @@ impl TerminalElement { let mut previous_cell_had_extras = false; for cell in line { - let mut fg = cell.fg; - let mut bg = cell.bg; - if cell.flags.contains(Flags::INVERSE) { + let point = cell.point(); + let cell = cell.cell(); + let mut fg = cell.foreground(); + let mut bg = cell.background(); + if cell.is_inverse() { mem::swap(&mut fg, &mut bg); } // Collect background regions (skip default background) - if !matches!(bg, Named(NamedColor::Background)) { + if !is_default_background_color(bg) { let color = convert_color(&bg, theme); - let col = cell.point.column.0 as i32; + let col = point.column as i32; // Try to extend the last region if it's on the same line with the same color if let Some(last_region) = background_regions.last_mut() && last_region.color == color - && last_region.start_line == alac_line - && last_region.end_line == alac_line + && last_region.start_line == display_line + && last_region.end_line == display_line && last_region.end_col + 1 == col { last_region.end_col = col; } else { - background_regions.push(BackgroundRegion::new(alac_line, col, color)); + background_regions.push(BackgroundRegion::new(display_line, col, color)); } } // Skip wide character spacers - they're just placeholders for the second cell of wide characters - if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + if cell.is_wide_char_spacer() { continue; } // Skip spaces that follow cells with extras (emoji variation sequences) - if cell.c == ' ' && previous_cell_had_extras { + if cell.character() == ' ' && previous_cell_had_extras { previous_cell_had_extras = false; continue; } @@ -403,10 +446,11 @@ impl TerminalElement { //Layout current cell text { - if !is_blank(&cell) { + if !is_blank(cell) { cell_count += 1; let cell_style = TerminalElement::cell_style( - &cell, + point, + cell, fg, bg, theme, @@ -415,7 +459,7 @@ impl TerminalElement { minimum_contrast, ); - let cell_point = AlacPoint::new(alac_line, cell.point.column.0 as i32); + let cell_point = LayoutPoint::new(display_line, point.column as i32); let zero_width_chars = cell.zerowidth(); // Try to batch with existing run @@ -425,7 +469,7 @@ impl TerminalElement { && batch.start_point.column + batch.cell_count as i32 == cell_point.column { - batch.append_char(cell.c); + batch.append_char(cell.character()); if let Some(chars) = zero_width_chars { batch.append_zero_width_chars(chars); } @@ -435,7 +479,7 @@ impl TerminalElement { batched_runs.push(old_batch); let mut new_batch = BatchedTextRun::new_from_char( cell_point, - cell.c, + cell.character(), cell_style, text_style.font_size, ); @@ -448,7 +492,7 @@ impl TerminalElement { // Start new batch let mut new_batch = BatchedTextRun::new_from_char( cell_point, - cell.c, + cell.character(), cell_style, text_style.font_size, ); @@ -477,7 +521,7 @@ impl TerminalElement { for region in merged_regions { for line in region.start_line..=region.end_line { rects.push(LayoutRect::new( - AlacPoint::new(line, region.start_col), + LayoutPoint::new(line, region.start_col), (region.end_col - region.start_col + 1) as usize, region.color, )); @@ -501,8 +545,11 @@ impl TerminalElement { } /// Computes the cursor position based on the cursor point and terminal dimensions. - fn cursor_position(cursor_point: DisplayCursor, size: TerminalBounds) -> Option> { - if cursor_point.line() < size.total_lines() as i32 { + fn cursor_position( + cursor_point: DisplayCursor, + size: TerminalBounds, + ) -> Option> { + if cursor_point.line() < size.num_lines() as i32 { // When on pixel boundaries round the origin down Some(point( (cursor_point.col() as f32 * size.cell_width()).floor(), @@ -544,68 +591,60 @@ impl TerminalElement { /// 6x6x6 cube at 16..=231 and the 24-step grayscale ramp at 232..=255). /// Indices 0..=15 still go through contrast adjustment since those map to /// theme-defined ANSI colors that can clash with the theme background. - fn is_app_chosen_exact_color(fg: &terminal::alacritty_terminal::vte::ansi::Color) -> bool { - matches!( - fg, - terminal::alacritty_terminal::vte::ansi::Color::Spec(_) - | terminal::alacritty_terminal::vte::ansi::Color::Indexed(16..=255) - ) + fn is_app_chosen_exact_color(fg: &Color) -> bool { + terminal_is_app_chosen_exact_color(*fg) } - /// Converts the Alacritty cell styles to GPUI text styles and background color. + /// Converts terminal cell styles to GPUI text styles and background color. fn cell_style( - indexed: &IndexedCell, - fg: terminal::alacritty_terminal::vte::ansi::Color, - bg: terminal::alacritty_terminal::vte::ansi::Color, + point: Point, + cell: &Cell, + fg: Color, + bg: Color, colors: &Theme, text_style: &TextStyle, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, + hyperlink: Option<(HighlightStyle, &Range)>, minimum_contrast: f32, ) -> TextRun { - let flags = indexed.cell.flags; let skip_contrast = Self::is_app_chosen_exact_color(&fg); let mut fg = convert_color(&fg, colors); let bg = convert_color(&bg, colors); - if !skip_contrast && !Self::is_decorative_character(indexed.c) { + if !skip_contrast && !Self::is_decorative_character(cell.character()) { fg = ensure_minimum_contrast(fg, bg, minimum_contrast); } - // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty - // uses 0.75. We're using 0.7 because it's pretty well in the middle of that. - if flags.intersects(Flags::DIM) { + // Use a dim multiplier that stays close to the existing Alacritty look. + if cell.is_dim() { fg.a *= 0.7; } - let underline = (flags.intersects(Flags::ALL_UNDERLINES) - || indexed.cell.hyperlink().is_some()) - .then(|| UnderlineStyle { - color: Some(fg), - thickness: Pixels::from(1.0), - wavy: flags.contains(Flags::UNDERCURL), - }); - - let strikethrough = flags - .intersects(Flags::STRIKEOUT) - .then(|| StrikethroughStyle { + let underline = + (cell.has_underline() || cell.hyperlink().is_some()).then(|| UnderlineStyle { color: Some(fg), thickness: Pixels::from(1.0), + wavy: cell.has_undercurl(), }); - let weight = if flags.intersects(Flags::BOLD) { + let strikethrough = cell.has_strikeout().then(|| StrikethroughStyle { + color: Some(fg), + thickness: Pixels::from(1.0), + }); + + let weight = if cell.is_bold() { FontWeight::BOLD } else { text_style.font_weight }; - let style = if flags.intersects(Flags::ITALIC) { + let style = if cell.is_italic() { FontStyle::Italic } else { FontStyle::Normal }; let mut result = TextRun { - len: indexed.c.len_utf8(), + len: cell.character().len_utf8(), color: fg, background_color: None, font: Font { @@ -618,7 +657,7 @@ impl TerminalElement { }; if let Some((style, range)) = hyperlink - && range.contains(&indexed.point) + && range.contains(point) { if let Some(underline) = style.underline { result.underline = Some(underline); @@ -654,7 +693,7 @@ impl TerminalElement { fn register_mouse_listeners( &mut self, - mode: TermMode, + mode: Modes, hitbox: &Hitbox, content_mode: &ContentMode, window: &mut Window, @@ -760,7 +799,7 @@ impl TerminalElement { // Mouse mode handlers: // All mouse modes need the extra click handlers - if mode.intersects(TermMode::MOUSE_MODE) { + if mode.intersects(Modes::MOUSE_MODE) { self.interactivity.on_mouse_down( MouseButton::Right, TerminalElement::generic_button_handler( @@ -1077,7 +1116,7 @@ impl Element for TerminalElement { element }); - let TerminalContent { + let Content { cells, mode, display_offset, @@ -1096,7 +1135,7 @@ impl Element for TerminalElement { } if let Some(selection) = selection { relative_highlighted_ranges - .push((selection.start..=selection.end, player_color.selection)); + .push((selection.point_range(), player_color.selection)); } // then have that representation be converted to the appropriate highlight data structure @@ -1127,7 +1166,7 @@ impl Element for TerminalElement { // Fast path: terminal fully visible, no clipping needed. // Avoid grouping/allocation overhead by streaming cells directly. TerminalElement::layout_grid( - cells.iter().cloned(), + cells.iter(), 0, &text_style, last_hovered_word @@ -1158,8 +1197,7 @@ impl Element for TerminalElement { .into_iter() .skip(rows_above_viewport) .take(visible_row_count) - .flat_map(|(_, line_cells)| line_cells) - .cloned(), + .flat_map(|(_, line_cells)| line_cells), rows_above_viewport as i32, &text_style, last_hovered_word @@ -1204,20 +1242,20 @@ impl Element for TerminalElement { size: size(cursor_width.ceil(), dimensions.line_height), }); - let cursor = if let AlacCursorShape::Hidden = cursor.shape { + let cursor = if let CursorShape::Hidden = cursor.shape { None } else { let focused = self.focused; ime_cursor_bounds.map(move |bounds| { let (shape, text) = match cursor.shape { - AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), - AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), - AlacCursorShape::Underline if !focused => (CursorShape::Hollow, None), - AlacCursorShape::Underline => (CursorShape::Underline, None), - AlacCursorShape::Beam if !focused => (CursorShape::Hollow, None), - AlacCursorShape::Beam => (CursorShape::Bar, None), - AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), - AlacCursorShape::Hidden => unreachable!(), + CursorShape::Block if !focused => (EditorCursorShape::Hollow, None), + CursorShape::Block => (EditorCursorShape::Block, Some(cursor_text)), + CursorShape::Underline if !focused => (EditorCursorShape::Hollow, None), + CursorShape::Underline => (EditorCursorShape::Underline, None), + CursorShape::Bar if !focused => (EditorCursorShape::Hollow, None), + CursorShape::Bar => (EditorCursorShape::Bar, None), + CursorShape::HollowBlock => (EditorCursorShape::Hollow, None), + CursorShape::Hidden => unreachable!(), }; CursorLayout::new( @@ -1234,7 +1272,7 @@ impl Element for TerminalElement { let block_below_cursor_element = if let Some(block) = &self.block_below_cursor { let terminal = self.terminal.read(cx); if terminal.last_content.display_offset == 0 { - let target_line = terminal.last_content.cursor.point.line.0 + 1; + let target_line = terminal.last_content.cursor.point.line + 1; let render = &block.render; let mut block_cx = BlockContext { window, @@ -1249,7 +1287,7 @@ impl Element for TerminalElement { block.height as f32 * dimensions.line_height(), ), ); - let origin = Point::new(bounds.origin.x, dimensions.bounds.origin.y) + let origin = GpuiPoint::new(bounds.origin.x, dimensions.bounds.origin.y) + point(px(0.), target_line as f32 * dimensions.line_height()) - point(px(0.), scroll_top); window.with_rem_size(rem_size, |window| { @@ -1298,7 +1336,7 @@ impl Element for TerminalElement { let scroll_top = self.terminal_view.read(cx).scroll_top; window.paint_quad(fill(bounds, layout.background_color)); - let origin = layout.dimensions.bounds.origin - Point::new(px(0.), scroll_top); + let origin = layout.dimensions.bounds.origin - GpuiPoint::new(px(0.), scroll_top); let scale_factor = window.scale_factor(); let snap_px = |value: Pixels| { Pixels::from((f32::from(value) * scale_factor).floor() / scale_factor) @@ -1490,7 +1528,7 @@ impl InputHandler for TerminalInputHandler { .read(cx) .last_content .mode - .contains(TermMode::ALT_SCREEN) + .contains(Modes::ALT_SCREEN) { None } else { @@ -1581,7 +1619,7 @@ impl InputHandler for TerminalInputHandler { fn character_index_for_point( &mut self, - _point: Point, + _point: GpuiPoint, _window: &mut Window, _cx: &mut App, ) -> Option { @@ -1589,12 +1627,12 @@ impl InputHandler for TerminalInputHandler { } } -pub fn is_blank(cell: &IndexedCell) -> bool { - if cell.c != ' ' { +pub fn is_blank(cell: &Cell) -> bool { + if cell.character() != ' ' { return false; } - if cell.bg != AnsiColor::Named(NamedColor::Background) { + if !is_default_background_color(cell.background()) { return false; } @@ -1602,10 +1640,7 @@ pub fn is_blank(cell: &IndexedCell) -> bool { return false; } - if cell - .flags - .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT) - { + if cell.has_visible_style_modifier() { return false; } @@ -1613,9 +1648,9 @@ pub fn is_blank(cell: &IndexedCell) -> bool { } fn to_highlighted_range_lines( - range: &RangeInclusive, + range: &Range, layout: &LayoutState, - origin: Point, + origin: GpuiPoint, ) -> Option<(Pixels, Vec)> { // Step 1. Normalize the points to be viewport relative. // When display_offset = 1, here's how the grid is arranged: @@ -1635,24 +1670,20 @@ fn to_highlighted_range_lines( // of the grid data we should be looking at. But for the rendering step, we don't // want negatives. We want things relative to the 'viewport' (the area of the grid // which is currently shown according to the display offset) - let unclamped_start = AlacPoint::new( - range.start().line + layout.display_offset, - range.start().column, - ); - let unclamped_end = - AlacPoint::new(range.end().line + layout.display_offset, range.end().column); + let display_offset = i32::try_from(layout.display_offset).unwrap_or(i32::MAX); + let unclamped_start_line = range.start().line.saturating_add(display_offset); + let unclamped_start_column = range.start().column; + let unclamped_end_line = range.end().line.saturating_add(display_offset); + let unclamped_end_column = range.end().column; // Step 2. Clamp range to viewport, and return None if it doesn't overlap - if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 { + if unclamped_end_line < 0 || unclamped_start_line > layout.dimensions.num_lines() as i32 { return None; } - let clamped_start_line = unclamped_start.line.0.max(0) as usize; + let clamped_start_line = unclamped_start_line.max(0) as usize; - let clamped_end_line = unclamped_end - .line - .0 - .min(layout.dimensions.num_lines() as i32) as usize; + let clamped_end_line = unclamped_end_line.min(layout.dimensions.num_lines() as i32) as usize; // Convert the start of the range to pixels let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height; @@ -1662,14 +1693,13 @@ fn to_highlighted_range_lines( let mut highlighted_range_lines = Vec::new(); for line in clamped_start_line..=clamped_end_line { let mut line_start = 0; - let mut line_end = layout.dimensions.columns(); + let mut line_end = layout.dimensions.num_columns(); - if line == clamped_start_line && unclamped_start.line.0 >= 0 { - line_start = unclamped_start.column.0; + if line == clamped_start_line && unclamped_start_line >= 0 { + line_start = unclamped_start_column; } - if line == clamped_end_line && unclamped_end.line.0 <= layout.dimensions.num_lines() as i32 - { - line_end = unclamped_end.column.0 + 1; // +1 for inclusive + if line == clamped_end_line && unclamped_end_line <= layout.dimensions.num_lines() as i32 { + line_end = unclamped_end_column + 1; // +1 for inclusive } highlighted_range_lines.push(HighlightedRangeLine { @@ -1682,11 +1712,11 @@ fn to_highlighted_range_lines( } /// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent. -pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla { +pub fn convert_color(fg: &Color, theme: &Theme) -> Hsla { let colors = theme.colors(); match fg { // Named and theme defined colors - terminal::alacritty_terminal::vte::ansi::Color::Named(n) => match n { + Color::Named(color) => match color { NamedColor::Black => colors.terminal_ansi_black, NamedColor::Red => colors.terminal_ansi_red, NamedColor::Green => colors.terminal_ansi_green, @@ -1718,13 +1748,9 @@ pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: NamedColor::DimForeground => colors.terminal_dim_foreground, }, // 'True' colors - terminal::alacritty_terminal::vte::ansi::Color::Spec(rgb) => { - terminal::rgba_color(rgb.r, rgb.g, rgb.b) - } + Color::Spec(rgb) => terminal::rgba_color(rgb.r, rgb.g, rgb.b), // 8 bit, indexed colors - terminal::alacritty_terminal::vte::ansi::Color::Indexed(i) => { - terminal::get_color_at_index(*i as usize, theme) - } + Color::Indexed(i) => terminal::get_color_at_index(*i as usize, theme), } } @@ -1833,7 +1859,7 @@ mod tests { #[test] fn test_is_app_chosen_exact_color() { - use terminal::alacritty_terminal::vte::ansi::{Color, NamedColor, Rgb}; + use terminal::{Color, NamedColor, Rgb}; // Indices 0..=15 are theme-overridable ANSI colors; contrast adjustment must still apply. assert!(!TerminalElement::is_app_chosen_exact_color( @@ -2054,7 +2080,7 @@ mod tests { }; let font_size = AbsoluteLength::Pixels(px(12.0)); - let batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1, font_size); + let batch = BatchedTextRun::new_from_char(LayoutPoint::new(0, 0), 'a', style1, font_size); // Should be able to append same style assert!(batch.can_append(&style2)); @@ -2073,7 +2099,8 @@ mod tests { }; let font_size = AbsoluteLength::Pixels(px(12.0)); - let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style, font_size); + let mut batch = + BatchedTextRun::new_from_char(LayoutPoint::new(0, 0), 'a', style, font_size); assert_eq!(batch.text, "a"); assert_eq!(batch.cell_count, 1); @@ -2102,7 +2129,8 @@ mod tests { }; let font_size = AbsoluteLength::Pixels(px(12.0)); - let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'x', style, font_size); + let mut batch = + BatchedTextRun::new_from_char(LayoutPoint::new(0, 0), 'x', style, font_size); assert_eq!(batch.text, "x"); assert_eq!(batch.cell_count, 1); @@ -2132,7 +2160,8 @@ mod tests { }; let font_size = AbsoluteLength::Pixels(px(12.0)); - let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'x', style, font_size); + let mut batch = + BatchedTextRun::new_from_char(LayoutPoint::new(0, 0), 'x', style, font_size); let combining = '\u{0301}'; batch.append_zero_width_chars(&[combining]); @@ -2244,16 +2273,14 @@ mod tests { // This works for both Scrollable and Inline modes because we filter // by enumerated line group index, not by cell.point.line values. use itertools::Itertools; - use terminal::IndexedCell; - use terminal::alacritty_terminal::index::{Column, Line, Point as AlacPoint}; - use terminal::alacritty_terminal::term::cell::Cell; + use terminal::{Cell, IndexedCell, Point}; // Create mock cells for lines 0-23 (typical terminal with 24 visible lines) let mut cells = Vec::new(); for line in 0..24i32 { for col in 0..3i32 { cells.push(IndexedCell { - point: AlacPoint::new(Line(line), Column(col as usize)), + point: Point::new(line, col as usize), cell: Cell::default(), }); } @@ -2280,14 +2307,14 @@ mod tests { // First filtered cell should be line 5 assert_eq!( filtered.first().unwrap().point.line, - Line(5), + 5, "First cell should be on line 5" ); // Last filtered cell should be line 15 assert_eq!( filtered.last().unwrap().point.line, - Line(15), + 15, "Last cell should be on line 15" ); } @@ -2298,9 +2325,7 @@ mod tests { // for scrollback history. The screen-position filtering approach works because // we filter by enumerated line group index, not by cell.point.line values. use itertools::Itertools; - use terminal::IndexedCell; - use terminal::alacritty_terminal::index::{Column, Line, Point as AlacPoint}; - use terminal::alacritty_terminal::term::cell::Cell; + use terminal::{Cell, IndexedCell, Point}; // Simulate cells from a scrolled terminal with scrollback // These have negative line numbers representing scrollback history @@ -2308,7 +2333,7 @@ mod tests { for line in -588i32..=-578i32 { for col in 0..80i32 { scrollback_cells.push(IndexedCell { - point: AlacPoint::new(Line(line), Column(col as usize)), + point: Point::new(line, col as usize), cell: Cell::default(), }); } @@ -2334,14 +2359,14 @@ mod tests { // First filtered cell should be line -585 (skipped 3 lines from -588) assert_eq!( filtered.first().unwrap().point.line, - Line(-585), + -585, "First cell should be on line -585" ); // Last filtered cell should be line -581 (5 lines: -585, -584, -583, -582, -581) assert_eq!( filtered.last().unwrap().point.line, - Line(-581), + -581, "Last cell should be on line -581" ); } @@ -2350,14 +2375,12 @@ mod tests { fn test_screen_position_filtering_skip_all() { // Test what happens when we skip more rows than exist use itertools::Itertools; - use terminal::IndexedCell; - use terminal::alacritty_terminal::index::{Column, Line, Point as AlacPoint}; - use terminal::alacritty_terminal::term::cell::Cell; + use terminal::{Cell, IndexedCell, Point}; let mut cells = Vec::new(); for line in 0..10i32 { cells.push(IndexedCell { - point: AlacPoint::new(Line(line), Column(0)), + point: Point::new(line, 0), cell: Cell::default(), }); } @@ -2422,15 +2445,14 @@ mod tests { // not by cell.point.line values. This makes the filtering agnostic to the // actual line numbers in the cells. use itertools::Itertools; - use terminal::IndexedCell; - use terminal::alacritty_terminal::index::{Column, Line, Point as AlacPoint}; - use terminal::alacritty_terminal::term::cell::Cell; + use terminal::Point; + use terminal::{Cell, IndexedCell}; // Test with positive line numbers (Inline mode style) let positive_cells: Vec<_> = (0..10i32) .flat_map(|line| { (0..3i32).map(move |col| IndexedCell { - point: AlacPoint::new(Line(line), Column(col as usize)), + point: Point::new(line, col as usize), cell: Cell::default(), }) }) @@ -2440,7 +2462,7 @@ mod tests { let negative_cells: Vec<_> = (-10i32..0i32) .flat_map(|line| { (0..3i32).map(move |col| IndexedCell { - point: AlacPoint::new(Line(line), Column(col as usize)), + point: Point::new(line, col as usize), cell: Cell::default(), }) }) @@ -2474,11 +2496,11 @@ mod tests { assert_eq!(negative_filtered.len(), 12); // Positive: lines 3, 4, 5, 6 - assert_eq!(positive_filtered.first().unwrap().point.line, Line(3)); - assert_eq!(positive_filtered.last().unwrap().point.line, Line(6)); + assert_eq!(positive_filtered.first().unwrap().point.line, 3); + assert_eq!(positive_filtered.last().unwrap().point.line, 6); // Negative: lines -7, -6, -5, -4 - assert_eq!(negative_filtered.first().unwrap().point.line, Line(-7)); - assert_eq!(negative_filtered.last().unwrap().point.line, Line(-4)); + assert_eq!(negative_filtered.first().unwrap().point.line, -7); + assert_eq!(negative_filtered.last().unwrap().point.line, -4); } } diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs index e6e6e94bff..c90a2ce3d9 100644 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ b/crates/terminal_view/src/terminal_path_like_target.rs @@ -210,8 +210,7 @@ mod tests { use serde_json::json; use std::path::{Path, PathBuf}; use terminal::{ - HoveredWord, TerminalBuilder, - alacritty_terminal::index::Point as AlacPoint, + HoveredWord, Point, Range, TerminalBuilder, terminal_settings::{AlternateScroll, CursorShape}, }; use util::path; @@ -258,7 +257,6 @@ mod tests { cx.background_executor(), PathStyle::local(), ) - .expect("Failed to create display-only terminal") .subscribe(cx) }); @@ -329,7 +327,7 @@ mod tests { let (hover_target, open_target) = test_path_like( HoveredWord { word: maybe_path.to_string(), - word_match: AlacPoint::default()..=AlacPoint::default(), + word_match: Range::new(Point::new(0, 0), Point::new(0, 0)), id: 0, }, PathLikeTarget { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 905db4513d..63943a2022 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -11,8 +11,8 @@ use editor::{ use gpui::{ Action, AnyElement, App, ClipboardEntry, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Font, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, - Pixels, Point, Render, ScrollWheelEvent, Styled, Subscription, Task, TaskExt, WeakEntity, - actions, anchored, deferred, div, + Pixels, Point as GpuiPoint, Render, ScrollWheelEvent, Styled, Subscription, Task, TaskExt, + WeakEntity, actions, anchored, deferred, div, }; use itertools::Itertools; use menu; @@ -26,7 +26,7 @@ use settings::{ use std::{ any::Any, cmp, - ops::{Range, RangeInclusive}, + ops::Range as StdRange, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -34,13 +34,9 @@ use std::{ }; use task::TaskId; use terminal::{ - Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Paste, PasteText, ScrollLineDown, - ScrollLineUp, ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, - TaskState, TaskStatus, Terminal, TerminalBounds, ToggleViMode, - alacritty_terminal::{ - index::Point as AlacPoint, - term::{TermMode, point_to_viewport, search::RegexSearch}, - }, + Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Modes, Paste, PasteText, Point, Range, + ScrollLineDown, ScrollLineUp, ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, + Search, ShowCharacterPalette, TaskState, TaskStatus, Terminal, TerminalBounds, ToggleViMode, terminal_settings::{CursorShape, TerminalSettings}, }; use terminal_element::TerminalElement; @@ -70,6 +66,16 @@ struct ImeState { marked_text: String, } +fn viewport_line_for_point(point: Point, display_offset: usize) -> Option { + let display_offset = i32::try_from(display_offset).unwrap_or(i32::MAX); + let line = point.line.saturating_add(display_offset); + if line < 0 { + None + } else { + usize::try_from(line).ok() + } +} + const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); /// Event to transmit the scroll from the element to the view @@ -129,7 +135,7 @@ pub struct TerminalView { focus_handle: FocusHandle, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, - context_menu: Option<(Entity, Point, Subscription)>, + context_menu: Option<(Entity, GpuiPoint, Subscription)>, cursor_shape: CursorShape, blink_manager: Entity, mode: TerminalMode, @@ -353,7 +359,7 @@ impl TerminalView { } /// Gets the current marked range (UTF-16). - pub(crate) fn marked_text_range(&self) -> Option> { + pub(crate) fn marked_text_range(&self) -> Option> { self.ime_state .as_ref() .map(|state| 0..state.marked_text.encode_utf16().count()) @@ -490,7 +496,7 @@ impl TerminalView { pub fn deploy_context_menu( &mut self, - position: Point, + position: GpuiPoint, has_selection: bool, window: &mut Window, cx: &mut Context, @@ -596,7 +602,7 @@ impl TerminalView { .read(cx) .last_content .mode - .contains(TermMode::ALT_SCREEN) + .contains(Modes::ALT_SCREEN) { self.terminal.update(cx, |term, cx| { term.try_keystroke( @@ -639,13 +645,13 @@ impl TerminalView { let line_height = terminal.last_content().terminal_bounds.line_height; let viewport_lines = terminal.viewport_lines(); - let cursor = point_to_viewport( - terminal.last_content.display_offset, + let cursor_line = viewport_line_for_point( terminal.last_content.cursor.point, + terminal.last_content.display_offset, ) .unwrap_or_default(); let max_scroll_top_in_lines = - (block.height as usize).saturating_sub(viewport_lines.saturating_sub(cursor.line + 1)); + (block.height as usize).saturating_sub(viewport_lines.saturating_sub(cursor_line + 1)); max_scroll_top_in_lines as f32 * line_height } @@ -678,7 +684,7 @@ impl TerminalView { .read(cx) .last_content .mode - .contains(TermMode::ALT_SCREEN) + .contains(Modes::ALT_SCREEN) } fn scroll_line_up(&mut self, _: &ScrollLineUp, _: &mut Window, cx: &mut Context) { @@ -808,7 +814,7 @@ impl TerminalView { .read(cx) .last_content .mode - .contains(TermMode::ALT_SCREEN) + .contains(Modes::ALT_SCREEN) { return true; } @@ -956,55 +962,55 @@ impl TerminalView { let mode = self.terminal.read(cx).last_content.mode; dispatch_context.set( "screen", - if mode.contains(TermMode::ALT_SCREEN) { + if mode.contains(Modes::ALT_SCREEN) { "alt" } else { "normal" }, ); - if mode.contains(TermMode::APP_CURSOR) { + if mode.contains(Modes::APP_CURSOR) { dispatch_context.add("DECCKM"); } - if mode.contains(TermMode::APP_KEYPAD) { + if mode.contains(Modes::APP_KEYPAD) { dispatch_context.add("DECPAM"); } else { dispatch_context.add("DECPNM"); } - if mode.contains(TermMode::SHOW_CURSOR) { + if mode.contains(Modes::SHOW_CURSOR) { dispatch_context.add("DECTCEM"); } - if mode.contains(TermMode::LINE_WRAP) { + if mode.contains(Modes::LINE_WRAP) { dispatch_context.add("DECAWM"); } - if mode.contains(TermMode::ORIGIN) { + if mode.contains(Modes::ORIGIN) { dispatch_context.add("DECOM"); } - if mode.contains(TermMode::INSERT) { + if mode.contains(Modes::INSERT) { dispatch_context.add("IRM"); } //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html - if mode.contains(TermMode::LINE_FEED_NEW_LINE) { + if mode.contains(Modes::LINE_FEED_NEW_LINE) { dispatch_context.add("LNM"); } - if mode.contains(TermMode::FOCUS_IN_OUT) { + if mode.contains(Modes::FOCUS_IN_OUT) { dispatch_context.add("report_focus"); } - if mode.contains(TermMode::ALTERNATE_SCROLL) { + if mode.contains(Modes::ALTERNATE_SCROLL) { dispatch_context.add("alternate_scroll"); } - if mode.contains(TermMode::BRACKETED_PASTE) { + if mode.contains(Modes::BRACKETED_PASTE) { dispatch_context.add("bracketed_paste"); } - if mode.intersects(TermMode::MOUSE_MODE) { + if mode.intersects(Modes::MOUSE_MODE) { dispatch_context.add("any_mouse_reporting"); } { - let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { + let mouse_reporting = if mode.contains(Modes::MOUSE_REPORT_CLICK) { "click" - } else if mode.contains(TermMode::MOUSE_DRAG) { + } else if mode.contains(Modes::MOUSE_DRAG) { "drag" - } else if mode.contains(TermMode::MOUSE_MOTION) { + } else if mode.contains(Modes::MOUSE_MOTION) { "motion" } else { "off" @@ -1012,9 +1018,9 @@ impl TerminalView { dispatch_context.set("mouse_reporting", mouse_reporting); } { - let format = if mode.contains(TermMode::SGR_MOUSE) { + let format = if mode.contains(Modes::SGR_MOUSE) { "sgr" - } else if mode.contains(TermMode::UTF8_MOUSE) { + } else if mode.contains(Modes::UTF8_MOUSE) { "utf8" } else { "normal" @@ -1193,15 +1199,15 @@ fn subscribe_for_terminal_events( vec![terminal_subscription, terminal_events_subscription] } -fn regex_search_for_query(query: &SearchQuery) -> Option { +fn regex_search_for_query(query: &SearchQuery) -> Option { let str = query.as_str(); if query.is_regex() { if str == "." { return None; } - RegexSearch::new(str).ok() + Search::new(str) } else { - RegexSearch::new(®ex::escape(str)).ok() + Search::new(®ex::escape(str)) } } @@ -1912,7 +1918,7 @@ impl SerializableItem for TerminalView { } impl SearchableItem for TerminalView { - type Match = RangeInclusive; + type Match = Range; fn supported_options(&self) -> SearchOptions { SearchOptions { @@ -2026,8 +2032,8 @@ impl SearchableItem for TerminalView { .enumerate() .rev() .find(|(_, search_match)| { - search_match.contains(&selection_head) - || search_match.start() < &selection_head + search_match.contains(selection_head) + || search_match.start() < selection_head }) .map(|(ix, _)| ix) .unwrap_or(0), @@ -2040,8 +2046,8 @@ impl SearchableItem for TerminalView { .iter() .enumerate() .find(|(_, search_match)| { - search_match.contains(&selection_head) - || search_match.start() > &selection_head + search_match.contains(selection_head) + || search_match.start() > selection_head }) .map(|(ix, _)| ix) .unwrap_or(matches.len().saturating_sub(1)), @@ -2223,7 +2229,7 @@ mod tests { }); }); terminal.read_with(&cx, |terminal, _| { - assert!(!terminal.last_content.mode.contains(TermMode::ALT_SCREEN)); + assert!(!terminal.last_content.mode.contains(Modes::ALT_SCREEN)); assert_eq!(terminal.last_content.display_offset, 0); }); @@ -2265,7 +2271,7 @@ mod tests { }); }); terminal.read_with(&cx, |terminal, _| { - assert!(terminal.last_content.mode.contains(TermMode::ALT_SCREEN)); + assert!(terminal.last_content.mode.contains(Modes::ALT_SCREEN)); }); cx.simulate_keystrokes("shift-up"); @@ -2479,7 +2485,6 @@ mod tests { cx.background_executor(), PathStyle::local(), ) - .unwrap() .subscribe(cx) }); let terminal_view = cx.new(|cx| {