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
This commit is contained in:
1 parent
72de52d291
commit
1977fb05fe
28 files changed
+2839
-1604
No files matched your search
Generated
+3
-3
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1675,7 +1675,6 @@ mod tests {
|
||||
cx.background_executor(),
|
||||
PathStyle::local(),
|
||||
)
|
||||
.unwrap()
|
||||
.subscribe(cx)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::<ansi::StdSyncHandler>::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<usize>, Option<ansi::Color>)>,
|
||||
background_spans: Vec<(Range<usize>, Option<ansi::Color>)>,
|
||||
current_range_start: usize,
|
||||
current_background_range_start: usize,
|
||||
current_color: Option<ansi::Color>,
|
||||
current_background_color: Option<ansi::Color>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl ConsoleHandler {
|
||||
fn break_span(&mut self, color: Option<ansi::Color>) {
|
||||
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<ansi::Color>) {
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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::<ansi::StdSyncHandler>::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<Workspace>,
|
||||
action: impl Into<SharedString>,
|
||||
@@ -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::<ansi::StdSyncHandler>::default();
|
||||
processor.advance(&mut handler, input.as_bytes());
|
||||
assert_eq!(handler.output, expected);
|
||||
assert_eq!(terminal::strip_ansi_text(input.as_bytes()), expected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -143,14 +143,14 @@ impl Output {
|
||||
pub fn to_nbformat(&self, cx: &App) -> Option<nbformat::v4::Output> {
|
||||
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<String> =
|
||||
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<String> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<Entity<Buffer>>,
|
||||
/// ANSI escape sequence processor for parsing input text.
|
||||
parser: Processor,
|
||||
/// Alacritty terminal instance that manages the terminal state and content.
|
||||
handler: alacritty_terminal::Term<VoidListener>,
|
||||
terminal: Entity<Terminal>,
|
||||
}
|
||||
|
||||
/// 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>) -> 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>) -> 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>) {
|
||||
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<String> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<SettingsStore, _>(|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::<String>();
|
||||
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<Self>) -> 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<ClipboardItem> {
|
||||
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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+150
-90
@@ -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<RegexSearch>,
|
||||
path_hyperlink_regexes: Vec<Regex>,
|
||||
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<Item: AsRef<str>>,
|
||||
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<T: EventListener>(
|
||||
impl Default for RegexSearches {
|
||||
fn default() -> Self {
|
||||
Self::new(Vec::<String>::new(), 0)
|
||||
}
|
||||
}
|
||||
impl RegexSearches {
|
||||
pub(crate) fn new(
|
||||
path_hyperlink_regexes: impl IntoIterator<Item: AsRef<str>>,
|
||||
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<Item: AsRef<str>>,
|
||||
) -> Vec<Regex> {
|
||||
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<T: EventListener>(
|
||||
term: &Term<T>,
|
||||
point: AlacPoint,
|
||||
regex_searches: &mut RegexSearches,
|
||||
path_style: PathStyle,
|
||||
) -> Option<(String, bool, Match)> {
|
||||
) -> Option<HyperlinkMatch> {
|
||||
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<T: EventListener>(
|
||||
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<T: EventListener>(
|
||||
}
|
||||
};
|
||||
|
||||
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<T>(
|
||||
if line.len() <= hovered_point_byte_offset {
|
||||
return None;
|
||||
}
|
||||
let found_from_range = |path_range: Range<usize>,
|
||||
link_range: Range<usize>,
|
||||
let found_from_range = |path_range: StdRange<usize>,
|
||||
link_range: StdRange<usize>,
|
||||
position: Option<(u32, Option<u32>)>| {
|
||||
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<VoidListener>,
|
||||
point: AlacPoint,
|
||||
) -> Option<(String, bool, Match)> {
|
||||
) -> Option<HyperlinkMatch> {
|
||||
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()
|
||||
@@ -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<Rgba>) -> AlacRgb {
|
||||
pub(crate) fn to_vte_rgb(color: impl Into<Rgba>) -> 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 }
|
||||
}
|
||||
@@ -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<Cow<'static, str>> {
|
||||
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]
|
||||
|
||||
@@ -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;
|
||||
@@ -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<MouseButton>) -> 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<impl Iterator<Item = Vec<u8>>> {
|
||||
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<Vec<u8>> = 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<Vec<u8>> = 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<u8> {
|
||||
pub(crate) fn alt_scroll(scroll_lines: i32) -> Vec<u8> {
|
||||
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<u8> {
|
||||
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<Vec<u8>> {
|
||||
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<MouseButton>,
|
||||
modifiers: Modifiers,
|
||||
mode: TermMode,
|
||||
mode: Modes,
|
||||
) -> Option<Vec<u8>> {
|
||||
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<Pixels>,
|
||||
pub(crate) fn grid_point(
|
||||
pos: GpuiPoint<Pixels>,
|
||||
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<Pixels>,
|
||||
pub(crate) fn grid_point_and_side(
|
||||
pos: GpuiPoint<Pixels>,
|
||||
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<Vec<u8>> {
|
||||
let AlacPoint { line, column } = point;
|
||||
fn normal_mouse_report(point: Point, button: u8, utf8: bool) -> Option<Vec<u8>> {
|
||||
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<Vec<u
|
||||
vec![first as u8, second as u8]
|
||||
};
|
||||
|
||||
if utf8 && column >= 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!(
|
||||
|
||||
@@ -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<Pid> {
|
||||
// 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<Pid> {
|
||||
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<String>,
|
||||
pub(crate) struct ProcessInfo {
|
||||
pub(crate) name: String,
|
||||
pub(crate) cwd: PathBuf,
|
||||
pub(crate) argv: Vec<String>,
|
||||
}
|
||||
|
||||
/// Fetches Zed-relevant Pseudo-Terminal (PTY) process information
|
||||
pub struct PtyProcessInfo {
|
||||
pub(crate) struct PtyProcessInfo {
|
||||
system: RwLock<System>,
|
||||
refresh_kind: ProcessRefreshKind,
|
||||
pid_getter: ProcessIdGetter,
|
||||
pub current: RwLock<Option<ProcessInfo>>,
|
||||
pub(crate) current: RwLock<Option<ProcessInfo>>,
|
||||
task: Mutex<Option<Task<()>>>,
|
||||
}
|
||||
|
||||
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<Self>, cx: &mut Context<'_, Terminal>) {
|
||||
pub(crate) fn emit_title_changed_if_changed(self: &Arc<Self>, cx: &mut Context<'_, Terminal>) {
|
||||
if self.task.lock().is_some() {
|
||||
return;
|
||||
}
|
||||
@@ -220,7 +202,7 @@ impl PtyProcessInfo {
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn pid(&self) -> Option<Pid> {
|
||||
pub(crate) fn pid(&self) -> Option<Pid> {
|
||||
self.pid_getter.pid()
|
||||
}
|
||||
}
|
||||
+909
-675
File diff suppressed because it is too large.
Load diff
@@ -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<settings::CursorShapeContent> for CursorShape {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CursorShape> 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<CursorShape> for AlacCursorStyle {
|
||||
fn from(value: CursorShape) -> Self {
|
||||
AlacCursorStyle {
|
||||
shape: value.into(),
|
||||
blinking: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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<BatchedTextRun>,
|
||||
rects: Vec<LayoutRect>,
|
||||
relative_highlighted_ranges: Vec<(RangeInclusive<AlacPoint>, Hsla)>,
|
||||
relative_highlighted_ranges: Vec<(Range, Hsla)>,
|
||||
cursor: Option<CursorLayout>,
|
||||
ime_cursor_bounds: Option<Bounds<Pixels>>,
|
||||
background_color: Hsla,
|
||||
dimensions: TerminalBounds,
|
||||
mode: TermMode,
|
||||
mode: Modes,
|
||||
display_offset: usize,
|
||||
hyperlink_tooltip: Option<AnyElement>,
|
||||
block_below_cursor_element: Option<AnyElement>,
|
||||
@@ -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<i32, i32>,
|
||||
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<i32, i32>,
|
||||
start_point: LayoutPoint,
|
||||
c: char,
|
||||
style: TextRun,
|
||||
font_size: AbsoluteLength,
|
||||
@@ -135,17 +147,17 @@ impl BatchedTextRun {
|
||||
|
||||
pub fn paint(
|
||||
&self,
|
||||
origin: Point<Pixels>,
|
||||
origin: GpuiPoint<Pixels>,
|
||||
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<i32, i32>,
|
||||
point: LayoutPoint,
|
||||
num_of_cells: usize,
|
||||
color: Hsla,
|
||||
}
|
||||
|
||||
impl LayoutRect {
|
||||
fn new(point: AlacPoint<i32, i32>, 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<Pixels>, dimensions: &TerminalBounds, window: &mut Window) {
|
||||
pub fn paint(
|
||||
&self,
|
||||
origin: GpuiPoint<Pixels>,
|
||||
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<BackgroundRegion>) -> Vec<BackgroundRegion> {
|
||||
if regions.is_empty() {
|
||||
@@ -326,13 +369,11 @@ impl TerminalElement {
|
||||
.track_focus(&focus)
|
||||
}
|
||||
|
||||
//Vec<Range<AlacPoint>> -> Clip out the parts of the ranges
|
||||
|
||||
pub fn layout_grid(
|
||||
grid: impl Iterator<Item = IndexedCell>,
|
||||
pub fn layout_grid<T: TerminalLayoutCell>(
|
||||
grid: impl Iterator<Item = T>,
|
||||
start_line_offset: i32,
|
||||
text_style: &TextStyle,
|
||||
hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
|
||||
hyperlink: Option<(HighlightStyle, &Range)>,
|
||||
minimum_contrast: f32,
|
||||
cx: &App,
|
||||
) -> (Vec<LayoutRect>, Vec<BatchedTextRun>) {
|
||||
@@ -352,9 +393,9 @@ impl TerminalElement {
|
||||
let mut current_batch: Option<BatchedTextRun> = 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<Point<Pixels>> {
|
||||
if cursor_point.line() < size.total_lines() as i32 {
|
||||
fn cursor_position(
|
||||
cursor_point: DisplayCursor,
|
||||
size: TerminalBounds,
|
||||
) -> Option<GpuiPoint<Pixels>> {
|
||||
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<AlacPoint>)>,
|
||||
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<Pixels>,
|
||||
_point: GpuiPoint<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Option<usize> {
|
||||
@@ -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<AlacPoint>,
|
||||
range: &Range,
|
||||
layout: &LayoutState,
|
||||
origin: Point<Pixels>,
|
||||
origin: GpuiPoint<Pixels>,
|
||||
) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<usize> {
|
||||
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<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
context_menu: Option<(Entity<ContextMenu>, GpuiPoint<Pixels>, Subscription)>,
|
||||
cursor_shape: CursorShape,
|
||||
blink_manager: Entity<BlinkManager>,
|
||||
mode: TerminalMode,
|
||||
@@ -353,7 +359,7 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
/// Gets the current marked range (UTF-16).
|
||||
pub(crate) fn marked_text_range(&self) -> Option<Range<usize>> {
|
||||
pub(crate) fn marked_text_range(&self) -> Option<StdRange<usize>> {
|
||||
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<Pixels>,
|
||||
position: GpuiPoint<Pixels>,
|
||||
has_selection: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -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<Self>) {
|
||||
@@ -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<RegexSearch> {
|
||||
fn regex_search_for_query(query: &SearchQuery) -> Option<Search> {
|
||||
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<AlacPoint>;
|
||||
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| {
|
||||
|
||||
Reference in new issue
Block a user