diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index bea53ece45b2a672879453bb522fd64814e635f3..6cd3660bf5120d4b16f1f6988588a537b7b92a31 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -417,7 +417,8 @@ "up": "terminal::Up", "down": "terminal::Down", "tab": "terminal::Tab", - "cmd-v": "terminal::Paste" + "cmd-v": "terminal::Paste", + "cmd-c": "terminal::Copy" } } ] \ No newline at end of file diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c1e25575557bef78921841f1c2a9eb8e589085a2..808926ff50f3e71b157f6b8380980fc1bd109044 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8232,7 +8232,7 @@ mod tests { fox ju|mps over the lazy dog"}); cx.update_editor(|e, cx| e.copy(&Copy, cx)); - cx.assert_clipboard_content(Some("fox jumps over\n")); + cx.cx.assert_clipboard_content(Some("fox jumps over\n")); // Paste with three selections, noticing how the copied full-line selection is inserted // before the empty selections but replaces the selection that is non-empty. diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9c16ca5cd1d12042181ab86eaf5dca8b7714c9d9..56f664566ecd03bba53cbd7960e3ac5be8d33287 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1695,22 +1695,22 @@ impl Cursor { } #[derive(Debug)] -struct HighlightedRange { - start_y: f32, - line_height: f32, - lines: Vec, - color: Color, - corner_radius: f32, +pub struct HighlightedRange { + pub start_y: f32, + pub line_height: f32, + pub lines: Vec, + pub color: Color, + pub corner_radius: f32, } #[derive(Debug)] -struct HighlightedRangeLine { - start_x: f32, - end_x: f32, +pub struct HighlightedRangeLine { + pub start_x: f32, + pub end_x: f32, } impl HighlightedRange { - fn paint(&self, bounds: RectF, scene: &mut Scene) { + pub fn paint(&self, bounds: RectF, scene: &mut Scene) { if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x { self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene); self.paint_lines( diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index d1316a85a0bf38afe5c39cb596b6647b89119252..0affe06f64b555890ea3b6289ec497ace6aeb18d 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -404,14 +404,6 @@ impl<'a> EditorTestContext<'a> { editor_text_with_selections } - - pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) { - self.cx.update(|cx| { - let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned()); - let expected_content = expected_content.map(|content| content.to_owned()); - assert_eq!(actual_content, expected_content); - }) - } } impl<'a> Deref for EditorTestContext<'a> { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e7c69445ec2d934d1a7c10e51b86d38d401ffd1b..901c131d032bd6683951c866b7000bb2f4b4fb4a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -627,6 +627,14 @@ impl TestAppContext { } }) } + + pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) { + self.update(|cx| { + let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned()); + let expected_content = expected_content.map(|content| content.to_owned()); + assert_eq!(actual_content, expected_content); + }) + } } impl AsyncAppContext { diff --git a/crates/terminal/src/color_translation.rs b/crates/terminal/src/color_translation.rs new file mode 100644 index 0000000000000000000000000000000000000000..78c2a569dbbeac5806e6535a2c87b7c0b6b57c03 --- /dev/null +++ b/crates/terminal/src/color_translation.rs @@ -0,0 +1,134 @@ +use alacritty_terminal::{ansi::Color as AnsiColor, term::color::Rgb as AlacRgb}; +use gpui::color::Color; +use theme::TerminalStyle; + +///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent +pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { + match alac_color { + //Named and theme defined colors + alacritty_terminal::ansi::Color::Named(n) => match n { + alacritty_terminal::ansi::NamedColor::Black => style.black, + alacritty_terminal::ansi::NamedColor::Red => style.red, + alacritty_terminal::ansi::NamedColor::Green => style.green, + alacritty_terminal::ansi::NamedColor::Yellow => style.yellow, + alacritty_terminal::ansi::NamedColor::Blue => style.blue, + alacritty_terminal::ansi::NamedColor::Magenta => style.magenta, + alacritty_terminal::ansi::NamedColor::Cyan => style.cyan, + alacritty_terminal::ansi::NamedColor::White => style.white, + alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black, + alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red, + alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green, + alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow, + alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue, + alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta, + alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan, + alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white, + alacritty_terminal::ansi::NamedColor::Foreground => style.foreground, + alacritty_terminal::ansi::NamedColor::Background => style.background, + alacritty_terminal::ansi::NamedColor::Cursor => style.cursor, + alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black, + alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red, + alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green, + alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow, + alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue, + alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta, + alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan, + alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white, + alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, + alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, + }, + //'True' colors + alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), + //8 bit, indexed colors + alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style), + } +} + +///Converts an 8 bit ANSI color to it's GPUI equivalent. +///Accepts usize for compatability with the alacritty::Colors interface, +///Other than that use case, should only be called with values in the [0,255] range +pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color { + match index { + //0-15 are the same as the named colors above + 0 => style.black, + 1 => style.red, + 2 => style.green, + 3 => style.yellow, + 4 => style.blue, + 5 => style.magenta, + 6 => style.cyan, + 7 => style.white, + 8 => style.bright_black, + 9 => style.bright_red, + 10 => style.bright_green, + 11 => style.bright_yellow, + 12 => style.bright_blue, + 13 => style.bright_magenta, + 14 => style.bright_cyan, + 15 => style.bright_white, + //16-231 are mapped to their RGB colors on a 0-5 range per channel + 16..=231 => { + let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components + let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow + Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color + } + //232-255 are a 24 step grayscale from black to white + 232..=255 => { + let i = *index as u8 - 232; //Align index to 0..24 + let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks + Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale + } + //For compatability with the alacritty::Colors interface + 256 => style.foreground, + 257 => style.background, + 258 => style.cursor, + 259 => style.dim_black, + 260 => style.dim_red, + 261 => style.dim_green, + 262 => style.dim_yellow, + 263 => style.dim_blue, + 264 => style.dim_magenta, + 265 => style.dim_cyan, + 266 => style.dim_white, + 267 => style.bright_foreground, + 268 => style.black, //'Dim Background', non-standard color + _ => Color::new(0, 0, 0, 255), + } +} +///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube +///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). +/// +///Wikipedia gives a formula for calculating the index for a given color: +/// +///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) +/// +///This function does the reverse, calculating the r, g, and b components from a given index. +fn rgb_for_index(i: &u8) -> (u8, u8, u8) { + debug_assert!(i >= &16 && i <= &231); + let i = i - 16; + let r = (i - (i % 36)) / 36; + let g = ((i % 36) - (i % 6)) / 6; + let b = (i % 36) % 6; + (r, g, b) +} + +//Convenience method to convert from a GPUI color to an alacritty Rgb +pub fn to_alac_rgb(color: Color) -> AlacRgb { + AlacRgb { + r: color.r, + g: color.g, + b: color.g, + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_rgb_for_index() { + //Test every possible value in the color cube + for i in 16..=231 { + let (r, g, b) = crate::color_translation::rgb_for_index(&(i as u8)); + assert_eq!(i, 16 + 36 * r + 6 * g + b); + } + } +} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 4542e283ee2fd5ee5c0db34bace104fdada1f861..46591f14815788d14f25cc72b0f529578f4f9ab6 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -4,19 +4,19 @@ use alacritty_terminal::{ event_loop::{EventLoop, Msg, Notifier}, grid::Scroll, sync::FairMutex, - term::{color::Rgb as AlacRgb, SizeInfo}, + term::SizeInfo, tty::{self, setup_env}, Term, }; - +use color_translation::{get_color_at_index, to_alac_rgb}; use dirs::home_dir; use futures::{ channel::mpsc::{unbounded, UnboundedSender}, StreamExt, }; use gpui::{ - actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle, - ClipboardItem, Entity, MutableAppContext, View, ViewContext, + actions, elements::*, impl_internal_actions, platform::CursorStyle, ClipboardItem, Entity, + MutableAppContext, View, ViewContext, }; use project::{LocalWorktree, Project, ProjectPath}; use settings::Settings; @@ -24,7 +24,7 @@ use smallvec::SmallVec; use std::{collections::HashMap, path::PathBuf, sync::Arc}; use workspace::{Item, Workspace}; -use crate::terminal_element::{get_color_at_index, TerminalEl}; +use crate::terminal_element::TerminalEl; //ASCII Control characters on a keyboard const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c' @@ -37,7 +37,12 @@ const RIGHT_SEQ: &str = "\x1b[C"; const UP_SEQ: &str = "\x1b[A"; const DOWN_SEQ: &str = "\x1b[B"; const DEFAULT_TITLE: &str = "Terminal"; +const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space. +const DEBUG_TERMINAL_HEIGHT: f32 = 200.; +const DEBUG_CELL_WIDTH: f32 = 5.; +const DEBUG_LINE_HEIGHT: f32 = 5.; +pub mod color_translation; pub mod gpui_func_tools; pub mod terminal_element; @@ -51,7 +56,7 @@ pub struct ScrollTerminal(pub i32); actions!( terminal, - [Sigint, Escape, Del, Return, Left, Right, Up, Down, Tab, Clear, Paste, Deploy, Quit] + [Sigint, Escape, Del, Return, Left, Right, Up, Down, Tab, Clear, Copy, Paste, Deploy, Quit] ); impl_internal_actions!(terminal, [Input, ScrollTerminal]); @@ -63,12 +68,13 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::escape); cx.add_action(Terminal::quit); cx.add_action(Terminal::del); - cx.add_action(Terminal::carriage_return); //TODO figure out how to do this properly. Should we be checking the terminal mode? + cx.add_action(Terminal::carriage_return); cx.add_action(Terminal::left); cx.add_action(Terminal::right); cx.add_action(Terminal::up); cx.add_action(Terminal::down); cx.add_action(Terminal::tab); + cx.add_action(Terminal::copy); cx.add_action(Terminal::paste); cx.add_action(Terminal::scroll_terminal); } @@ -126,12 +132,11 @@ impl Terminal { .detach(); let pty_config = PtyConfig { - shell: None, + shell: None, //Use the users default shell working_directory: working_directory.clone(), hold: false, }; - //Does this mangle the zed Env? I'm guessing it does... do child processes have a seperate ENV? let mut env: HashMap = HashMap::new(); //TODO: Properly set the current locale, env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); @@ -145,8 +150,15 @@ impl Terminal { setup_env(&config); //The details here don't matter, the terminal will be resized on the first layout - //Set to something small for easier debugging - let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false); + let size_info = SizeInfo::new( + DEBUG_TERMINAL_WIDTH, + DEBUG_TERMINAL_HEIGHT, + DEBUG_CELL_WIDTH, + DEBUG_LINE_HEIGHT, + 0., + 0., + false, + ); //Set up the terminal... let term = Term::new(&config, size_info, ZedListener(events_tx.clone())); @@ -222,24 +234,7 @@ impl Terminal { AlacTermEvent::ColorRequest(index, format) => { let color = self.term.lock().colors()[index].unwrap_or_else(|| { let term_style = &cx.global::().theme.terminal; - match index { - 0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)), - //These additional values are required to match the Alacritty Colors object's behavior - 256 => to_alac_rgb(term_style.foreground), - 257 => to_alac_rgb(term_style.background), - 258 => to_alac_rgb(term_style.cursor), - 259 => to_alac_rgb(term_style.dim_black), - 260 => to_alac_rgb(term_style.dim_red), - 261 => to_alac_rgb(term_style.dim_green), - 262 => to_alac_rgb(term_style.dim_yellow), - 263 => to_alac_rgb(term_style.dim_blue), - 264 => to_alac_rgb(term_style.dim_magenta), - 265 => to_alac_rgb(term_style.dim_cyan), - 266 => to_alac_rgb(term_style.dim_white), - 267 => to_alac_rgb(term_style.bright_foreground), - 268 => to_alac_rgb(term_style.black), //Dim Background, non-standard - _ => AlacRgb { r: 0, g: 0, b: 0 }, - } + to_alac_rgb(get_color_at_index(&index, term_style)) }); self.write_to_pty(&Input(format(color)), cx) } @@ -291,6 +286,16 @@ impl Terminal { cx.emit(Event::CloseTerminal); } + ///Attempt to paste the clipboard into the terminal + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + let term = self.term.lock(); + let copy_text = term.selection_to_string(); + match copy_text { + Some(s) => cx.write_to_clipboard(ClipboardItem::new(s)), + None => (), + } + } + ///Attempt to paste the clipboard into the terminal fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { if let Some(item) = cx.read_from_clipboard() { @@ -479,15 +484,6 @@ impl Item for Terminal { } } -//Convenience method for less lines -fn to_alac_rgb(color: Color) -> AlacRgb { - AlacRgb { - r: color.r, - g: color.g, - b: color.g, - } -} - fn get_working_directory(wt: &LocalWorktree) -> Option { Some(wt.abs_path().to_path_buf()) .filter(|path| path.is_dir()) @@ -497,13 +493,17 @@ fn get_working_directory(wt: &LocalWorktree) -> Option { #[cfg(test)] mod tests { - use std::{path::Path, sync::atomic::AtomicUsize, time::Duration}; - use super::*; - use alacritty_terminal::{grid::GridIterator, term::cell::Cell}; + use alacritty_terminal::{ + grid::GridIterator, + index::{Column, Line, Point, Side}, + selection::{Selection, SelectionType}, + term::cell::Cell, + }; use gpui::TestAppContext; use itertools::Itertools; use project::{FakeFs, Fs, RealFs, RemoveOptions, Worktree}; + use std::{path::Path, sync::atomic::AtomicUsize, time::Duration}; ///Basic integration test, can we get the terminal to show up, execute a command, //and produce noticable output? @@ -511,7 +511,6 @@ mod tests { async fn test_terminal(cx: &mut TestAppContext) { let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None)); cx.set_condition_duration(Duration::from_secs(2)); - terminal.update(cx, |terminal, cx| { terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx); terminal.carriage_return(&Return, cx); @@ -521,20 +520,12 @@ mod tests { .condition(cx, |terminal, _cx| { let term = terminal.term.clone(); let content = grid_as_str(term.lock().renderable_content().display_iter); + dbg!(&content); content.contains("7") }) .await; } - pub(crate) fn grid_as_str(grid_iterator: GridIterator) -> String { - let lines = grid_iterator.group_by(|i| i.point.line.0); - lines - .into_iter() - .map(|(_, line)| line.map(|i| i.c).collect::()) - .collect::>() - .join("\n") - } - #[gpui::test] async fn single_file_worktree(cx: &mut TestAppContext) { let mut async_cx = cx.to_async(); @@ -615,4 +606,46 @@ mod tests { .ok() .expect("Could not remove test directory"); } + + ///If this test is failing for you, check that DEBUG_TERMINAL_WIDTH is wide enough to fit your entire command prompt! + #[gpui::test] + async fn test_copy(cx: &mut TestAppContext) { + let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None)); + cx.set_condition_duration(Duration::from_secs(2)); + + terminal.update(cx, |terminal, cx| { + terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx); + terminal.carriage_return(&Return, cx); + }); + + terminal + .condition(cx, |terminal, _cx| { + let term = terminal.term.clone(); + let content = grid_as_str(term.lock().renderable_content().display_iter); + content.contains("7") + }) + .await; + + terminal.update(cx, |terminal, cx| { + let mut term = terminal.term.lock(); + term.selection = Some(Selection::new( + SelectionType::Semantic, + Point::new(Line(2), Column(0)), + Side::Right, + )); + drop(term); + terminal.copy(&Copy, cx) + }); + + cx.assert_clipboard_content(Some(&"7")); + } + + pub(crate) fn grid_as_str(grid_iterator: GridIterator) -> String { + let lines = grid_iterator.group_by(|i| i.point.line.0); + lines + .into_iter() + .map(|(_, line)| line.map(|i| i.c).collect::()) + .collect::>() + .join("\n") + } } diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index ecbd94f640d08bc9555a97ddae5b0e511cde726f..1ad6aed6aeab935b20087166de1ee4d3c53a1507 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -1,13 +1,15 @@ use alacritty_terminal::{ - ansi::Color as AnsiColor, grid::{Dimensions, GridIterator, Indexed}, - index::Point, + index::{Column as GridCol, Line as GridLine, Point, Side}, + selection::{Selection, SelectionRange, SelectionType}, + sync::FairMutex, term::{ cell::{Cell, Flags}, SizeInfo, }, + Term, }; -use editor::{Cursor, CursorShape}; +use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ color::Color, elements::*, @@ -24,10 +26,14 @@ use gpui::{ use itertools::Itertools; use ordered_float::OrderedFloat; use settings::Settings; -use std::rc::Rc; +use std::{cmp::min, ops::Range, rc::Rc, sync::Arc}; +use std::{fmt::Debug, ops::Sub}; use theme::TerminalStyle; -use crate::{gpui_func_tools::paint_layer, Input, ScrollTerminal, Terminal}; +use crate::{ + color_translation::convert_color, gpui_func_tools::paint_layer, Input, ScrollTerminal, + Terminal, ZedListener, +}; ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I @@ -44,14 +50,27 @@ pub struct TerminalEl { view: WeakViewHandle, } -///Helper types so I don't mix these two up +///New type pattern so I don't mix these two up struct CellWidth(f32); struct LineHeight(f32); +struct LayoutLine { + cells: Vec, + highlighted_range: Option>, +} + +///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than +struct PaneRelativePos(Vector2F); + +///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position +fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos { + PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating +} + #[derive(Clone, Debug, Default)] struct LayoutCell { point: Point, - text: Line, + text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN! background_color: Color, } @@ -67,13 +86,14 @@ impl LayoutCell { ///The information generated during layout that is nescessary for painting pub struct LayoutState { - cells: Vec<(Point, Line)>, - background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan + layout_lines: Vec, line_height: LineHeight, em_width: CellWidth, cursor: Option, background_color: Color, cur_size: SizeInfo, + terminal: Arc>>, + selection_color: Color, } impl TerminalEl { @@ -105,46 +125,30 @@ impl Element for TerminalEl { //Tell the view our new size. Requires a mutable borrow of cx and the view let cur_size = make_new_size(constraint, &cell_width, &line_height); //Note that set_size locks and mutates the terminal. - //TODO: Would be nice to lock once for the whole of layout view_handle.update(cx.app, |view, _cx| view.set_size(cur_size)); //Now that we're done with the mutable portion, grab the immutable settings and view again - let terminal_theme = &(cx.global::()).theme.terminal; - let term = view_handle.read(cx).term.lock(); + let (selection_color, terminal_theme) = { + let theme = &(cx.global::()).theme; + (theme.editor.selection.selection, &theme.terminal) + }; + let terminal_mutex = view_handle.read(cx).term.clone(); + let term = terminal_mutex.lock(); let grid = term.grid(); let cursor_point = grid.cursor.point; let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string(); let content = term.renderable_content(); - let layout_cells = layout_cells( + let layout_lines = layout_lines( content.display_iter, &text_style, terminal_theme, cx.text_layout_cache, + content.selection, ); - let cells = layout_cells - .iter() - .map(|c| (c.point, c.text.clone())) - .collect::, Line)>>(); - let background_rects = layout_cells - .iter() - .map(|cell| { - ( - RectF::new( - vec2f( - cell.point.column as f32 * cell_width.0, - cell.point.line as f32 * line_height.0, - ), - vec2f(cell_width.0, line_height.0), - ), - cell.background_color, - ) - }) - .collect::>(); - let block_text = cx.text_layout_cache.layout_str( &cursor_text, text_style.font_size, @@ -183,17 +187,19 @@ impl Element for TerminalEl { Some(block_text.clone()), ) }); + drop(term); ( constraint.max, LayoutState { - cells, + layout_lines, line_height, em_width: cell_width, cursor, cur_size, - background_rects, background_color: terminal_theme.background, + terminal: terminal_mutex, + selection_color, }, ) } @@ -207,17 +213,21 @@ impl Element for TerminalEl { ) -> Self::PaintState { //Setup element stuff let clip_bounds = Some(visible_bounds); - paint_layer(cx, clip_bounds, |cx| { - //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse - cx.scene.push_mouse_region(MouseRegion { - view_id: self.view.id(), - mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())), - bounds: visible_bounds, - ..Default::default() - }); + paint_layer(cx, clip_bounds, |cx| { + let cur_size = layout.cur_size.clone(); let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); + //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse + attach_mouse_handlers( + origin, + cur_size, + self.view.id(), + &layout.terminal, + visible_bounds, + cx, + ); + paint_layer(cx, clip_bounds, |cx| { //Start with a background color cx.scene.push_quad(Quad { @@ -228,25 +238,83 @@ impl Element for TerminalEl { }); //Draw cell backgrounds - for background_rect in &layout.background_rects { - let new_origin = origin + background_rect.0.origin(); - cx.scene.push_quad(Quad { - bounds: RectF::new(new_origin, background_rect.0.size()), - background: Some(background_rect.1), - border: Default::default(), - corner_radius: 0., + for layout_line in &layout.layout_lines { + for layout_cell in &layout_line.cells { + let position = vec2f( + origin.x() + layout_cell.point.column as f32 * layout.em_width.0, + origin.y() + layout_cell.point.line as f32 * layout.line_height.0, + ); + let size = vec2f(layout.em_width.0, layout.line_height.0); + + cx.scene.push_quad(Quad { + bounds: RectF::new(position, size), + background: Some(layout_cell.background_color), + border: Default::default(), + corner_radius: 0., + }) + } + } + }); + + //Draw Selection + paint_layer(cx, clip_bounds, |cx| { + let mut highlight_y = None; + let highlight_lines = layout + .layout_lines + .iter() + .filter_map(|line| { + if let Some(range) = &line.highlighted_range { + if let None = highlight_y { + highlight_y = Some( + origin.y() + + line.cells[0].point.line as f32 * layout.line_height.0, + ); + } + let start_x = origin.x() + + line.cells[range.start].point.column as f32 * layout.em_width.0; + let end_x = origin.x() + + line.cells[range.end].point.column as f32 * layout.em_width.0 + + layout.em_width.0; + + return Some(HighlightedRangeLine { start_x, end_x }); + } else { + return None; + } }) + .collect::>(); + + if let Some(y) = highlight_y { + let hr = HighlightedRange { + start_y: y, //Need to change this + line_height: layout.line_height.0, + lines: highlight_lines, + color: layout.selection_color, + //Copied from editor. TODO: move to theme or something + corner_radius: 0.15 * layout.line_height.0, + }; + hr.paint(bounds, cx.scene); } }); //Draw text paint_layer(cx, clip_bounds, |cx| { - for (point, cell) in &layout.cells { - let cell_origin = vec2f( - origin.x() + point.column as f32 * layout.em_width.0, - origin.y() + point.line as f32 * layout.line_height.0, - ); - cell.paint(cell_origin, visible_bounds, layout.line_height.0, cx); + for layout_line in &layout.layout_lines { + for layout_cell in &layout_line.cells { + let point = layout_cell.point; + + //Don't actually know the start_x for a line, until here: + let cell_origin = vec2f( + origin.x() + point.column as f32 * layout.em_width.0, + origin.y() + point.line as f32 * layout.line_height.0, + ); + + layout_cell.text.paint( + cell_origin, + visible_bounds, + layout.line_height.0, + cx, + ); + } } }); @@ -311,6 +379,18 @@ impl Element for TerminalEl { } } +fn mouse_to_cell_data( + pos: Vector2F, + origin: Vector2F, + cur_size: SizeInfo, + display_offset: usize, +) -> (Point, alacritty_terminal::index::Direction) { + let relative_pos = relative_pos(pos, origin); + let point = grid_cell(&relative_pos, cur_size, display_offset); + let side = cell_side(&relative_pos, cur_size); + (point, side) +} + ///Configures a text style from the current settings. fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { TextStyle { @@ -343,38 +423,56 @@ fn make_new_size( ) } -fn layout_cells( +fn layout_lines( grid: GridIterator, text_style: &TextStyle, terminal_theme: &TerminalStyle, text_layout_cache: &TextLayoutCache, -) -> Vec { - let mut line_count: i32 = 0; + selection_range: Option, +) -> Vec { let lines = grid.group_by(|i| i.point.line); lines .into_iter() - .map(|(_, line)| { - line_count += 1; - line.map(|indexed_cell| { - let cell_text = &indexed_cell.c.to_string(); - - let cell_style = cell_style(&indexed_cell, terminal_theme, text_style); - - let layout_cell = text_layout_cache.layout_str( - cell_text, - text_style.font_size, - &[(cell_text.len(), cell_style)], - ); - LayoutCell::new( - Point::new(line_count - 1, indexed_cell.point.column.0 as i32), - layout_cell, - convert_color(&indexed_cell.bg, terminal_theme), - ) - }) - .collect::>() + .enumerate() + .map(|(line_index, (_, line))| { + let mut highlighted_range = None; + let cells = line + .enumerate() + .map(|(x_index, indexed_cell)| { + if selection_range + .map(|range| range.contains(indexed_cell.point)) + .unwrap_or(false) + { + let mut range = highlighted_range.take().unwrap_or(x_index..x_index); + range.end = range.end.max(x_index); + highlighted_range = Some(range); + } + + let cell_text = &indexed_cell.c.to_string(); + + let cell_style = cell_style(&indexed_cell, terminal_theme, text_style); + + //This is where we might be able to get better performance + let layout_cell = text_layout_cache.layout_str( + cell_text, + text_style.font_size, + &[(cell_text.len(), cell_style)], + ); + + LayoutCell::new( + Point::new(line_index as i32, indexed_cell.point.column.0 as i32), + layout_cell, + convert_color(&indexed_cell.bg, terminal_theme), + ) + }) + .collect::>(); + + LayoutLine { + cells, + highlighted_range, + } }) - .flatten() - .collect::>() + .collect::>() } // Compute the cursor position and expected block width, may return a zero width if x_for_index returns @@ -430,98 +528,113 @@ fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle, text_style: &Text } } -///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent -fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { - match alac_color { - //Named and theme defined colors - alacritty_terminal::ansi::Color::Named(n) => match n { - alacritty_terminal::ansi::NamedColor::Black => style.black, - alacritty_terminal::ansi::NamedColor::Red => style.red, - alacritty_terminal::ansi::NamedColor::Green => style.green, - alacritty_terminal::ansi::NamedColor::Yellow => style.yellow, - alacritty_terminal::ansi::NamedColor::Blue => style.blue, - alacritty_terminal::ansi::NamedColor::Magenta => style.magenta, - alacritty_terminal::ansi::NamedColor::Cyan => style.cyan, - alacritty_terminal::ansi::NamedColor::White => style.white, - alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black, - alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red, - alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green, - alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow, - alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue, - alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta, - alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan, - alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white, - alacritty_terminal::ansi::NamedColor::Foreground => style.foreground, - alacritty_terminal::ansi::NamedColor::Background => style.background, - alacritty_terminal::ansi::NamedColor::Cursor => style.cursor, - alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black, - alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red, - alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green, - alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow, - alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue, - alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta, - alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan, - alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white, - alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, - alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, - }, - //'True' colors - alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), - //8 bit, indexed colors - alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), - } +fn attach_mouse_handlers( + origin: Vector2F, + cur_size: SizeInfo, + view_id: usize, + terminal_mutex: &Arc>>, + visible_bounds: RectF, + cx: &mut PaintContext, +) { + let click_mutex = terminal_mutex.clone(); + let drag_mutex = terminal_mutex.clone(); + let mouse_down_mutex = terminal_mutex.clone(); + + cx.scene.push_mouse_region(MouseRegion { + view_id, + mouse_down: Some(Rc::new(move |pos, _| { + let mut term = mouse_down_mutex.lock(); + let (point, side) = mouse_to_cell_data( + pos, + origin, + cur_size, + term.renderable_content().display_offset, + ); + term.selection = Some(Selection::new(SelectionType::Simple, point, side)) + })), + click: Some(Rc::new(move |pos, click_count, cx| { + let mut term = click_mutex.lock(); + + let (point, side) = mouse_to_cell_data( + pos, + origin, + cur_size, + term.renderable_content().display_offset, + ); + + let selection_type = match click_count { + 0 => return, //This is a release + 1 => Some(SelectionType::Simple), + 2 => Some(SelectionType::Semantic), + 3 => Some(SelectionType::Lines), + _ => None, + }; + + let selection = + selection_type.map(|selection_type| Selection::new(selection_type, point, side)); + + term.selection = selection; + cx.focus_parent_view(); + cx.notify(); + })), + bounds: visible_bounds, + drag: Some(Rc::new(move |_delta, pos, cx| { + let mut term = drag_mutex.lock(); + + let (point, side) = mouse_to_cell_data( + pos, + origin, + cur_size, + term.renderable_content().display_offset, + ); + + if let Some(mut selection) = term.selection.take() { + selection.update(point, side); + term.selection = Some(selection); + } + + cx.notify(); + })), + ..Default::default() + }); } -///Converts an 8 bit ANSI color to it's GPUI equivalent. -pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color { - match index { - //0-15 are the same as the named colors above - 0 => style.black, - 1 => style.red, - 2 => style.green, - 3 => style.yellow, - 4 => style.blue, - 5 => style.magenta, - 6 => style.cyan, - 7 => style.white, - 8 => style.bright_black, - 9 => style.bright_red, - 10 => style.bright_green, - 11 => style.bright_yellow, - 12 => style.bright_blue, - 13 => style.bright_magenta, - 14 => style.bright_cyan, - 15 => style.bright_white, - //16-231 are mapped to their RGB colors on a 0-5 range per channel - 16..=231 => { - let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components - let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow - Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color - } - //232-255 are a 24 step grayscale from black to white - 232..=255 => { - let i = index - 232; //Align index to 0..24 - let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks - Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale - } +///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side() +fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side { + let x = pos.0.x() as usize; + let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize; + let half_cell_width = (cur_size.cell_width() / 2.0) as usize; + + let additional_padding = + (cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width(); + let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding; + + if cell_x > half_cell_width + // Edge case when mouse leaves the window. + || x as f32 >= end_of_grid + { + Side::Right + } else { + Side::Left } } -///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube -///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). -/// -///Wikipedia gives a formula for calculating the index for a given color: -/// -///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) -/// -///This function does the reverse, calculating the r, g, and b components from a given index. -fn rgb_for_index(i: &u8) -> (u8, u8, u8) { - debug_assert!(i >= &16 && i <= &231); - let i = i - 16; - let r = (i - (i % 36)) / 36; - let g = ((i % 36) - (i % 6)) / 6; - let b = (i % 36) % 6; - (r, g, b) +///Copied (with modifications) from alacritty/src/event.rs > Mouse::point() +///Position is a pane-relative position. That means the top left corner of the mouse +///Region should be (0,0) +fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point { + let pos = pos.0; + let col = pos.x() / cur_size.cell_width(); //TODO: underflow... + let col = min(GridCol(col as usize), cur_size.last_column()); + + let line = pos.y() / cur_size.cell_height(); + let line = min(line as i32, cur_size.bottommost_line().0); + + //when clicking, need to ADD to get to the top left cell + //e.g. total_lines - viewport_height, THEN subtract display offset + //0 -> total_lines - viewport_height - display_offset + mouse_line + + Point::new(GridLine(line - display_offset as i32), col) } ///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between @@ -555,14 +668,73 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex } } -#[cfg(test)] -mod tests { +mod test { + #[test] - fn test_rgb_for_index() { - //Test every possible value in the color cube - for i in 16..=231 { - let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8)); - assert_eq!(i, 16 + 36 * r + 6 * g + b); - } + fn test_mouse_to_selection() { + let term_width = 100.; + let term_height = 200.; + let cell_width = 10.; + let line_height = 20.; + let mouse_pos_x = 100.; //Window relative + let mouse_pos_y = 100.; //Window relative + let origin_x = 10.; + let origin_y = 20.; + + let cur_size = alacritty_terminal::term::SizeInfo::new( + term_width, + term_height, + cell_width, + line_height, + 0., + 0., + false, + ); + + let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y); + let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in + let (point, _) = + crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); + assert_eq!( + point, + alacritty_terminal::index::Point::new( + alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32), + alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize), + ) + ); + } + + #[test] + fn test_mouse_to_selection_off_edge() { + let term_width = 100.; + let term_height = 200.; + let cell_width = 10.; + let line_height = 20.; + let mouse_pos_x = 100.; //Window relative + let mouse_pos_y = 100.; //Window relative + let origin_x = 10.; + let origin_y = 20.; + + let cur_size = alacritty_terminal::term::SizeInfo::new( + term_width, + term_height, + cell_width, + line_height, + 0., + 0., + false, + ); + + let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y); + let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in + let (point, _) = + crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0); + assert_eq!( + point, + alacritty_terminal::index::Point::new( + alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32), + alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize), + ) + ); } } diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 57d0174703bbf5da9781c1f6f70f5bb23a9527d7..08ec4bd5e961315fa8c661ffbcfac89ab3a59207 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -147,14 +147,6 @@ impl<'a> VimTestContext<'a> { let mode = self.mode(); VimBindingTestContext::new(keystrokes, mode, mode, self) } - - pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) { - self.cx.update(|cx| { - let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned()); - let expected_content = expected_content.map(|content| content.to_owned()); - assert_eq!(actual_content, expected_content); - }) - } } impl<'a> Deref for VimTestContext<'a> { diff --git a/pbcpoy b/pbcpoy new file mode 100644 index 0000000000000000000000000000000000000000..f70f10e4db19068f79bc43844b49f3eece45c4e8 --- /dev/null +++ b/pbcpoy @@ -0,0 +1 @@ +A