Merge pull request #1299 from zed-industries/terminal-selections

Mikayla Maki created

Terminal selections first pass

Change summary

assets/keymaps/default.json              |   3 
crates/editor/src/editor.rs              |   2 
crates/editor/src/element.rs             |  20 
crates/editor/src/test.rs                |   8 
crates/gpui/src/app.rs                   |   8 
crates/terminal/src/color_translation.rs | 134 ++++++
crates/terminal/src/terminal.rs          | 135 ++++--
crates/terminal/src/terminal_element.rs  | 522 +++++++++++++++++--------
crates/vim/src/vim_test_context.rs       |   8 
pbcpoy                                   |   1 
10 files changed, 587 insertions(+), 254 deletions(-)

Detailed changes

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"
         }
     }
 ]

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.

crates/editor/src/element.rs 🔗

@@ -1695,22 +1695,22 @@ impl Cursor {
 }
 
 #[derive(Debug)]
-struct HighlightedRange {
-    start_y: f32,
-    line_height: f32,
-    lines: Vec<HighlightedRangeLine>,
-    color: Color,
-    corner_radius: f32,
+pub struct HighlightedRange {
+    pub start_y: f32,
+    pub line_height: f32,
+    pub lines: Vec<HighlightedRangeLine>,
+    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(

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> {

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 {

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);
+        }
+    }
+}

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<String, String> = 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::<Settings>().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<Self>) {
+        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<Self>) {
         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<PathBuf> {
     Some(wt.abs_path().to_path_buf())
         .filter(|path| path.is_dir())
@@ -497,13 +493,17 @@ fn get_working_directory(wt: &LocalWorktree) -> Option<PathBuf> {
 #[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<Cell>) -> String {
-        let lines = grid_iterator.group_by(|i| i.point.line.0);
-        lines
-            .into_iter()
-            .map(|(_, line)| line.map(|i| i.c).collect::<String>())
-            .collect::<Vec<String>>()
-            .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<Cell>) -> String {
+        let lines = grid_iterator.group_by(|i| i.point.line.0);
+        lines
+            .into_iter()
+            .map(|(_, line)| line.map(|i| i.c).collect::<String>())
+            .collect::<Vec<String>>()
+            .join("\n")
+    }
 }

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<Terminal>,
 }
 
-///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<LayoutCell>,
+    highlighted_range: Option<Range<usize>>,
+}
+
+///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<i32, i32>,
-    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<i32, i32>, Line)>,
-    background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan
+    layout_lines: Vec<LayoutLine>,
     line_height: LineHeight,
     em_width: CellWidth,
     cursor: Option<Cursor>,
     background_color: Color,
     cur_size: SizeInfo,
+    terminal: Arc<FairMutex<Term<ZedListener>>>,
+    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::<Settings>()).theme.terminal;
-        let term = view_handle.read(cx).term.lock();
+        let (selection_color, terminal_theme) = {
+            let theme = &(cx.global::<Settings>()).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::<Vec<(Point<i32, i32>, 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::<Vec<(RectF, Color)>>();
-
         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::<Vec<HighlightedRangeLine>>();
+
+                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<Cell>,
     text_style: &TextStyle,
     terminal_theme: &TerminalStyle,
     text_layout_cache: &TextLayoutCache,
-) -> Vec<LayoutCell> {
-    let mut line_count: i32 = 0;
+    selection_range: Option<SelectionRange>,
+) -> Vec<LayoutLine> {
     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::<Vec<LayoutCell>>()
+        .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::<Vec<LayoutCell>>();
+
+            LayoutLine {
+                cells,
+                highlighted_range,
+            }
         })
-        .flatten()
-        .collect::<Vec<LayoutCell>>()
+        .collect::<Vec<LayoutLine>>()
 }
 
 // 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<FairMutex<Term<ZedListener>>>,
+    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),
+            )
+        );
     }
 }

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> {