Implemented cell for mouse pointer

Mikayla Maki created

Change summary

Cargo.lock                                         |   1 
crates/terminal/Cargo.toml                         |   1 
crates/terminal/src/terminal.rs                    | 143 +++++++++++++--
crates/terminal/src/terminal_container_view.rs     |   5 
crates/terminal/src/tests/terminal_test_context.rs |  40 ++++
5 files changed, 159 insertions(+), 31 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5391,6 +5391,7 @@ dependencies = [
  "ordered-float",
  "procinfo",
  "project",
+ "rand 0.8.5",
  "settings",
  "shellexpand",
  "smallvec",

crates/terminal/Cargo.toml 🔗

@@ -36,3 +36,4 @@ gpui = { path = "../gpui", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"]}
 project = { path = "../project", features = ["test-support"]}
 workspace = { path = "../workspace", features = ["test-support"] }
+rand = "0.8.5"

crates/terminal/src/terminal.rs 🔗

@@ -270,7 +270,6 @@ impl TerminalBuilder {
         working_directory: Option<PathBuf>,
         shell: Option<Shell>,
         env: Option<HashMap<String, String>>,
-        initial_size: TerminalSize,
         blink_settings: Option<TerminalBlink>,
         alternate_scroll: &AlternateScroll,
         window_id: usize,
@@ -310,7 +309,11 @@ impl TerminalBuilder {
         //TODO: Remove with a bounded sender which can be dispatched on &self
         let (events_tx, events_rx) = unbounded();
         //Set up the terminal...
-        let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
+        let mut term = Term::new(
+            &config,
+            &TerminalSize::default(),
+            ZedListener(events_tx.clone()),
+        );
 
         //Start off blinking if we need to
         if let Some(TerminalBlink::On) = blink_settings {
@@ -325,7 +328,11 @@ impl TerminalBuilder {
         let term = Arc::new(FairMutex::new(term));
 
         //Setup the pty...
-        let pty = match tty::new(&pty_config, initial_size.into(), window_id as u64) {
+        let pty = match tty::new(
+            &pty_config,
+            TerminalSize::default().into(),
+            window_id as u64,
+        ) {
             Ok(pty) => pty,
             Err(error) => {
                 bail!(TerminalError {
@@ -357,7 +364,6 @@ impl TerminalBuilder {
             term,
             events: VecDeque::with_capacity(10), //Should never get this high.
             last_content: Default::default(),
-            cur_size: initial_size,
             last_mouse: None,
             matches: Vec::new(),
             last_synced: Instant::now(),
@@ -453,6 +459,7 @@ pub struct TerminalContent {
     selection: Option<SelectionRange>,
     cursor: RenderableCursor,
     cursor_char: char,
+    size: TerminalSize,
 }
 
 impl Default for TerminalContent {
@@ -468,6 +475,7 @@ impl Default for TerminalContent {
                 point: Point::new(Line(0), Column(0)),
             },
             cursor_char: Default::default(),
+            size: Default::default(),
         }
     }
 }
@@ -478,7 +486,6 @@ pub struct Terminal {
     events: VecDeque<InternalEvent>,
     last_mouse: Option<(Point, AlacDirection)>,
     pub matches: Vec<RangeInclusive<Point>>,
-    cur_size: TerminalSize,
     last_content: TerminalContent,
     last_synced: Instant,
     sync_task: Option<Task<()>>,
@@ -511,7 +518,7 @@ impl Terminal {
             )),
             AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
             AlacTermEvent::TextAreaSizeRequest(format) => {
-                self.write_to_pty(format(self.cur_size.into()))
+                self.write_to_pty(format(self.last_content.size.into()))
             }
             AlacTermEvent::CursorBlinkingChange => {
                 cx.emit(Event::BlinkChanged);
@@ -580,7 +587,7 @@ impl Terminal {
                 new_size.height = f32::max(new_size.line_height, new_size.height);
                 new_size.width = f32::max(new_size.cell_width, new_size.width);
 
-                self.cur_size = new_size.clone();
+                self.last_content.size = new_size.clone();
 
                 self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
 
@@ -609,8 +616,12 @@ impl Terminal {
             }
             InternalEvent::UpdateSelection(position) => {
                 if let Some(mut selection) = term.selection.take() {
-                    let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
-                    let side = mouse_side(*position, self.cur_size);
+                    let point = mouse_point(
+                        *position,
+                        self.last_content.size,
+                        term.grid().display_offset(),
+                    );
+                    let side = mouse_side(*position, self.last_content.size);
 
                     selection.update(point, side);
                     term.selection = Some(selection);
@@ -733,11 +744,11 @@ impl Terminal {
             self.process_terminal_event(&e, &mut terminal, cx)
         }
 
-        self.last_content = Self::make_content(&terminal);
+        self.last_content = Self::make_content(&terminal, self.last_content.size);
         self.last_synced = Instant::now();
     }
 
-    fn make_content(term: &Term<ZedListener>) -> TerminalContent {
+    fn make_content(term: &Term<ZedListener>, last_size: TerminalSize) -> TerminalContent {
         let content = term.renderable_content();
         TerminalContent {
             cells: content
@@ -760,6 +771,7 @@ impl Terminal {
             selection: content.selection,
             cursor: content.cursor,
             cursor_char: term.grid()[content.cursor.point].c,
+            size: last_size,
         }
     }
 
@@ -799,8 +811,12 @@ impl Terminal {
     pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
 
-        let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
-        let side = mouse_side(position, self.cur_size);
+        let point = mouse_point(
+            position,
+            self.last_content.size,
+            self.last_content.display_offset,
+        );
+        let side = mouse_side(position, self.last_content.size);
 
         if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
             if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
@@ -825,7 +841,7 @@ impl Terminal {
                     None => return,
                 };
 
-                let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
+                let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
 
                 self.events
                     .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
@@ -837,8 +853,8 @@ impl Terminal {
 
     fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
         //TODO: Why do these need to be doubled? Probably the same problem that the IME has
-        let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
-        let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
+        let top = e.region.origin_y() + (self.last_content.size.line_height * 2.);
+        let bottom = e.region.lower_left().y() - (self.last_content.size.line_height * 2.);
         let scroll_delta = if e.position.y() < top {
             (top - e.position.y()).powf(1.1)
         } else if e.position.y() > bottom {
@@ -851,8 +867,12 @@ impl Terminal {
 
     pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
-        let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
-        let side = mouse_side(position, self.cur_size);
+        let point = mouse_point(
+            position,
+            self.last_content.size,
+            self.last_content.display_offset,
+        );
+        let side = mouse_side(position, self.last_content.size);
 
         if self.mouse_mode(e.shift) {
             if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
@@ -870,8 +890,12 @@ impl Terminal {
         let position = e.position.sub(origin);
 
         if !self.mouse_mode(e.shift) {
-            let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
-            let side = mouse_side(position, self.cur_size);
+            let point = mouse_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
+            let side = mouse_side(position, self.last_content.size);
 
             let selection_type = match e.click_count {
                 0 => return, //This is a release
@@ -894,7 +918,11 @@ impl Terminal {
     pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
         if self.mouse_mode(e.shift) {
-            let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
+            let point = mouse_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
 
             if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
                 self.pty_tx.notify(bytes);
@@ -915,7 +943,7 @@ impl Terminal {
             if mouse_mode {
                 let point = mouse_point(
                     e.position.sub(origin),
-                    self.cur_size,
+                    self.last_content.size,
                     self.last_content.display_offset,
                 );
 
@@ -954,20 +982,22 @@ impl Terminal {
             }
             /* Calculate the appropriate scroll lines */
             Some(gpui::TouchPhase::Moved) => {
-                let old_offset = (self.scroll_px / self.cur_size.line_height) as i32;
+                let old_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
 
                 self.scroll_px += e.delta.y() * scroll_multiplier;
 
-                let new_offset = (self.scroll_px / self.cur_size.line_height) as i32;
+                let new_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
 
                 // Whenever we hit the edges, reset our stored scroll to 0
                 // so we can respond to changes in direction quickly
-                self.scroll_px %= self.cur_size.height;
+                self.scroll_px %= self.last_content.size.height;
 
                 Some(new_offset - old_offset)
             }
             /* Fall back to delta / line_height */
-            None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32),
+            None => Some(
+                ((e.delta.y() * scroll_multiplier) / self.last_content.size.line_height) as i32,
+            ),
             _ => None,
         }
     }
@@ -1043,7 +1073,68 @@ fn all_search_matches<'a, T>(
     RegexIter::new(start, end, AlacDirection::Right, term, regex)
 }
 
+fn cell_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> &'a IndexedCell {
+    fn pos_to_viewport(pos: Vector2F, size: TerminalSize) -> Point {
+        Point {
+            line: Line((pos.x() / size.cell_width()) as i32),
+            column: Column((pos.y() / size.line_height()) as usize),
+        }
+    }
+
+    fn cell_for_pos<'a>(point: Point, content: &'a TerminalContent) -> &'a IndexedCell {
+        dbg!(point.line.0, content.size.columns(), point.column.0);
+        debug_assert!(point.line.0.is_positive() || point.line.0 == 0);
+        &content.cells[(point.line.0 as usize * content.size.columns() + point.column.0)]
+    }
+
+    cell_for_pos(pos_to_viewport(pos, content.size), &content)
+}
+
 #[cfg(test)]
 mod tests {
+    use gpui::geometry::vector::vec2f;
+    use rand::{thread_rng, Rng};
+
+    use crate::cell_for_mouse;
+
+    use self::terminal_test_context::TerminalTestContext;
+
     pub mod terminal_test_context;
+
+    #[test]
+    fn test_mouse_to_cell() {
+        let mut rng = thread_rng();
+
+        for _ in 0..10 {
+            let viewport_cells = rng.gen_range(5..50);
+            let cell_size = rng.gen_range(5.0..20.0);
+
+            let size = crate::TerminalSize {
+                cell_width: cell_size,
+                line_height: cell_size,
+                height: cell_size * (viewport_cells as f32),
+                width: cell_size * (viewport_cells as f32),
+            };
+
+            let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng);
+
+            for i in 0..viewport_cells {
+                let i = i as usize;
+                for j in 0..viewport_cells {
+                    let j = j as usize;
+                    let min_row = i as f32 * cell_size;
+                    let max_row = (i + 1) as f32 * cell_size;
+                    let min_col = j as f32 * cell_size;
+                    let max_col = (j + 1) as f32 * cell_size;
+
+                    let mouse_pos = vec2f(
+                        rng.gen_range(min_row..max_row),
+                        rng.gen_range(min_col..max_col),
+                    );
+
+                    assert_eq!(cell_for_mouse(mouse_pos, &content).c, cells[i][j]);
+                }
+            }
+        }
+    }
 }

crates/terminal/src/terminal_container_view.rs 🔗

@@ -11,7 +11,6 @@ use util::truncate_and_trailoff;
 use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
 use workspace::{Item, ItemEvent, ToolbarItemLocation, Workspace};
 
-use crate::TerminalSize;
 use project::{LocalWorktree, Project, ProjectPath};
 use settings::{AlternateScroll, Settings, WorkingDirectory};
 use smallvec::SmallVec;
@@ -87,9 +86,6 @@ impl TerminalContainer {
         modal: bool,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        //The exact size here doesn't matter, the terminal will be resized on the first layout
-        let size_info = TerminalSize::default();
-
         let settings = cx.global::<Settings>();
         let shell = settings.terminal_overrides.shell.clone();
         let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
@@ -111,7 +107,6 @@ impl TerminalContainer {
             working_directory.clone(),
             shell,
             envs,
-            size_info,
             settings.terminal_overrides.blinking.clone(),
             scroll,
             cx.window_id(),

crates/terminal/src/tests/terminal_test_context.rs 🔗

@@ -1,10 +1,17 @@
 use std::{path::Path, time::Duration};
 
+use alacritty_terminal::{
+    index::{Column, Line, Point},
+    term::cell::Cell,
+};
 use gpui::{ModelHandle, TestAppContext, ViewHandle};
 
 use project::{Entry, Project, ProjectPath, Worktree};
+use rand::{rngs::ThreadRng, Rng};
 use workspace::{AppState, Workspace};
 
+use crate::{IndexedCell, TerminalContent, TerminalSize};
+
 pub struct TerminalTestContext<'a> {
     pub cx: &'a mut TestAppContext,
 }
@@ -86,6 +93,39 @@ impl<'a> TerminalTestContext<'a> {
             project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
         });
     }
+
+    pub fn create_terminal_content(
+        size: TerminalSize,
+        rng: &mut ThreadRng,
+    ) -> (TerminalContent, Vec<Vec<char>>) {
+        let mut ic = Vec::new();
+        let mut cells = Vec::new();
+
+        for row in 0..((size.height() / size.line_height()) as usize) {
+            let mut row_vec = Vec::new();
+            for col in 0..((size.width() / size.cell_width()) as usize) {
+                let cell_char = rng.gen();
+                ic.push(IndexedCell {
+                    point: Point::new(Line(row as i32), Column(col)),
+                    cell: Cell {
+                        c: cell_char,
+                        ..Default::default()
+                    },
+                });
+                row_vec.push(cell_char)
+            }
+            cells.push(row_vec)
+        }
+
+        (
+            TerminalContent {
+                cells: ic,
+                size,
+                ..Default::default()
+            },
+            cells,
+        )
+    }
 }
 
 impl<'a> Drop for TerminalTestContext<'a> {