From 4dbded3f0209f58a042bfa7567602ee2a25a61b6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 14 Sep 2022 15:41:55 -0700 Subject: [PATCH 01/32] Implemented cell for mouse pointer --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 1 + crates/terminal/src/terminal.rs | 143 ++++++++++++++---- .../terminal/src/terminal_container_view.rs | 5 - .../src/tests/terminal_test_context.rs | 40 +++++ 5 files changed, 159 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 634ef452a369b142c983302bcb471c00395e1a19..141a542bd2fea5e6232ad8b569a67a783cb69af1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5391,6 +5391,7 @@ dependencies = [ "ordered-float", "procinfo", "project", + "rand 0.8.5", "settings", "shellexpand", "smallvec", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 7831be1c5af47f1dd9e80bac7f5be5d070e21e51..eb1bc560176bf06a3470936aafba60ab86cff786 100644 --- a/crates/terminal/Cargo.toml +++ b/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" diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a93dd096705d664df6021d37acf8d6902f6070f1..b9c8021d0099cb8c7ce27efbb67ade18767b4a69 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -270,7 +270,6 @@ impl TerminalBuilder { working_directory: Option, shell: Option, env: Option>, - initial_size: TerminalSize, blink_settings: Option, 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, 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, last_mouse: Option<(Point, AlacDirection)>, pub matches: Vec>, - cur_size: TerminalSize, last_content: TerminalContent, last_synced: Instant, sync_task: Option>, @@ -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) -> TerminalContent { + fn make_content(term: &Term, 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 { //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]); + } + } + } + } } diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index 1aebd1f5e7a06200594df31bd4bc2dd6fc42b356..e0fe6ef6cbf275d768970da9d926b1adda7b509f 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/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 { - //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::(); 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(), diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index bee78e3ce0b3feeef432215ee6e02d5213668223..d2b5e2aaa68151119b300cc7eb67d84eeaa4b1fa 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/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>) { + 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> { From ac390745a73fae22f20c012b540c4c7e3ddf2758 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 16 Sep 2022 19:35:18 -0700 Subject: [PATCH 02/32] WIP hyperlinks --- crates/terminal/src/mappings/mouse.rs | 6 +- crates/terminal/src/terminal.rs | 117 ++++++++++++++++---------- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/crates/terminal/src/mappings/mouse.rs b/crates/terminal/src/mappings/mouse.rs index c90cbb5cd3887f9ee650817bfe0816e9a611f8fa..e9d163416558075c66128056fdf27265ef665940 100644 --- a/crates/terminal/src/mappings/mouse.rs +++ b/crates/terminal/src/mappings/mouse.rs @@ -201,7 +201,7 @@ pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal:: } } -pub fn mouse_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point { +pub fn grid_point(pos: Vector2F, cur_size: TerminalSize, display_offset: usize) -> Point { let col = pos.x() / cur_size.cell_width; let col = min(GridCol(col as usize), cur_size.last_column()); let line = pos.y() / cur_size.line_height; @@ -294,7 +294,7 @@ fn sgr_mouse_report(point: Point, button: u8, pressed: bool) -> String { #[cfg(test)] mod test { - use crate::mappings::mouse::mouse_point; + use crate::mappings::mouse::grid_point; #[test] fn test_mouse_to_selection() { @@ -316,7 +316,7 @@ mod test { 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 mouse_pos = mouse_pos - origin; - let point = mouse_point(mouse_pos, cur_size, 0); + let point = grid_point(mouse_pos, cur_size, 0); assert_eq!( point, alacritty_terminal::index::Point::new( diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b9c8021d0099cb8c7ce27efbb67ade18767b4a69..5049008c37ba9dc65ec0c34344dbcde4a96e3358 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -30,7 +30,7 @@ use futures::{ }; use mappings::mouse::{ - alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report, + alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report, }; use modal::deploy_modal; @@ -616,7 +616,7 @@ impl Terminal { } InternalEvent::UpdateSelection(position) => { if let Some(mut selection) = term.selection.take() { - let point = mouse_point( + let point = grid_point( *position, self.last_content.size, term.grid().display_offset(), @@ -809,18 +809,27 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - let position = e.position.sub(origin); + if self.mouse_mode(e.shift) { + let position = e.position.sub(origin); - let point = mouse_point( - position, - self.last_content.size, - self.last_content.display_offset, - ); - let side = mouse_side(position, self.last_content.size); + let point = grid_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) { - self.pty_tx.notify(bytes); + if self.mouse_changed(point, side) { + if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) { + self.pty_tx.notify(bytes); + } + } + } else { + if let Some(link) = cell_for_mouse(e.position, &self.last_content) + .cell + .hyperlink() + { + link.uri() } } } @@ -867,7 +876,7 @@ impl Terminal { pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); - let point = mouse_point( + let point = grid_point( position, self.last_content.size, self.last_content.display_offset, @@ -890,7 +899,7 @@ impl Terminal { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { - let point = mouse_point( + let point = grid_point( position, self.last_content.size, self.last_content.display_offset, @@ -918,7 +927,7 @@ 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( + let point = grid_point( position, self.last_content.size, self.last_content.display_offset, @@ -941,7 +950,7 @@ impl Terminal { if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) { if mouse_mode { - let point = mouse_point( + let point = grid_point( e.position.sub(origin), self.last_content.size, self.last_content.display_offset, @@ -1046,24 +1055,6 @@ fn make_selection(range: &RangeInclusive) -> Selection { selection } -/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches() -/// Iterate over all visible regex matches. -// fn visible_search_matches<'a, T>( -// term: &'a Term, -// regex: &'a RegexSearch, -// ) -> impl Iterator + 'a { -// let viewport_start = Line(-(term.grid().display_offset() as i32)); -// let viewport_end = viewport_start + term.bottommost_line(); -// let mut start = term.line_search_left(Point::new(viewport_start, Column(0))); -// let mut end = term.line_search_right(Point::new(viewport_end, Column(0))); -// start.line = start.line.max(viewport_start - MAX_SEARCH_LINES); -// end.line = end.line.min(viewport_end + MAX_SEARCH_LINES); - -// RegexIter::new(start, end, AlacDirection::Right, term, regex) -// .skip_while(move |rm| rm.end().line < viewport_start) -// .take_while(move |rm| rm.start().line <= viewport_end) -// } - fn all_search_matches<'a, T>( term: &'a Term, regex: &'a RegexSearch, @@ -1074,20 +1065,56 @@ fn all_search_matches<'a, T>( } 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), + let point = Point { + line: Line((pos.x() / content.size.cell_width()) as i32), + column: Column((pos.y() / content.size.line_height()) as usize), + }; + + debug_assert!(point.line.0.is_positive() || point.line.0 == 0); + &content.cells[(point.line.0 as usize * content.size.columns() + point.column.0)] +} + +fn open_uri(uri: String) { + // MacOS command is 'open' + pub fn spawn_daemon( + program: &str, + args: I, + master_fd: RawFd, + shell_pid: u32, + ) -> io::Result<()> + where + I: IntoIterator + Copy, + S: AsRef, + { + let mut command = Command::new(program); + command + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + if let Ok(cwd) = foreground_process_path(master_fd, shell_pid) { + command.current_dir(cwd); } - } + unsafe { + command + .pre_exec(|| { + match libc::fork() { + -1 => return Err(io::Error::last_os_error()), + 0 => (), + _ => libc::_exit(0), + } - 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)] - } + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } - cell_for_pos(pos_to_viewport(pos, content.size), &content) + Ok(()) + }) + .spawn()? + .wait() + .map(|_| ()) + } + } } #[cfg(test)] From 1993a870e183e8f9b3be08d7223e8bb8d5a7fb46 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sun, 18 Sep 2022 23:33:06 -0700 Subject: [PATCH 03/32] Hyperlink clicking is working --- crates/terminal/src/terminal.rs | 165 +++++++++++++++++--------------- 1 file changed, 88 insertions(+), 77 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 5049008c37ba9dc65ec0c34344dbcde4a96e3358..46fcb403110e71b5f49591ff8c7809478330140f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -14,7 +14,7 @@ use alacritty_terminal::{ selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, term::{ - cell::Cell, + cell::{Cell, Hyperlink}, color::Rgb, search::{Match, RegexIter, RegexSearch}, RenderableCursor, TermMode, @@ -36,13 +36,17 @@ use modal::deploy_modal; use procinfo::LocalProcessInfo; use settings::{AlternateScroll, Settings, Shell, TerminalBlink}; +use util::ResultExt; use std::{ + cmp::min, collections::{HashMap, VecDeque}, fmt::Display, + io, ops::{Deref, RangeInclusive, Sub}, - os::unix::prelude::AsRawFd, + os::unix::{prelude::AsRawFd, process::CommandExt}, path::PathBuf, + process::Command, sync::Arc, time::{Duration, Instant}, }; @@ -374,6 +378,7 @@ impl TerminalBuilder { foreground_process_info: None, breadcrumb_text: String::new(), scroll_px: 0., + last_hovered_hyperlink: None, }; Ok(TerminalBuilder { @@ -488,6 +493,7 @@ pub struct Terminal { pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, + last_hovered_hyperlink: Option, sync_task: Option>, selection_head: Option, breadcrumb_text: String, @@ -809,9 +815,10 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - if self.mouse_mode(e.shift) { - let position = e.position.sub(origin); + self.last_hovered_hyperlink = None; + let position = e.position.sub(origin); + if self.mouse_mode(e.shift) { let point = grid_point( position, self.last_content.size, @@ -824,13 +831,10 @@ impl Terminal { self.pty_tx.notify(bytes); } } - } else { - if let Some(link) = cell_for_mouse(e.position, &self.last_content) + } else if e.cmd { + self.last_hovered_hyperlink = cell_for_mouse(e.position, &self.last_content) .cell - .hyperlink() - { - link.uri() - } + .hyperlink(); } } @@ -897,29 +901,34 @@ impl Terminal { pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); - if !self.mouse_mode(e.shift) { - let point = grid_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 - 1 => Some(SelectionType::Simple), - 2 => Some(SelectionType::Semantic), - 3 => Some(SelectionType::Lines), - _ => None, - }; + if e.cmd { + if let Some(link) = cell_for_mouse(position, &self.last_content).hyperlink() { + open_uri(link.uri()).log_err(); + } + } else { + let point = grid_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 + 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)); + let selection = selection_type + .map(|selection_type| Selection::new(selection_type, point, side)); - if let Some(sel) = selection { - self.events - .push_back(InternalEvent::SetSelection(Some((sel, point)))); + if let Some(sel) = selection { + self.events + .push_back(InternalEvent::SetSelection(Some((sel, point)))); + } } } } @@ -1065,55 +1074,40 @@ fn all_search_matches<'a, T>( } fn cell_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> &'a IndexedCell { - let point = Point { - line: Line((pos.x() / content.size.cell_width()) as i32), - column: Column((pos.y() / content.size.line_height()) as usize), - }; - - debug_assert!(point.line.0.is_positive() || point.line.0 == 0); - &content.cells[(point.line.0 as usize * content.size.columns() + point.column.0)] + let col = min( + (pos.x() / content.size.cell_width()) as usize, + content.size.columns() - 1, + ) as usize; + let line = min( + (pos.y() / content.size.line_height()) as usize, + content.size.screen_lines() - 1, + ) as usize; + + &content.cells[(line * content.size.columns() + col)] } -fn open_uri(uri: String) { - // MacOS command is 'open' - pub fn spawn_daemon( - program: &str, - args: I, - master_fd: RawFd, - shell_pid: u32, - ) -> io::Result<()> - where - I: IntoIterator + Copy, - S: AsRef, - { - let mut command = Command::new(program); +fn open_uri(uri: &str) -> Result<(), std::io::Error> { + let mut command = Command::new("open"); + command.arg(uri); + + unsafe { command - .args(args) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - if let Ok(cwd) = foreground_process_path(master_fd, shell_pid) { - command.current_dir(cwd); - } - unsafe { - command - .pre_exec(|| { - match libc::fork() { - -1 => return Err(io::Error::last_os_error()), - 0 => (), - _ => libc::_exit(0), - } + .pre_exec(|| { + match libc::fork() { + -1 => return Err(io::Error::last_os_error()), + 0 => (), + _ => libc::_exit(0), + } - if libc::setsid() == -1 { - return Err(io::Error::last_os_error()); - } + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } - Ok(()) - }) - .spawn()? - .wait() - .map(|_| ()) - } + Ok(()) + }) + .spawn()? + .wait() + .map(|_| ()) } } @@ -1145,9 +1139,9 @@ mod tests { let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng); - for i in 0..viewport_cells { + for i in 0..(viewport_cells - 1) { let i = i as usize; - for j in 0..viewport_cells { + for j in 0..(viewport_cells - 1) { let j = j as usize; let min_row = i as f32 * cell_size; let max_row = (i + 1) as f32 * cell_size; @@ -1159,9 +1153,26 @@ mod tests { rng.gen_range(min_col..max_col), ); - assert_eq!(cell_for_mouse(mouse_pos, &content).c, cells[i][j]); + assert_eq!(cell_for_mouse(mouse_pos, &content).c, cells[j][i]); } } } } + + #[test] + fn test_mouse_to_cell_clamp() { + let mut rng = thread_rng(); + + let size = crate::TerminalSize { + cell_width: 10., + line_height: 10., + height: 100., + width: 100., + }; + + let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng); + + assert_eq!(cell_for_mouse(vec2f(-10., -10.), &content).c, cells[0][0]); + assert_eq!(cell_for_mouse(vec2f(1000., 1000.), &content).c, cells[9][9]); + } } From 9f81f39f510d4f77a7417214e891f9f0489d88cb Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 19 Sep 2022 09:07:41 -0700 Subject: [PATCH 04/32] WIP Hyperlinks --- crates/terminal/src/terminal.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 46fcb403110e71b5f49591ff8c7809478330140f..9da2831740578f7f5320b70861a9d8f6cf0cfb47 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -43,7 +43,7 @@ use std::{ collections::{HashMap, VecDeque}, fmt::Display, io, - ops::{Deref, RangeInclusive, Sub}, + ops::{Deref, Range, RangeInclusive, Sub}, os::unix::{prelude::AsRawFd, process::CommandExt}, path::PathBuf, process::Command, @@ -493,7 +493,7 @@ pub struct Terminal { pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, - last_hovered_hyperlink: Option, + last_hovered_hyperlink: Option<(Hyperlink, Range)>, sync_task: Option>, selection_head: Option, breadcrumb_text: String, @@ -832,7 +832,7 @@ impl Terminal { } } } else if e.cmd { - self.last_hovered_hyperlink = cell_for_mouse(e.position, &self.last_content) + let hyperlink = cell_for_mouse(e.position, &self.last_content) .cell .hyperlink(); } From b8f362fd843c50c2a76eb21595a8566e85d234f6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 19 Sep 2022 13:41:35 -0700 Subject: [PATCH 05/32] WIP hyperlink detection --- crates/terminal/src/terminal.rs | 81 +++++++++++++++++++++---- crates/terminal/src/terminal_element.rs | 25 +++----- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9da2831740578f7f5320b70861a9d8f6cf0cfb47..e67f3bdbb0e73bb040aa4f36d4f2f70934ab4b69 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -83,6 +83,11 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; +/// Copied from alacritty's ui_config.rs +const URL_REGEX: &str = + "(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ + [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+"; + ///Upward flowing events, for changing the title and such #[derive(Clone, Copy, Debug)] pub enum Event { @@ -105,6 +110,7 @@ enum InternalEvent { ScrollToPoint(Point), SetSelection(Option<(Selection, Point)>), UpdateSelection(Vector2F), + HyperlinkHover(Vector2F), Copy, } @@ -643,6 +649,16 @@ impl Terminal { } } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), + InternalEvent::HyperlinkHover(position) => { + let point = grid_point( + *position, + self.last_content.size, + term.grid().display_offset(), + ); + let side = mouse_side(*position, self.last_content.size); + + println!("Hyperlink hover") + } } } @@ -817,7 +833,6 @@ impl Terminal { pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { self.last_hovered_hyperlink = None; let position = e.position.sub(origin); - if self.mouse_mode(e.shift) { let point = grid_point( position, @@ -832,9 +847,39 @@ impl Terminal { } } } else if e.cmd { - let hyperlink = cell_for_mouse(e.position, &self.last_content) - .cell - .hyperlink(); + let content_index = content_index_for_mouse(position, &self.last_content); + let link = self.last_content.cells[content_index].hyperlink(); + + if link.is_some() { + let mut min_index = content_index; + loop { + if self.last_content.cells[min_index - 1].hyperlink() == link { + min_index = min_index - 1; + } else { + break; + } + } + + let mut max_index = content_index; + loop { + if self.last_content.cells[max_index + 1].hyperlink() == link { + max_index = max_index + 1; + } else { + break; + } + } + + self.last_hovered_hyperlink = link.map(|link| { + ( + link, + self.last_content.cells[min_index].point + ..self.last_content.cells[max_index].point, + ) + }); + } else { + self.events + .push_back(InternalEvent::HyperlinkHover(position)); + } } } @@ -903,7 +948,12 @@ impl Terminal { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { if e.cmd { - if let Some(link) = cell_for_mouse(position, &self.last_content).hyperlink() { + if let Some(link) = self.last_content.cells + [content_index_for_mouse(position, &self.last_content)] + .hyperlink() + { + dbg!(&link); + dbg!(&self.last_hovered_hyperlink); open_uri(link.uri()).log_err(); } } else { @@ -1073,7 +1123,7 @@ 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 content_index_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> usize { let col = min( (pos.x() / content.size.cell_width()) as usize, content.size.columns() - 1, @@ -1083,7 +1133,7 @@ fn cell_for_mouse<'a>(pos: Vector2F, content: &'a TerminalContent) -> &'a Indexe content.size.screen_lines() - 1, ) as usize; - &content.cells[(line * content.size.columns() + col)] + line * content.size.columns() + col } fn open_uri(uri: &str) -> Result<(), std::io::Error> { @@ -1116,7 +1166,7 @@ mod tests { use gpui::geometry::vector::vec2f; use rand::{thread_rng, Rng}; - use crate::cell_for_mouse; + use crate::content_index_for_mouse; use self::terminal_test_context::TerminalTestContext; @@ -1153,7 +1203,10 @@ mod tests { rng.gen_range(min_col..max_col), ); - assert_eq!(cell_for_mouse(mouse_pos, &content).c, cells[j][i]); + assert_eq!( + content.cells[content_index_for_mouse(mouse_pos, &content)].c, + cells[j][i] + ); } } } @@ -1172,7 +1225,13 @@ mod tests { let (content, cells) = TerminalTestContext::create_terminal_content(size, &mut rng); - assert_eq!(cell_for_mouse(vec2f(-10., -10.), &content).c, cells[0][0]); - assert_eq!(cell_for_mouse(vec2f(1000., 1000.), &content).c, cells[9][9]); + assert_eq!( + content.cells[content_index_for_mouse(vec2f(-10., -10.), &content)].c, + cells[0][0] + ); + assert_eq!( + content.cells[content_index_for_mouse(vec2f(1000., 1000.), &content)].c, + cells[9][9] + ); } } diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 51ebf63e5e41dd3caccd397119bc881a5710613c..52949ef213f4592272872d51064a84176261b952 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -427,6 +427,16 @@ impl TerminalElement { position: e.position, }); } + }) + .on_move(move |event, cx| { + if cx.is_parent_view_focused() { + if let Some(conn_handle) = connection.upgrade(cx.app) { + conn_handle.update(cx.app, |terminal, cx| { + terminal.mouse_move(&event, origin); + cx.notify(); + }) + } + } }); // Mouse mode handlers: @@ -474,21 +484,6 @@ impl TerminalElement { ), ) } - //Mouse move manages both dragging and motion events - if mode.intersects(TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION) { - region = region - //TODO: This does not fire on right-mouse-down-move events. - .on_move(move |event, cx| { - if cx.is_parent_view_focused() { - if let Some(conn_handle) = connection.upgrade(cx.app) { - conn_handle.update(cx.app, |terminal, cx| { - terminal.mouse_move(&event, origin); - cx.notify(); - }) - } - } - }) - } cx.scene.push_mouse_region(region); } From b3fafec20c96070e872fc57aebde51488a628f5b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Sep 2022 15:43:07 -0700 Subject: [PATCH 06/32] 0.54.0 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05701b7c56009e791c54b44d3e1917841b9394db..4e558def1362b12de879b5f9761b527477605ede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7127,7 +7127,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.53.1" +version = "0.54.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 446139eaafbd25c8f4c4374b0f6538ee44586b58..fe73513d42dfd416c0d9a4a3706c889e9757c019 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.53.1" +version = "0.54.0" [lib] name = "zed" From e0635a3ed87617319b54c5524ff2b0847052e181 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 19 Sep 2022 17:05:10 -0700 Subject: [PATCH 07/32] Fixed autoscroll jump on 4-click --- crates/editor/src/editor.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c60abc187a001997ffead3cfef251c5db6e74dac..a7d46f771046a0067b31a9aeb1a8e6d6c926a98b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1575,17 +1575,20 @@ impl Editor { let start; let end; let mode; + let auto_scroll; match click_count { 1 => { start = buffer.anchor_before(position.to_point(&display_map)); end = start.clone(); mode = SelectMode::Character; + auto_scroll = true; } 2 => { let range = movement::surrounding_word(&display_map, position); start = buffer.anchor_before(range.start.to_point(&display_map)); end = buffer.anchor_before(range.end.to_point(&display_map)); mode = SelectMode::Word(start.clone()..end.clone()); + auto_scroll = true; } 3 => { let position = display_map @@ -1599,15 +1602,17 @@ impl Editor { start = buffer.anchor_before(line_start); end = buffer.anchor_before(next_line_start); mode = SelectMode::Line(start.clone()..end.clone()); + auto_scroll = true; } _ => { start = buffer.anchor_before(0); end = buffer.anchor_before(buffer.len()); mode = SelectMode::All; + auto_scroll = false; } } - self.change_selections(Some(Autoscroll::Newest), cx, |s| { + self.change_selections(auto_scroll.then(|| Autoscroll::Newest), cx, |s| { if !add { s.clear_disjoint(); } else if click_count > 1 { From ba32dcbb88d401c59060856714c82159b4625f08 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 19 Sep 2022 17:19:03 -0700 Subject: [PATCH 08/32] Reworking hyperlink events --- crates/terminal/src/terminal.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e67f3bdbb0e73bb040aa4f36d4f2f70934ab4b69..d1e417c9b80c85ea322e0f57f8dc85d17b1f8658 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -110,7 +110,7 @@ enum InternalEvent { ScrollToPoint(Point), SetSelection(Option<(Selection, Point)>), UpdateSelection(Vector2F), - HyperlinkHover(Vector2F), + Hyperlink(Vector2F, bool), Copy, } @@ -649,7 +649,7 @@ impl Terminal { } } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), - InternalEvent::HyperlinkHover(position) => { + InternalEvent::Hyperlink(position, _hover) => { let point = grid_point( *position, self.last_content.size, @@ -657,7 +657,7 @@ impl Terminal { ); let side = mouse_side(*position, self.last_content.size); - println!("Hyperlink hover") + println!("Hyperlink hover | click ") } } } @@ -878,7 +878,7 @@ impl Terminal { }); } else { self.events - .push_back(InternalEvent::HyperlinkHover(position)); + .push_back(InternalEvent::Hyperlink(position, false)); } } } @@ -955,6 +955,9 @@ impl Terminal { dbg!(&link); dbg!(&self.last_hovered_hyperlink); open_uri(link.uri()).log_err(); + } else { + self.events + .push_back(InternalEvent::Hyperlink(position, true)); } } else { let point = grid_point( From b3202c382dd9290956e912674cd097462abb917e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 19 Sep 2022 17:21:24 -0700 Subject: [PATCH 09/32] WI{ --- crates/terminal/src/terminal.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d1e417c9b80c85ea322e0f57f8dc85d17b1f8658..72aa28cb808c17e0ff8b9118e52b10a27d716267 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -110,6 +110,7 @@ enum InternalEvent { ScrollToPoint(Point), SetSelection(Option<(Selection, Point)>), UpdateSelection(Vector2F), + // Adjusted mouse position, should open Hyperlink(Vector2F, bool), Copy, } @@ -649,7 +650,7 @@ impl Terminal { } } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), - InternalEvent::Hyperlink(position, _hover) => { + InternalEvent::Hyperlink(position, open) => { let point = grid_point( *position, self.last_content.size, From f706cbe1432f65033b7aad41421974863ff72be5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 20 Sep 2022 11:20:57 -0700 Subject: [PATCH 10/32] WIP hyperlink searching --- crates/terminal/src/terminal.rs | 53 ++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 72aa28cb808c17e0ff8b9118e52b10a27d716267..bf5c98d03cb1a64d9e482a4d73cd612c678f954f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -14,7 +14,7 @@ use alacritty_terminal::{ selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, term::{ - cell::{Cell, Hyperlink}, + cell::Cell, color::Rgb, search::{Match, RegexIter, RegexSearch}, RenderableCursor, TermMode, @@ -43,7 +43,7 @@ use std::{ collections::{HashMap, VecDeque}, fmt::Display, io, - ops::{Deref, Range, RangeInclusive, Sub}, + ops::{Deref, RangeInclusive, Sub}, os::unix::{prelude::AsRawFd, process::CommandExt}, path::PathBuf, process::Command, @@ -77,16 +77,14 @@ pub fn init(cx: &mut MutableAppContext) { ///Scroll multiplier that is set to 3 by default. This will be removed when I ///Implement scroll bars. const SCROLL_MULTIPLIER: f32 = 4.; -// const MAX_SEARCH_LINES: usize = 100; +const MAX_SEARCH_LINES: usize = 100; const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; /// Copied from alacritty's ui_config.rs -const URL_REGEX: &str = - "(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ - [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+"; +static URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); ///Upward flowing events, for changing the title and such #[derive(Clone, Copy, Debug)] @@ -500,7 +498,7 @@ pub struct Terminal { pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, - last_hovered_hyperlink: Option<(Hyperlink, Range)>, + last_hovered_hyperlink: Option<(String, RangeInclusive)>, sync_task: Option>, selection_head: Option, breadcrumb_text: String, @@ -651,14 +649,23 @@ impl Terminal { } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), InternalEvent::Hyperlink(position, open) => { + self.last_hovered_hyperlink = None; + let point = grid_point( *position, self.last_content.size, term.grid().display_offset(), ); - let side = mouse_side(*position, self.last_content.size); - println!("Hyperlink hover | click ") + if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) { + let url = term.bounds_to_string(*url_match.start(), *url_match.end()); + + if *open { + open_uri(&url).log_err(); + } else { + self.last_hovered_hyperlink = Some((url, url_match)); + } + } } } } @@ -872,9 +879,9 @@ impl Terminal { self.last_hovered_hyperlink = link.map(|link| { ( - link, + link.uri().to_owned(), self.last_content.cells[min_index].point - ..self.last_content.cells[max_index].point, + ..=self.last_content.cells[max_index].point, ) }); } else { @@ -1112,6 +1119,30 @@ impl Entity for Terminal { type Event = Event; } +/// Based on alacritty/src/display/hint.rs > regex_match_at +/// Retrieve the match, if the specified point is inside the content matching the regex. +fn regex_match_at(term: &Term, point: Point, regex: &RegexSearch) -> Option { + visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point)) +} + +/// Copied from alacritty/src/display/hint.rs: +/// Iterate over all visible regex matches. +pub fn visible_regex_match_iter<'a, T>( + term: &'a Term, + regex: &'a RegexSearch, +) -> impl Iterator + 'a { + let viewport_start = Line(-(term.grid().display_offset() as i32)); + let viewport_end = viewport_start + term.bottommost_line(); + let mut start = term.line_search_left(Point::new(viewport_start, Column(0))); + let mut end = term.line_search_right(Point::new(viewport_end, Column(0))); + start.line = start.line.max(viewport_start - MAX_SEARCH_LINES); + end.line = end.line.min(viewport_end + MAX_SEARCH_LINES); + + RegexIter::new(start, end, AlacDirection::Right, term, regex) + .skip_while(move |rm| rm.end().line < viewport_start) + .take_while(move |rm| rm.start().line <= viewport_end) +} + fn make_selection(range: &RangeInclusive) -> Selection { let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left); selection.update(*range.end(), AlacDirection::Right); From a29d5dd693cf32877582d7f8587380cdb97abd6e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 2 Aug 2022 11:00:15 +0200 Subject: [PATCH 11/32] Use a pre-packaged version of the JSON language server This ensures users can still edit settings even if they don't have Node or NPM installed. --- crates/zed/src/languages/json.rs | 85 ++++++++++++++++---------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 7b6569d3362fdb1e551f194f45a1201ac8f4e7fe..118afe6ea3048911abb1f5b0a85c19c30c24c54e 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,26 +1,21 @@ -use super::installation::{npm_install_packages, npm_package_latest_version}; -use anyhow::{anyhow, Context, Result}; +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use anyhow::{anyhow, Result}; use async_trait::async_trait; use client::http::HttpClient; use collections::HashMap; use futures::StreamExt; use language::{LanguageServerName, LspAdapter}; use serde_json::json; -use smol::fs; -use std::{any::Any, path::PathBuf, sync::Arc}; +use smol::fs::{self, File}; +use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; use util::ResultExt; pub struct JsonLspAdapter; -impl JsonLspAdapter { - const BIN_PATH: &'static str = - "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; -} - #[async_trait] impl LspAdapter for JsonLspAdapter { async fn name(&self) -> LanguageServerName { - LanguageServerName("vscode-json-languageserver".into()) + LanguageServerName("json-language-server".into()) } async fn server_args(&self) -> Vec { @@ -29,28 +24,44 @@ impl LspAdapter for JsonLspAdapter { async fn fetch_latest_server_version( &self, - _: Arc, - ) -> Result> { - Ok(Box::new(npm_package_latest_version("vscode-json-languageserver").await?) as Box<_>) + http: Arc, + ) -> Result> { + let release = latest_github_release("zed-industries/json-language-server", http).await?; + let asset_name = format!("json-language-server-darwin-{}", consts::ARCH); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) } async fn fetch_server_binary( &self, version: Box, - _: Arc, + http: Arc, container_dir: PathBuf, ) -> Result { - let version = version.downcast::().unwrap(); - let version_dir = container_dir.join(version.as_str()); - fs::create_dir_all(&version_dir) - .await - .context("failed to create version directory")?; - let binary_path = version_dir.join(Self::BIN_PATH); - - if fs::metadata(&binary_path).await.is_err() { - npm_install_packages( - [("vscode-json-languageserver", version.as_str())], - &version_dir, + let version = version.downcast::().unwrap(); + let destination_path = container_dir.join(format!( + "json-language-server-{}-{}", + version.name, + consts::ARCH + )); + if fs::metadata(&destination_path).await.is_err() { + let mut response = http + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let mut file = File::create(&destination_path).await?; + futures::io::copy(response.body_mut(), &mut file).await?; + fs::set_permissions( + &destination_path, + ::from_mode(0o755), ) .await?; @@ -58,37 +69,25 @@ impl LspAdapter for JsonLspAdapter { while let Some(entry) = entries.next().await { if let Some(entry) = entry.log_err() { let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); + if entry_path.as_path() != destination_path { + fs::remove_file(&entry_path).await.log_err(); } } } } } - Ok(binary_path) + Ok(destination_path) } async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { (|| async move { - let mut last_version_dir = None; + let mut last = None; let mut entries = fs::read_dir(&container_dir).await?; while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_dir() { - last_version_dir = Some(entry.path()); - } - } - let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let bin_path = last_version_dir.join(Self::BIN_PATH); - if bin_path.exists() { - Ok(bin_path) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - last_version_dir - )) + last = Some(entry?.path()); } + last.ok_or_else(|| anyhow!("no cached binary")) })() .await .log_err() From eb71ac96049d2b796c6b9aef2e3655234bffc179 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 21 Sep 2022 09:49:18 +0200 Subject: [PATCH 12/32] Download json-language-server as a zip instead of as a binary It turns out that this lifts the code-signing limitation and lets us run arbitrary executables. --- crates/zed/src/languages/json.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 118afe6ea3048911abb1f5b0a85c19c30c24c54e..d7f87bee6c0d64cf936eedac874581e33690e25d 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,9 +1,10 @@ use super::installation::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Result}; +use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; use client::http::HttpClient; use collections::HashMap; -use futures::StreamExt; +use futures::{io::BufReader, StreamExt}; use language::{LanguageServerName, LspAdapter}; use serde_json::json; use smol::fs::{self, File}; @@ -27,7 +28,7 @@ impl LspAdapter for JsonLspAdapter { http: Arc, ) -> Result> { let release = latest_github_release("zed-industries/json-language-server", http).await?; - let asset_name = format!("json-language-server-darwin-{}", consts::ARCH); + let asset_name = format!("json-language-server-darwin-{}.gz", consts::ARCH); let asset = release .assets .iter() @@ -52,13 +53,15 @@ impl LspAdapter for JsonLspAdapter { version.name, consts::ARCH )); + if fs::metadata(&destination_path).await.is_err() { let mut response = http .get(&version.url, Default::default(), true) .await .map_err(|err| anyhow!("error downloading release: {}", err))?; + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); let mut file = File::create(&destination_path).await?; - futures::io::copy(response.body_mut(), &mut file).await?; + futures::io::copy(decompressed_bytes, &mut file).await?; fs::set_permissions( &destination_path, ::from_mode(0o755), From 9da7fd22f7d6218336107d80787dfbe4e93e1a82 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 21 Sep 2022 17:18:39 +0200 Subject: [PATCH 13/32] Set `MACOSX_DEPLOYMENT_TARGET` when running `script/bundle` This ensures that every library and binary we build doesn't assume that it's going to run on the same machine that created it. Co-Authored-By: Nathan Sobo --- script/bundle | 1 + 1 file changed, 1 insertion(+) diff --git a/script/bundle b/script/bundle index 42ed7d9244a1b6486569bfebb8354f90fc2b84fd..f3fc4e74341f24118647b1d04e796012ff84d6b5 100755 --- a/script/bundle +++ b/script/bundle @@ -3,6 +3,7 @@ set -e export ZED_BUNDLE=true +export MACOSX_DEPLOYMENT_TARGET=10.14 echo "Installing cargo bundle" cargo install cargo-bundle --version 0.5.0 From 0c4c5f92383008f7877772d8bd404dc3d4ca7a6c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 21 Sep 2022 10:36:46 -0600 Subject: [PATCH 14/32] 0.54.1 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e558def1362b12de879b5f9761b527477605ede..dad1219ddf33923c80845edef255020bd3e4813e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7127,7 +7127,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.54.0" +version = "0.54.1" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fe73513d42dfd416c0d9a4a3706c889e9757c019..7800ea74153b0a12ca2b3d6ade8c4233728ab743 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.54.0" +version = "0.54.1" [lib] name = "zed" From ab7f7b3754ed90470e2d3d68d0d4f3cc4d40408c Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 21 Sep 2022 22:23:07 -0700 Subject: [PATCH 15/32] Added on_scroll to mouse_event_handler and fixed the uniform list scroll implementation --- .../gpui/src/elements/mouse_event_handler.rs | 11 ++++- crates/gpui/src/elements/uniform_list.rs | 47 ++++++++++++------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index bb958ce99c2728eed3e0908ee040db5b0ccfaa29..5b3b9b13f6c77ae2f3fe2b517a3136e83729e4e0 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -7,7 +7,8 @@ use crate::{ platform::CursorStyle, scene::{ ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, - HandlerSet, HoverRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent, + HandlerSet, HoverRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent, UpOutRegionEvent, + UpRegionEvent, }, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext, MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View, @@ -122,6 +123,14 @@ impl MouseEventHandler { self } + pub fn on_scroll( + mut self, + handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static, + ) -> Self { + self.handlers = self.handlers.on_scroll(handler); + self + } + pub fn with_hoverable(mut self, is_hoverable: bool) -> Self { self.hoverable = is_hoverable; self diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 103cb00d8cc1f46d77d85928e95aea666c589bed..718a9fe8a242da71d512bb22d6d160a81a54589b 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -6,7 +6,8 @@ use crate::{ }, json::{self, json}, presenter::MeasurementContext, - ElementBox, RenderContext, ScrollWheelEvent, View, + scene::ScrollWheelRegionEvent, + ElementBox, MouseRegion, RenderContext, ScrollWheelEvent, View, }; use json::ToJson; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -50,6 +51,7 @@ pub struct UniformList { padding_top: f32, padding_bottom: f32, get_width_from_item: Option, + view_id: usize, } impl UniformList { @@ -77,6 +79,7 @@ impl UniformList { padding_top: 0., padding_bottom: 0., get_width_from_item: None, + view_id: cx.handle().id(), } } @@ -96,7 +99,7 @@ impl UniformList { } fn scroll( - &self, + state: UniformListState, _: Vector2F, mut delta: Vector2F, precise: bool, @@ -107,7 +110,7 @@ impl UniformList { delta *= 20.; } - let mut state = self.state.0.borrow_mut(); + let mut state = state.0.borrow_mut(); state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max); cx.notify(); @@ -283,6 +286,28 @@ impl Element for UniformList { ) -> Self::PaintState { cx.scene.push_layer(Some(bounds)); + cx.scene.push_mouse_region( + MouseRegion::new::(self.view_id, 0, bounds).on_scroll({ + let scroll_max = layout.scroll_max; + let state = self.state.clone(); + move |ScrollWheelRegionEvent { + platform_event: + ScrollWheelEvent { + position, + delta, + precise, + .. + }, + .. + }, + cx| { + if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) { + cx.propogate_event(); + } + } + }), + ); + let mut item_origin = bounds.origin() - vec2f( 0., @@ -300,7 +325,7 @@ impl Element for UniformList { fn dispatch_event( &mut self, event: &Event, - bounds: RectF, + _: RectF, _: RectF, layout: &mut Self::LayoutState, _: &mut Self::PaintState, @@ -311,20 +336,6 @@ impl Element for UniformList { handled = item.dispatch_event(event, cx) || handled; } - if let Event::ScrollWheel(ScrollWheelEvent { - position, - delta, - precise, - .. - }) = event - { - if bounds.contains_point(*position) - && self.scroll(*position, *delta, *precise, layout.scroll_max, cx) - { - handled = true; - } - } - handled } From f4d4ea41232cc55a9612ad6c66ca8983201274f5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 21 Sep 2022 23:26:42 -0700 Subject: [PATCH 16/32] WIP fixing scrollable flex --- crates/gpui/src/elements/flex.rs | 141 +++++++++++++---------- crates/gpui/src/elements/uniform_list.rs | 2 +- 2 files changed, 81 insertions(+), 62 deletions(-) diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index e68810fb360527dd2e047d12c98f75632bac6105..bc3b352b6cb141e7d8d51e397db4ff47d1fb1aa0 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -1,10 +1,10 @@ -use std::{any::Any, f32::INFINITY, ops::Range}; +use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc}; use crate::{ json::{self, ToJson, Value}, presenter::MeasurementContext, Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, - LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint, + LayoutContext, MouseRegion, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint, Vector2FExt, View, }; use pathfinder_geometry::{ @@ -13,7 +13,7 @@ use pathfinder_geometry::{ }; use serde_json::json; -#[derive(Default)] +#[derive(Default, Clone, Copy)] struct ScrollState { scroll_to: Option, scroll_position: f32, @@ -22,7 +22,7 @@ struct ScrollState { pub struct Flex { axis: Axis, children: Vec, - scroll_state: Option>, + scroll_state: Option<(ElementStateHandle>>, usize)>, } impl Flex { @@ -52,9 +52,15 @@ impl Flex { Tag: 'static, V: View, { - let scroll_state = cx.default_element_state::(element_id); - scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to); - self.scroll_state = Some(scroll_state); + let scroll_state_handle = + cx.default_element_state::>>(element_id); + let scroll_state_cell = scroll_state_handle.read(cx); + let mut scroll_state = scroll_state_cell.get(); + scroll_state.scroll_to = scroll_to; + scroll_state_cell.set(scroll_state); + + self.scroll_state = Some((scroll_state_handle, cx.handle().id())); + self } @@ -100,6 +106,38 @@ impl Flex { } } } + + fn handle_scroll( + e: ScrollWheelEvent, + axis: Axis, + scroll_state: Rc>, + remaining_space: f32, + ) -> bool { + let precise = e.precise; + let delta = e.delta; + if remaining_space < 0. { + let mut delta = match axis { + Axis::Horizontal => { + if delta.x() != 0. { + delta.x() + } else { + delta.y() + } + } + Axis::Vertical => delta.y(), + }; + if !precise { + delta *= 20.; + } + + let mut old_state = scroll_state.get(); + old_state.scroll_position -= delta; + scroll_state.set(old_state); + + return true; + } + return false; + } } impl Extend for Flex { @@ -202,9 +240,9 @@ impl Element for Flex { } if let Some(scroll_state) = self.scroll_state.as_ref() { - scroll_state.update(cx, |scroll_state, _| { - if let Some(scroll_to) = scroll_state.scroll_to.take() { - let visible_start = scroll_state.scroll_position; + scroll_state.0.update(cx, |scroll_state, _| { + if let Some(scroll_to) = scroll_state.get().scroll_to.take() { + let visible_start = scroll_state.get().scroll_position; let visible_end = visible_start + size.along(self.axis); if let Some(child) = self.children.get(scroll_to) { let child_start: f32 = self.children[..scroll_to] @@ -212,16 +250,20 @@ impl Element for Flex { .map(|c| c.size().along(self.axis)) .sum(); let child_end = child_start + child.size().along(self.axis); + + let mut old_state = scroll_state.get(); if child_start < visible_start { - scroll_state.scroll_position = child_start; + old_state.scroll_position = child_start; } else if child_end > visible_end { - scroll_state.scroll_position = child_end - size.along(self.axis); + old_state.scroll_position = child_end - size.along(self.axis); } + scroll_state.set(old_state); } } - scroll_state.scroll_position = - scroll_state.scroll_position.min(-remaining_space).max(0.); + let mut old_state = scroll_state.get(); + old_state.scroll_position = old_state.scroll_position.min(-remaining_space).max(0.); + scroll_state.set(old_state); }); } @@ -242,9 +284,30 @@ impl Element for Flex { cx.scene.push_layer(Some(bounds)); } + if let Some(scroll_state) = &self.scroll_state { + cx.scene.push_mouse_region( + MouseRegion::new::(scroll_state.1, 0, bounds) + .on_scroll({ + let axis = self.axis; + let scroll_state = scroll_state.0.read(cx).clone(); + move |e, cx| { + if Self::handle_scroll( + e.platform_event, + axis, + scroll_state.clone(), + remaining_space, + ) { + cx.propogate_event(); + } + } + }) + .on_move(|_, _| { /* Eat move events so they don't propogate */ }), + ); + } + let mut child_origin = bounds.origin(); if let Some(scroll_state) = self.scroll_state.as_ref() { - let scroll_position = scroll_state.read(cx).scroll_position; + let scroll_position = scroll_state.0.read(cx).get().scroll_position; match self.axis { Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position), Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position), @@ -278,9 +341,9 @@ impl Element for Flex { fn dispatch_event( &mut self, event: &Event, - bounds: RectF, _: RectF, - remaining_space: &mut Self::LayoutState, + _: RectF, + _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, ) -> bool { @@ -288,50 +351,6 @@ impl Element for Flex { for child in &mut self.children { handled = child.dispatch_event(event, cx) || handled; } - if !handled { - if let &Event::ScrollWheel(ScrollWheelEvent { - position, - delta, - precise, - .. - }) = event - { - if *remaining_space < 0. && bounds.contains_point(position) { - if let Some(scroll_state) = self.scroll_state.as_ref() { - scroll_state.update(cx, |scroll_state, cx| { - let mut delta = match self.axis { - Axis::Horizontal => { - if delta.x() != 0. { - delta.x() - } else { - delta.y() - } - } - Axis::Vertical => delta.y(), - }; - if !precise { - delta *= 20.; - } - - scroll_state.scroll_position -= delta; - - handled = true; - cx.notify(); - }); - } - } - } - } - - if !handled { - if let &Event::MouseMoved(MouseMovedEvent { position, .. }) = event { - // If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent - // propogating it to the element below. - if self.scroll_state.is_some() && bounds.contains_point(position) { - handled = true; - } - } - } handled } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 718a9fe8a242da71d512bb22d6d160a81a54589b..6bc35c06922638c8221805d7a945b0c5f747ec65 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -287,7 +287,7 @@ impl Element for UniformList { cx.scene.push_layer(Some(bounds)); cx.scene.push_mouse_region( - MouseRegion::new::(self.view_id, 0, bounds).on_scroll({ + MouseRegion::new::(self.view_id, 0, visible_bounds).on_scroll({ let scroll_max = layout.scroll_max; let state = self.state.clone(); move |ScrollWheelRegionEvent { From dd7259c83288cb3cd5de5ae4bf4a35dfbb254447 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 22 Sep 2022 09:35:52 -0700 Subject: [PATCH 17/32] Finished fixing flex scrolls --- crates/gpui/src/elements/flex.rs | 115 +++++++++++++------------------ 1 file changed, 47 insertions(+), 68 deletions(-) diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index bc3b352b6cb141e7d8d51e397db4ff47d1fb1aa0..227f946ac6529e9d4e33f8acb5c535cc1805699d 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -4,8 +4,7 @@ use crate::{ json::{self, ToJson, Value}, presenter::MeasurementContext, Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, - LayoutContext, MouseRegion, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint, - Vector2FExt, View, + LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View, }; use pathfinder_geometry::{ rect::RectF, @@ -13,16 +12,16 @@ use pathfinder_geometry::{ }; use serde_json::json; -#[derive(Default, Clone, Copy)] +#[derive(Default)] struct ScrollState { - scroll_to: Option, - scroll_position: f32, + scroll_to: Cell>, + scroll_position: Cell, } pub struct Flex { axis: Axis, children: Vec, - scroll_state: Option<(ElementStateHandle>>, usize)>, + scroll_state: Option<(ElementStateHandle>, usize)>, } impl Flex { @@ -52,15 +51,9 @@ impl Flex { Tag: 'static, V: View, { - let scroll_state_handle = - cx.default_element_state::>>(element_id); - let scroll_state_cell = scroll_state_handle.read(cx); - let mut scroll_state = scroll_state_cell.get(); - scroll_state.scroll_to = scroll_to; - scroll_state_cell.set(scroll_state); - - self.scroll_state = Some((scroll_state_handle, cx.handle().id())); - + let scroll_state = cx.default_element_state::>(element_id); + scroll_state.read(cx).scroll_to.set(scroll_to); + self.scroll_state = Some((scroll_state, cx.handle().id())); self } @@ -106,38 +99,6 @@ impl Flex { } } } - - fn handle_scroll( - e: ScrollWheelEvent, - axis: Axis, - scroll_state: Rc>, - remaining_space: f32, - ) -> bool { - let precise = e.precise; - let delta = e.delta; - if remaining_space < 0. { - let mut delta = match axis { - Axis::Horizontal => { - if delta.x() != 0. { - delta.x() - } else { - delta.y() - } - } - Axis::Vertical => delta.y(), - }; - if !precise { - delta *= 20.; - } - - let mut old_state = scroll_state.get(); - old_state.scroll_position -= delta; - scroll_state.set(old_state); - - return true; - } - return false; - } } impl Extend for Flex { @@ -241,8 +202,8 @@ impl Element for Flex { if let Some(scroll_state) = self.scroll_state.as_ref() { scroll_state.0.update(cx, |scroll_state, _| { - if let Some(scroll_to) = scroll_state.get().scroll_to.take() { - let visible_start = scroll_state.get().scroll_position; + if let Some(scroll_to) = scroll_state.scroll_to.take() { + let visible_start = scroll_state.scroll_position.get(); let visible_end = visible_start + size.along(self.axis); if let Some(child) = self.children.get(scroll_to) { let child_start: f32 = self.children[..scroll_to] @@ -250,20 +211,23 @@ impl Element for Flex { .map(|c| c.size().along(self.axis)) .sum(); let child_end = child_start + child.size().along(self.axis); - - let mut old_state = scroll_state.get(); if child_start < visible_start { - old_state.scroll_position = child_start; + scroll_state.scroll_position.set(child_start); } else if child_end > visible_end { - old_state.scroll_position = child_end - size.along(self.axis); + scroll_state + .scroll_position + .set(child_end - size.along(self.axis)); } - scroll_state.set(old_state); } } - let mut old_state = scroll_state.get(); - old_state.scroll_position = old_state.scroll_position.min(-remaining_space).max(0.); - scroll_state.set(old_state); + scroll_state.scroll_position.set( + scroll_state + .scroll_position + .get() + .min(-remaining_space) + .max(0.), + ); }); } @@ -286,28 +250,43 @@ impl Element for Flex { if let Some(scroll_state) = &self.scroll_state { cx.scene.push_mouse_region( - MouseRegion::new::(scroll_state.1, 0, bounds) + crate::MouseRegion::new::(scroll_state.1, 0, bounds) .on_scroll({ - let axis = self.axis; let scroll_state = scroll_state.0.read(cx).clone(); + let axis = self.axis; move |e, cx| { - if Self::handle_scroll( - e.platform_event, - axis, - scroll_state.clone(), - remaining_space, - ) { + if remaining_space < 0. { + let mut delta = match axis { + Axis::Horizontal => { + if e.delta.x() != 0. { + e.delta.x() + } else { + e.delta.y() + } + } + Axis::Vertical => e.delta.y(), + }; + if !e.precise { + delta *= 20.; + } + + scroll_state + .scroll_position + .set(scroll_state.scroll_position.get() - delta); + + cx.notify(); + } else { cx.propogate_event(); } } }) - .on_move(|_, _| { /* Eat move events so they don't propogate */ }), - ); + .on_move(|_, _| { /* Capture move events */ }), + ) } let mut child_origin = bounds.origin(); if let Some(scroll_state) = self.scroll_state.as_ref() { - let scroll_position = scroll_state.0.read(cx).get().scroll_position; + let scroll_position = scroll_state.0.read(cx).scroll_position.get(); match self.axis { Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position), Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position), From 4761898d9b14765f5658ce987b495920a8c2f246 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 22 Sep 2022 10:31:29 -0700 Subject: [PATCH 18/32] removed the last dispatch_event I could find --- crates/gpui/src/elements/list.rs | 39 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 57436c256bd9ee241dc78d07206ba8c3655e2c54..d752a52a1666110a7fc1cc2160988aac86429212 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -5,8 +5,8 @@ use crate::{ }, json::json, presenter::MeasurementContext, - DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext, - RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext, + DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, MouseRegion, + PaintContext, RenderContext, SizeConstraint, View, ViewContext, }; use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc}; use sum_tree::{Bias, SumTree}; @@ -263,6 +263,22 @@ impl Element for List { ) { cx.scene.push_layer(Some(bounds)); + cx.scene + .push_mouse_region(MouseRegion::new::(10, 0, bounds).on_scroll({ + let state = self.state.clone(); + let height = bounds.height(); + let scroll_top = scroll_top.clone(); + move |e, cx| { + state.0.borrow_mut().scroll( + &scroll_top, + height, + e.platform_event.delta, + e.platform_event.precise, + cx, + ) + } + })); + let state = &mut *self.state.0.borrow_mut(); for (mut element, origin) in state.visible_elements(bounds, scroll_top) { element.paint(origin, visible_bounds, cx); @@ -312,20 +328,6 @@ impl Element for List { drop(cursor); state.items = new_items; - if let Event::ScrollWheel(ScrollWheelEvent { - position, - delta, - precise, - .. - }) = event - { - if bounds.contains_point(*position) - && state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) - { - handled = true; - } - } - handled } @@ -527,7 +529,7 @@ impl StateInner { mut delta: Vector2F, precise: bool, cx: &mut EventContext, - ) -> bool { + ) { if !precise { delta *= 20.; } @@ -554,9 +556,6 @@ impl StateInner { let visible_range = self.visible_range(height, scroll_top); self.scroll_handler.as_mut().unwrap()(visible_range, cx); } - cx.notify(); - - true } fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 { From f3395cf4fd5fe900dac3eccebb476655a6321de6 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 22 Sep 2022 18:21:05 -0400 Subject: [PATCH 19/32] Add editor action to manually invoke buffer format --- assets/keymaps/default.json | 1 + assets/settings/default.json | 13 ++++--- crates/collab/src/integration_tests.rs | 20 ++++++++--- crates/editor/src/editor.rs | 48 ++++++++++++++++++++++++-- crates/editor/src/items.rs | 10 +++--- crates/project/src/project.rs | 36 +++++++++++++++---- crates/rpc/proto/zed.proto | 8 ++++- crates/rpc/src/rpc.rs | 2 +- crates/settings/src/settings.rs | 19 ++++++---- 9 files changed, 124 insertions(+), 33 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 7a25dc19d3bcd0ff491f31d9ae1609b1ba267f3e..1d994f86afd925e0b1f0e46f7c7427af57acf0ce 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -93,6 +93,7 @@ "cmd-shift-down": "editor::SelectToEnd", "cmd-a": "editor::SelectAll", "cmd-l": "editor::SelectLine", + "cmd-shift-i": "editor::Format", "cmd-shift-left": [ "editor::SelectToBeginningOfLine", { diff --git a/assets/settings/default.json b/assets/settings/default.json index d8efdc41ff02851e28a8536c4bb63acc8c393ae0..34b665b41d70e3d8edb6e3692ac45076fda6de01 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -42,21 +42,20 @@ // 3. Position the dock full screen over the entire workspace" // "default_dock_anchor": "expanded" "default_dock_anchor": "right", - // How to auto-format modified buffers when saving them. This - // setting can take three values: + // Whether or not to perform a buffer format before saving + "format_on_save": true, + // How to perform a buffer format. This setting can take two values: // - // 1. Don't format code - // "format_on_save": "off" - // 2. Format code using the current language server: + // 1. Format code using the current language server: // "format_on_save": "language_server" - // 3. Format code using an external command: + // 2. Format code using an external command: // "format_on_save": { // "external": { // "command": "prettier", // "arguments": ["--stdin-filepath", "{buffer_path}"] // } // } - "format_on_save": "language_server", + "formatter": "language_server", // How to soft-wrap long lines of text. This setting can take // three values: // diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 6b512d950ff6f9a0e7a2c782a88bce7c60d918ee..073547472854be6be9dedec921a17a89b467d5e6 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -36,7 +36,7 @@ use project::{ use rand::prelude::*; use rpc::PeerId; use serde_json::json; -use settings::{FormatOnSave, Settings}; +use settings::{Formatter, Settings}; use sqlx::types::time::OffsetDateTime; use std::{ cell::RefCell, @@ -1990,6 +1990,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te #[gpui::test(iterations = 10)] async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + use project::FormatTrigger; + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -2042,7 +2044,12 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon project_b .update(cx_b, |project, cx| { - project.format(HashSet::from_iter([buffer_b.clone()]), true, cx) + project.format( + HashSet::from_iter([buffer_b.clone()]), + true, + FormatTrigger::Save, + cx, + ) }) .await .unwrap(); @@ -2055,7 +2062,7 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon // host's configuration is honored as opposed to using the guest's settings. cx_a.update(|cx| { cx.update_global(|settings: &mut Settings, _| { - settings.editor_defaults.format_on_save = Some(FormatOnSave::External { + settings.editor_defaults.formatter = Some(Formatter::External { command: "awk".to_string(), arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()], }); @@ -2063,7 +2070,12 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon }); project_b .update(cx_b, |project, cx| { - project.format(HashSet::from_iter([buffer_b.clone()]), true, cx) + project.format( + HashSet::from_iter([buffer_b.clone()]), + true, + FormatTrigger::Save, + cx, + ) }) .await .unwrap(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a7d46f771046a0067b31a9aeb1a8e6d6c926a98b..b23e1660b6d8005a25b4f7db14abaa7e0ee33f27 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -19,6 +19,7 @@ use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; +use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, @@ -49,7 +50,7 @@ pub use multi_buffer::{ }; use multi_buffer::{MultiBufferChunks, ToOffsetUtf16}; use ordered_float::OrderedFloat; -use project::{LocationLink, Project, ProjectPath, ProjectTransaction}; +use project::{FormatTrigger, LocationLink, Project, ProjectPath, ProjectTransaction}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -76,6 +77,8 @@ const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; +pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); + #[derive(Clone, Deserialize, PartialEq, Default)] pub struct SelectNext { #[serde(default)] @@ -204,6 +207,7 @@ actions!( OpenExcerpts, RestartLanguageServer, Hover, + Format, ] ); @@ -310,6 +314,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::toggle_code_actions); cx.add_action(Editor::open_excerpts); cx.add_action(Editor::jump); + cx.add_async_action(Editor::format); cx.add_action(Editor::restart_language_server); cx.add_action(Editor::show_character_palette); cx.add_async_action(Editor::confirm_completion); @@ -5173,6 +5178,43 @@ impl Editor { self.pending_rename.as_ref() } + fn format(&mut self, _: &Format, cx: &mut ViewContext<'_, Self>) -> Option>> { + let project = match &self.project { + Some(project) => project, + None => return None, + }; + + let buffer = self.buffer().clone(); + let buffers = buffer.read(cx).all_buffers(); + + let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); + let format = project.update(cx, |project, cx| { + project.format(buffers, true, FormatTrigger::Manual, cx) + }); + + Some(cx.spawn(|_, mut cx| async move { + let transaction = futures::select_biased! { + _ = timeout => { + log::warn!("timed out waiting for formatting"); + None + } + transaction = format.log_err().fuse() => transaction, + }; + + buffer.update(&mut cx, |buffer, cx| { + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0); + } + } + + cx.notify(); + }); + + Ok(()) + })) + } + fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext) { if let Some(project) = self.project.clone() { self.buffer.update(cx, |multi_buffer, cx| { @@ -10087,7 +10129,7 @@ mod tests { unreachable!() }); let save = cx.update(|cx| editor.save(project.clone(), cx)); - cx.foreground().advance_clock(items::FORMAT_TIMEOUT); + cx.foreground().advance_clock(super::FORMAT_TIMEOUT); cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( @@ -10203,7 +10245,7 @@ mod tests { }, ); let save = cx.update(|cx| editor.save(project.clone(), cx)); - cx.foreground().advance_clock(items::FORMAT_TIMEOUT); + cx.foreground().advance_clock(super::FORMAT_TIMEOUT); cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index fb6f12a16f7335dcdc62d0809158faa14d656881..ba600b2a5cad5c739098071bfa9f92f91ea70b16 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,7 +1,7 @@ use crate::{ display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer, - MultiBufferSnapshot, NavigationData, ToPoint as _, + MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT, }; use anyhow::{anyhow, Result}; use futures::FutureExt; @@ -10,7 +10,7 @@ use gpui::{ RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; use language::{Bias, Buffer, File as _, OffsetRangeExt, SelectionGoal}; -use project::{File, Project, ProjectEntryId, ProjectPath}; +use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath}; use rpc::proto::{self, update_view}; use settings::Settings; use smallvec::SmallVec; @@ -20,7 +20,6 @@ use std::{ fmt::Write, ops::Range, path::{Path, PathBuf}, - time::Duration, }; use text::{Point, Selection}; use util::TryFutureExt; @@ -30,7 +29,6 @@ use workspace::{ ToolbarItemLocation, }; -pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub const MAX_TAB_TITLE_LEN: usize = 24; impl FollowableItem for Editor { @@ -409,7 +407,9 @@ impl Item for Editor { let buffer = self.buffer().clone(); let buffers = buffer.read(cx).all_buffers(); let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); - let format = project.update(cx, |project, cx| project.format(buffers, true, cx)); + let format = project.update(cx, |project, cx| { + project.format(buffers, true, FormatTrigger::Save, cx) + }); cx.spawn(|_, mut cx| async move { let transaction = futures::select_biased! { _ = timeout => { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 09c5a72315d3552e4df29d0608186966503be3a6..8a3ffbc1c45a5257c590f85d2e04796c79770d70 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -364,6 +364,22 @@ impl ProjectEntryId { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatTrigger { + Save, + Manual, +} + +impl FormatTrigger { + fn from_proto(value: i32) -> FormatTrigger { + match value { + 0 => FormatTrigger::Save, + 1 => FormatTrigger::Manual, + _ => FormatTrigger::Save, + } + } +} + impl Project { pub fn init(client: &Arc) { client.add_model_message_handler(Self::handle_request_join_project); @@ -3047,6 +3063,7 @@ impl Project { &self, buffers: HashSet>, push_to_history: bool, + trigger: FormatTrigger, cx: &mut ModelContext, ) -> Task> { let mut local_buffers = Vec::new(); @@ -3076,6 +3093,7 @@ impl Project { let response = client .request(proto::FormatBuffers { project_id, + trigger: trigger as i32, buffer_ids: remote_buffers .iter() .map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id())) @@ -3092,18 +3110,22 @@ impl Project { } for (buffer, buffer_abs_path, language_server) in local_buffers { - let (format_on_save, tab_size) = buffer.read_with(&cx, |buffer, cx| { + let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| { let settings = cx.global::(); let language_name = buffer.language().map(|language| language.name()); ( settings.format_on_save(language_name.as_deref()), + settings.formatter(language_name.as_deref()), settings.tab_size(language_name.as_deref()), ) }); - let transaction = match format_on_save { - settings::FormatOnSave::Off => continue, - settings::FormatOnSave::LanguageServer => Self::format_via_lsp( + if trigger == FormatTrigger::Save && !format_on_save { + continue; + } + + let transaction = match formatter { + settings::Formatter::LanguageServer => Self::format_via_lsp( &this, &buffer, &buffer_abs_path, @@ -3113,7 +3135,8 @@ impl Project { ) .await .context("failed to format via language server")?, - settings::FormatOnSave::External { command, arguments } => { + + settings::Formatter::External { command, arguments } => { Self::format_via_external_command( &buffer, &buffer_abs_path, @@ -5296,7 +5319,8 @@ impl Project { .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?, ); } - Ok::<_, anyhow::Error>(this.format(buffers, false, cx)) + let trigger = FormatTrigger::from_proto(envelope.payload.trigger); + Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, cx)) })?; let project_transaction = format.await?; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 4cf7e38a139037ccd294ebc4cc377724779023cb..7840829b4461bc7ca497f7bf1bcc7b34e397f0f8 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -420,9 +420,15 @@ message ReloadBuffersResponse { ProjectTransaction transaction = 1; } +enum FormatTrigger { + Save = 0; + Manual = 1; +} + message FormatBuffers { uint64 project_id = 1; - repeated uint64 buffer_ids = 2; + FormatTrigger trigger = 2; + repeated uint64 buffer_ids = 3; } message FormatBuffersResponse { diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 308e5b0f4349c281616c56884279e4aae622a93e..b9f6e6a7390a759b4317ed53bd7309d47a9e37b3 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 31; +pub const PROTOCOL_VERSION: u32 = 32; diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index d2b076b0126e6b4ebe56f3ea340e33954cd4bd98..8e4c49572a8a4bb01c6efea0d79dc97924bb1684 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -58,7 +58,8 @@ pub struct EditorSettings { pub hard_tabs: Option, pub soft_wrap: Option, pub preferred_line_length: Option, - pub format_on_save: Option, + pub format_on_save: Option, + pub formatter: Option, pub enable_language_server: Option, } @@ -72,8 +73,7 @@ pub enum SoftWrap { #[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum FormatOnSave { - Off, +pub enum Formatter { LanguageServer, External { command: String, @@ -207,6 +207,7 @@ impl Settings { font_cache: &FontCache, themes: &ThemeRegistry, ) -> Self { + #[track_caller] fn required(value: Option) -> Option { assert!(value.is_some(), "missing default setting value"); value @@ -236,6 +237,7 @@ impl Settings { soft_wrap: required(defaults.editor.soft_wrap), preferred_line_length: required(defaults.editor.preferred_line_length), format_on_save: required(defaults.editor.format_on_save), + formatter: required(defaults.editor.formatter), enable_language_server: required(defaults.editor.enable_language_server), }, editor_overrides: Default::default(), @@ -322,8 +324,12 @@ impl Settings { self.language_setting(language, |settings| settings.preferred_line_length) } - pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave { - self.language_setting(language, |settings| settings.format_on_save.clone()) + pub fn format_on_save(&self, language: Option<&str>) -> bool { + self.language_setting(language, |settings| settings.format_on_save) + } + + pub fn formatter(&self, language: Option<&str>) -> Formatter { + self.language_setting(language, |settings| settings.formatter.clone()) } pub fn enable_language_server(&self, language: Option<&str>) -> bool { @@ -358,7 +364,8 @@ impl Settings { hard_tabs: Some(false), soft_wrap: Some(SoftWrap::None), preferred_line_length: Some(80), - format_on_save: Some(FormatOnSave::LanguageServer), + format_on_save: Some(true), + formatter: Some(Formatter::LanguageServer), enable_language_server: Some(true), }, editor_overrides: Default::default(), From 5cd56584b45a534b64ef83776b72d294305c32ee Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 22 Sep 2022 22:40:22 -0700 Subject: [PATCH 20/32] Completed terminal hyperlink clicking functionality. Just need to display it now --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 2 ++ crates/terminal/src/terminal.rs | 17 +++++++++-------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 141a542bd2fea5e6232ad8b569a67a783cb69af1..34a1324a41796c81cc3b1464fe3688e16da38c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5386,6 +5386,7 @@ dependencies = [ "futures", "gpui", "itertools", + "lazy_static", "libc", "mio-extras", "ordered-float", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index eb1bc560176bf06a3470936aafba60ab86cff786..da59979145955131c50f4614d9444f8c00cf3934 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -29,6 +29,8 @@ shellexpand = "2.1.0" libc = "0.2" anyhow = "1" thiserror = "1.0" +lazy_static = "1.4.0" + [dev-dependencies] diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index bf5c98d03cb1a64d9e482a4d73cd612c678f954f..ebbfd4fec3c3a25d0f128053720cecbefa198fa2 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -64,6 +64,7 @@ use crate::mappings::{ colors::{get_color_at_index, to_alac_rgb}, keys::to_esc_str, }; +use lazy_static::lazy_static; ///Initialize and register all of our action handlers pub fn init(cx: &mut MutableAppContext) { @@ -83,8 +84,11 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; -/// Copied from alacritty's ui_config.rs -static URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); +// Regex Copied from alacritty's ui_config.rs + +lazy_static! { + static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); +} ///Upward flowing events, for changing the title and such #[derive(Clone, Copy, Debug)] @@ -659,6 +663,7 @@ impl Terminal { if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) { let url = term.bounds_to_string(*url_match.start(), *url_match.end()); + dbg!(&url, &url_match, open); if *open { open_uri(&url).log_err(); @@ -956,12 +961,8 @@ impl Terminal { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { if e.cmd { - if let Some(link) = self.last_content.cells - [content_index_for_mouse(position, &self.last_content)] - .hyperlink() - { - dbg!(&link); - dbg!(&self.last_hovered_hyperlink); + let mouse_cell_index = content_index_for_mouse(position, &self.last_content); + if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { open_uri(link.uri()).log_err(); } else { self.events From 0584b2f5f05f24914e7435c72b4db508ef7f0803 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 22 Sep 2022 23:04:49 -0700 Subject: [PATCH 21/32] added the fields for drawing the hyperlinks --- crates/terminal/src/terminal.rs | 20 ++++++------- crates/terminal/src/terminal_element.rs | 39 ++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index ebbfd4fec3c3a25d0f128053720cecbefa198fa2..c3b3c06e338da1f3da698ff9f7ec532bb7391920 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -387,7 +387,6 @@ impl TerminalBuilder { foreground_process_info: None, breadcrumb_text: String::new(), scroll_px: 0., - last_hovered_hyperlink: None, }; Ok(TerminalBuilder { @@ -474,6 +473,7 @@ pub struct TerminalContent { cursor: RenderableCursor, cursor_char: char, size: TerminalSize, + last_hovered_hyperlink: Option<(String, RangeInclusive)>, } impl Default for TerminalContent { @@ -490,6 +490,7 @@ impl Default for TerminalContent { }, cursor_char: Default::default(), size: Default::default(), + last_hovered_hyperlink: None, } } } @@ -502,7 +503,6 @@ pub struct Terminal { pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, - last_hovered_hyperlink: Option<(String, RangeInclusive)>, sync_task: Option>, selection_head: Option, breadcrumb_text: String, @@ -653,7 +653,7 @@ impl Terminal { } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), InternalEvent::Hyperlink(position, open) => { - self.last_hovered_hyperlink = None; + self.last_content.last_hovered_hyperlink = None; let point = grid_point( *position, @@ -663,12 +663,11 @@ impl Terminal { if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) { let url = term.bounds_to_string(*url_match.start(), *url_match.end()); - dbg!(&url, &url_match, open); if *open { open_uri(&url).log_err(); } else { - self.last_hovered_hyperlink = Some((url, url_match)); + self.last_content.last_hovered_hyperlink = Some((url, url_match)); } } } @@ -779,11 +778,11 @@ impl Terminal { self.process_terminal_event(&e, &mut terminal, cx) } - self.last_content = Self::make_content(&terminal, self.last_content.size); + self.last_content = Self::make_content(&terminal, &self.last_content); self.last_synced = Instant::now(); } - fn make_content(term: &Term, last_size: TerminalSize) -> TerminalContent { + fn make_content(term: &Term, last_content: &TerminalContent) -> TerminalContent { let content = term.renderable_content(); TerminalContent { cells: content @@ -806,7 +805,8 @@ impl Terminal { selection: content.selection, cursor: content.cursor, cursor_char: term.grid()[content.cursor.point].c, - size: last_size, + size: last_content.size, + last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(), } } @@ -844,7 +844,7 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - self.last_hovered_hyperlink = None; + self.last_content.last_hovered_hyperlink = None; let position = e.position.sub(origin); if self.mouse_mode(e.shift) { let point = grid_point( @@ -882,7 +882,7 @@ impl Terminal { } } - self.last_hovered_hyperlink = link.map(|link| { + self.last_content.last_hovered_hyperlink = link.map(|link| { ( link.uri().to_owned(), self.last_content.cells[min_index].point diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 52949ef213f4592272872d51064a84176261b952..88f48fc4efd0f9f299567c7acec6ec6c0de398b3 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -237,7 +237,7 @@ impl TerminalElement { //Layout current cell text { let cell_text = &cell.c.to_string(); - if cell_text != " " { + if !is_blank(&cell) { let cell_style = TerminalElement::cell_style( &cell, fg, @@ -257,8 +257,8 @@ impl TerminalElement { Point::new(line_index as i32, cell.point.column.0 as i32), layout_cell, )) - } - }; + }; + } } if cur_rect.is_some() { @@ -308,7 +308,7 @@ impl TerminalElement { let flags = indexed.cell.flags; let fg = convert_color(&fg, &style.colors, modal); - let underline = flags + let mut underline = flags .intersects(Flags::ALL_UNDERLINES) .then(|| Underline { color: Some(fg), @@ -317,6 +317,13 @@ impl TerminalElement { }) .unwrap_or_default(); + if indexed.cell.hyperlink().is_some() { + underline.squiggly = true; + if underline.thickness == OrderedFloat(0.) { + underline.thickness = OrderedFloat(1.); + } + } + let mut properties = Properties::new(); if indexed .flags @@ -569,6 +576,7 @@ impl Element for TerminalElement { cursor_char, selection, cursor, + last_hovered_hyperlink, .. } = &terminal_handle.read(cx).last_content; @@ -824,6 +832,29 @@ impl Element for TerminalElement { } } +fn is_blank(cell: &IndexedCell) -> bool { + if cell.c != ' ' { + return false; + } + + if cell.bg != AnsiColor::Named(NamedColor::Background) { + return false; + } + + if cell.hyperlink().is_some() { + return false; + } + + if cell + .flags + .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT) + { + return false; + } + + return true; +} + fn to_highlighted_range_lines( range: &RangeInclusive, layout: &LayoutState, From 12e439bda950fc00fd6c712ebb8603f2a6144557 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 23 Sep 2022 12:15:24 -0400 Subject: [PATCH 22/32] Test manual buffer format trigger --- crates/editor/src/editor.rs | 95 +++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b23e1660b6d8005a25b4f7db14abaa7e0ee33f27..c9faa7c662462d744dd6fd1dd1386a2fd10cdaea 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5180,10 +5180,18 @@ impl Editor { fn format(&mut self, _: &Format, cx: &mut ViewContext<'_, Self>) -> Option>> { let project = match &self.project { - Some(project) => project, + Some(project) => project.clone(), None => return None, }; + Some(self.perform_format(project, cx)) + } + + fn perform_format( + &mut self, + project: ModelHandle, + cx: &mut ViewContext<'_, Self>, + ) -> Task> { let buffer = self.buffer().clone(); let buffers = buffer.read(cx).all_buffers(); @@ -5192,7 +5200,7 @@ impl Editor { project.format(buffers, true, FormatTrigger::Manual, cx) }); - Some(cx.spawn(|_, mut cx| async move { + cx.spawn(|_, mut cx| async move { let transaction = futures::select_biased! { _ = timeout => { log::warn!("timed out waiting for formatting"); @@ -5212,7 +5220,7 @@ impl Editor { }); Ok(()) - })) + }) } fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext) { @@ -10283,6 +10291,87 @@ mod tests { save.await.unwrap(); } + #[gpui::test] + async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + + let format = editor.update(cx, |editor, cx| editor.perform_format(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.foreground().start_waiting(); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + // Ensure we don't lock if formatting hangs. + fake_server.handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }); + let format = editor.update(cx, |editor, cx| editor.perform_format(project, cx)); + cx.foreground().advance_clock(super::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); + } + #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { let mut cx = EditorLspTestContext::new_rust( From d2d49633f142aa67fd7ce49629216268f6d50278 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 24 Sep 2022 08:32:06 -0700 Subject: [PATCH 23/32] WIP, almost done with tooltips --- crates/gpui/src/elements/tooltip.rs | 8 +- crates/terminal/src/terminal.rs | 27 +++++- crates/terminal/src/terminal_element.rs | 117 +++++++++++++++++++++--- crates/terminal/src/terminal_view.rs | 4 +- 4 files changed, 138 insertions(+), 18 deletions(-) diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 55ab7e44d25e0fb8edfdcdb210a6630ce62bbad6..c86230a5e158f30c5cef1716b67113a664b3a4df 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -36,10 +36,10 @@ struct TooltipState { #[derive(Clone, Deserialize, Default)] pub struct TooltipStyle { #[serde(flatten)] - container: ContainerStyle, - text: TextStyle, + pub container: ContainerStyle, + pub text: TextStyle, keystroke: KeystrokeStyle, - max_text_width: f32, + pub max_text_width: f32, } #[derive(Clone, Deserialize, Default)] @@ -126,7 +126,7 @@ impl Tooltip { } } - fn render_tooltip( + pub fn render_tooltip( text: String, style: TooltipStyle, action: Option>, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 6dab9e60afeb1d6bb1828c950cac1d417fb9aceb..90671dab1ae07337f05691c2c63139066171b60f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -384,6 +384,7 @@ impl TerminalBuilder { foreground_process_info: None, breadcrumb_text: String::new(), scroll_px: 0., + last_mouse_position: None, }; Ok(TerminalBuilder { @@ -496,7 +497,10 @@ pub struct Terminal { pty_tx: Notifier, term: Arc>>, events: VecDeque, + /// This is only used for mouse mode cell change detection last_mouse: Option<(Point, AlacDirection)>, + /// This is only used for terminal hyperlink checking + last_mouse_position: Option, pub matches: Vec>, last_content: TerminalContent, last_synced: Instant, @@ -813,7 +817,8 @@ impl Terminal { } } - pub fn focus_out(&self) { + pub fn focus_out(&mut self) { + self.last_mouse_position = None; if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) { self.write_to_pty("\x1b[O".to_string()); } @@ -843,6 +848,7 @@ impl Terminal { pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { self.last_content.last_hovered_hyperlink = None; let position = e.position.sub(origin); + self.last_mouse_position = Some(position); if self.mouse_mode(e.shift) { let point = grid_point( position, @@ -857,9 +863,14 @@ impl Terminal { } } } else if e.cmd { + self.fill_hyperlink(Some(position)); + } + } + + fn fill_hyperlink(&mut self, position: Option) { + if let Some(position) = position { let content_index = content_index_for_mouse(position, &self.last_content); let link = self.last_content.cells[content_index].hyperlink(); - if link.is_some() { let mut min_index = content_index; loop { @@ -895,6 +906,7 @@ impl Terminal { pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); + self.last_mouse_position = Some(position); if !self.mouse_mode(e.shift) { // Alacritty has the same ordering, of first updating the selection @@ -1048,6 +1060,17 @@ impl Terminal { } } + pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool { + self.last_content.last_hovered_hyperlink = None; + + if cmd { + self.fill_hyperlink(self.last_mouse_position); + true + } else { + false + } + } + fn determine_scroll_lines( &mut self, e: &ScrollWheelRegionEvent, diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index b66db8b440af9c2fe971c2637f5adcae2911ac4a..9c47e33da3f61372105ee2e7fc43b4c8189110b4 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -7,15 +7,17 @@ use alacritty_terminal::{ use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ color::Color, - fonts::{Properties, Style::Italic, TextStyle, Underline, Weight}, + elements::{Overlay, Tooltip}, + fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, serde_json::json, text_layout::{Line, RunStyle}, - Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion, - PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle, + Axis, Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, + ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint, + TextLayoutCache, WeakModelHandle, WeakViewHandle, }; use itertools::Itertools; use ordered_float::OrderedFloat; @@ -42,6 +44,7 @@ pub struct LayoutState { size: TerminalSize, mode: TermMode, display_offset: usize, + hyperlink_tooltip: Option, } ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points @@ -180,6 +183,7 @@ impl TerminalElement { text_layout_cache: &TextLayoutCache, font_cache: &FontCache, modal: bool, + hyperlink: Option<(HighlightStyle, &RangeInclusive)>, ) -> (Vec, Vec) { let mut cells = vec![]; let mut rects = vec![]; @@ -245,6 +249,7 @@ impl TerminalElement { text_style, font_cache, modal, + hyperlink, ); let layout_cell = text_layout_cache.layout_str( @@ -304,6 +309,7 @@ impl TerminalElement { text_style: &TextStyle, font_cache: &FontCache, modal: bool, + hyperlink: Option<(HighlightStyle, &RangeInclusive)>, ) -> RunStyle { let flags = indexed.cell.flags; let fg = convert_color(&fg, &style.colors, modal); @@ -339,11 +345,25 @@ impl TerminalElement { .select_font(text_style.font_family_id, &properties) .unwrap_or(text_style.font_id); - RunStyle { + let mut result = RunStyle { color: fg, font_id, underline, + }; + + if let Some((style, range)) = hyperlink { + if range.contains(&indexed.point) { + if let Some(underline) = style.underline { + result.underline = underline; + } + + if let Some(color) = style.color { + result.color = color; + } + } } + + result } fn generic_button_handler( @@ -373,7 +393,7 @@ impl TerminalElement { ) { let connection = self.terminal; - let mut region = MouseRegion::new::(view_id, view_id, visible_bounds); + let mut region = MouseRegion::new::(view_id, 0, visible_bounds); // Terminal Emulator controlled behavior: region = region @@ -549,6 +569,9 @@ impl Element for TerminalElement { //Setup layout information let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone. + let link_style = settings.theme.editor.link_definition; + let tooltip_style = settings.theme.tooltip.clone(); + let text_style = TerminalElement::make_text_style(font_cache, settings); let selection_color = settings.theme.editor.selection.selection; let match_color = settings.theme.search.match_background; @@ -571,9 +594,51 @@ impl Element for TerminalElement { }; let terminal_handle = self.terminal.upgrade(cx).unwrap(); - terminal_handle.update(cx.app, |terminal, cx| { - terminal.set_size(dimensions); - terminal.try_sync(cx) + let (last_hovered_hyperlink, last_mouse) = + terminal_handle.update(cx.app, |terminal, cx| { + terminal.set_size(dimensions); + terminal.try_sync(cx); + ( + terminal.last_content.last_hovered_hyperlink.clone(), + terminal.last_mouse_position, + ) + }); + + let view_handle = self.view.clone(); + let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _)| { + last_mouse.and_then(|last_mouse| { + view_handle.upgrade(cx).map(|handle| { + let mut tooltip = cx.render(&handle, |_, cx| { + // TODO: Use the correct dynamic line height + // let mut collapsed_tooltip = Tooltip::render_tooltip( + // uri.clone(), + // tooltip_style.clone(), + // None, + // false, + // ) + // .boxed(); + + Overlay::new( + Tooltip::render_tooltip(uri, tooltip_style, None, false) + .constrained() + .with_height(text_style.line_height(cx.font_cache())) + // .dynamically(move |constraint, cx| { + // SizeConstraint::strict_along( + // Axis::Vertical, + // collapsed_tooltip.layout(constraint, cx).y(), + // ) + // }) + .boxed(), + ) + .with_fit_mode(gpui::elements::OverlayFitMode::SwitchAnchor) + .with_anchor_position(last_mouse) + .boxed() + }); + + tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx); + tooltip + }) + }) }); let TerminalContent { @@ -585,7 +650,7 @@ impl Element for TerminalElement { cursor, last_hovered_hyperlink, .. - } = &terminal_handle.read(cx).last_content; + } = { &terminal_handle.read(cx).last_content }; // searches, highlights to a single range representations let mut relative_highlighted_ranges = Vec::new(); @@ -605,6 +670,9 @@ impl Element for TerminalElement { cx.text_layout_cache, cx.font_cache(), self.modal, + last_hovered_hyperlink + .as_ref() + .map(|(_, range)| (link_style, range)), ); //Layout cursor. Rectangle is used for IME, so we should lay it out even @@ -636,10 +704,11 @@ impl Element for TerminalElement { ) }; + let focused = self.focused; TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( move |(cursor_position, block_width)| { let shape = match cursor.shape { - AlacCursorShape::Block if !self.focused => CursorShape::Hollow, + AlacCursorShape::Block if !focused => CursorShape::Hollow, AlacCursorShape::Block => CursorShape::Block, AlacCursorShape::Underline => CursorShape::Underscore, AlacCursorShape::Beam => CursorShape::Bar, @@ -672,6 +741,7 @@ impl Element for TerminalElement { relative_highlighted_ranges, mode: *mode, display_offset: *display_offset, + hyperlink_tooltip, }, ) } @@ -694,7 +764,11 @@ impl Element for TerminalElement { cx.scene.push_cursor_region(gpui::CursorRegion { bounds, - style: gpui::CursorStyle::IBeam, + style: if layout.hyperlink_tooltip.is_some() { + gpui::CursorStyle::PointingHand + } else { + gpui::CursorStyle::IBeam + }, }); cx.paint_layer(clip_bounds, |cx| { @@ -746,6 +820,15 @@ impl Element for TerminalElement { }) } } + + if let Some(element) = &mut layout.hyperlink_tooltip { + element.paint( + visible_bounds.lower_left() + - vec2f(-layout.size.cell_width, layout.size.line_height), + visible_bounds, + cx, + ) + } }); } @@ -784,6 +867,18 @@ impl Element for TerminalElement { }) }) .unwrap_or(false) + } else if let Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) = event { + self.terminal + .upgrade(cx.app) + .map(|model_handle| { + if model_handle.update(cx.app, |term, _| term.refresh_hyperlink(*cmd)) { + cx.notify(); + true + } else { + false + } + }) + .unwrap_or(false) } else { false } diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index fc8bf20ca7e994e41641c027f89fdf28f54fbade..33d573a76a2755e60548c42d389d1099f19d5694 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -362,7 +362,9 @@ impl View for TerminalView { } fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.terminal.read(cx).focus_out(); + self.terminal.update(cx, |terminal, _| { + terminal.focus_out(); + }); cx.notify(); } From 879a0d8b125ca0bc4ae589615c3307b2f0c17c1e Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 23 Sep 2022 16:52:00 -0400 Subject: [PATCH 24/32] Backward compat format settings --- assets/settings/default.json | 2 +- crates/project/src/project.rs | 17 ++++++++++------- crates/settings/src/settings.rs | 19 +++++++++++++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 34b665b41d70e3d8edb6e3692ac45076fda6de01..b3f3e3e7d81e7bdf6092d14cde137d94a3428ea2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -43,7 +43,7 @@ // "default_dock_anchor": "expanded" "default_dock_anchor": "right", // Whether or not to perform a buffer format before saving - "format_on_save": true, + "format_on_save": "off", // How to perform a buffer format. This setting can take two values: // // 1. Format code using the current language server: diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8a3ffbc1c45a5257c590f85d2e04796c79770d70..36d0b4835a3c8f91790e2bb68d3e8505d88a3e39 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -40,7 +40,7 @@ use postage::watch; use rand::prelude::*; use search::SearchQuery; use serde::Serialize; -use settings::Settings; +use settings::{FormatOnSave, Formatter, Settings}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use std::{ @@ -3120,12 +3120,11 @@ impl Project { ) }); - if trigger == FormatTrigger::Save && !format_on_save { - continue; - } + let transaction = match (formatter, format_on_save) { + (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => continue, - let transaction = match formatter { - settings::Formatter::LanguageServer => Self::format_via_lsp( + (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) + | (_, FormatOnSave::LanguageServer) => Self::format_via_lsp( &this, &buffer, &buffer_abs_path, @@ -3136,7 +3135,11 @@ impl Project { .await .context("failed to format via language server")?, - settings::Formatter::External { command, arguments } => { + ( + Formatter::External { command, arguments }, + FormatOnSave::On | FormatOnSave::Off, + ) + | (_, FormatOnSave::External { command, arguments }) => { Self::format_via_external_command( &buffer, &buffer_abs_path, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 8e4c49572a8a4bb01c6efea0d79dc97924bb1684..e346ff60e6ba89a304e43b7e8696c90d09ac88cb 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -58,7 +58,7 @@ pub struct EditorSettings { pub hard_tabs: Option, pub soft_wrap: Option, pub preferred_line_length: Option, - pub format_on_save: Option, + pub format_on_save: Option, pub formatter: Option, pub enable_language_server: Option, } @@ -70,6 +70,17 @@ pub enum SoftWrap { EditorWidth, PreferredLineLength, } +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum FormatOnSave { + On, + Off, + LanguageServer, + External { + command: String, + arguments: Vec, + }, +} #[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -324,8 +335,8 @@ impl Settings { self.language_setting(language, |settings| settings.preferred_line_length) } - pub fn format_on_save(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| settings.format_on_save) + pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave { + self.language_setting(language, |settings| settings.format_on_save.clone()) } pub fn formatter(&self, language: Option<&str>) -> Formatter { @@ -364,7 +375,7 @@ impl Settings { hard_tabs: Some(false), soft_wrap: Some(SoftWrap::None), preferred_line_length: Some(80), - format_on_save: Some(true), + format_on_save: Some(FormatOnSave::On), formatter: Some(Formatter::LanguageServer), enable_language_server: Some(true), }, From 6a26158728c015f7a08bc558513cacb46955c111 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 10:13:54 -0700 Subject: [PATCH 25/32] v0.55.0 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dad1219ddf33923c80845edef255020bd3e4813e..91fde2e7d0b84ce016ebe4a772afcb18e3013274 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7127,7 +7127,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.54.1" +version = "0.55.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 7800ea74153b0a12ca2b3d6ade8c4233728ab743..dc2b0abd03e59e753f13eb323a372e4c817ad079 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.54.1" +version = "0.55.0" [lib] name = "zed" From cd07c98b7d8d68c1160b7f884ef5f3d97c65c886 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 26 Sep 2022 14:04:25 -0400 Subject: [PATCH 26/32] Re-enable format on save by default --- assets/settings/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index b3f3e3e7d81e7bdf6092d14cde137d94a3428ea2..a12cf44d94ae29c45851d3b39a3c4caa32008f96 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -43,7 +43,7 @@ // "default_dock_anchor": "expanded" "default_dock_anchor": "right", // Whether or not to perform a buffer format before saving - "format_on_save": "off", + "format_on_save": "on", // How to perform a buffer format. This setting can take two values: // // 1. Format code using the current language server: From 4bc0afdafa1e0136d2f6e2c09f580a84995afb54 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 16:33:29 -0700 Subject: [PATCH 27/32] Finished terminal hyperlinks for now --- crates/gpui/src/elements/overlay.rs | 29 +++++++- crates/terminal/src/terminal.rs | 89 +++++++++++++++++++------ crates/terminal/src/terminal_element.rs | 79 ++++++++-------------- 3 files changed, 124 insertions(+), 73 deletions(-) diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 20b6c75c8fd6906713a5cf2e2c0afc101eefe992..d47a39e958b8cc18ac12e5b32fa52c602ad0d714 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -14,6 +14,7 @@ pub struct Overlay { anchor_position: Option, anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, + position_mode: OverlayPositionMode, hoverable: bool, } @@ -24,6 +25,12 @@ pub enum OverlayFitMode { None, } +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum OverlayPositionMode { + Window, + Local, +} + #[derive(Clone, Copy, PartialEq, Eq)] pub enum AnchorCorner { TopLeft, @@ -73,6 +80,7 @@ impl Overlay { anchor_position: None, anchor_corner: AnchorCorner::TopLeft, fit_mode: OverlayFitMode::None, + position_mode: OverlayPositionMode::Window, hoverable: false, } } @@ -92,6 +100,11 @@ impl Overlay { self } + pub fn with_position_mode(mut self, position_mode: OverlayPositionMode) -> Self { + self.position_mode = position_mode; + self + } + pub fn with_hoverable(mut self, hoverable: bool) -> Self { self.hoverable = hoverable; self @@ -123,8 +136,20 @@ impl Element for Overlay { size: &mut Self::LayoutState, cx: &mut PaintContext, ) { - let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin()); - let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size); + let (anchor_position, mut bounds) = match self.position_mode { + OverlayPositionMode::Window => { + let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin()); + let bounds = self.anchor_corner.get_bounds(anchor_position, *size); + (anchor_position, bounds) + } + OverlayPositionMode::Local => { + let anchor_position = self.anchor_position.unwrap_or_default(); + let bounds = self + .anchor_corner + .get_bounds(bounds.origin() + anchor_position, *size); + (anchor_position, bounds) + } + }; match self.fit_mode { OverlayFitMode::SnapToWindow => { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 90671dab1ae07337f05691c2c63139066171b60f..56617001cfc264a8bf20f7c50fbae239cac1b800 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -385,6 +385,8 @@ impl TerminalBuilder { breadcrumb_text: String::new(), scroll_px: 0., last_mouse_position: None, + next_link_id: 0, + selection_phase: SelectionPhase::Ended, }; Ok(TerminalBuilder { @@ -471,7 +473,7 @@ pub struct TerminalContent { cursor: RenderableCursor, cursor_char: char, size: TerminalSize, - last_hovered_hyperlink: Option<(String, RangeInclusive)>, + last_hovered_hyperlink: Option<(String, RangeInclusive, usize)>, } impl Default for TerminalContent { @@ -493,6 +495,12 @@ impl Default for TerminalContent { } } +#[derive(PartialEq, Eq)] +pub enum SelectionPhase { + Selecting, + Ended, +} + pub struct Terminal { pty_tx: Notifier, term: Arc>>, @@ -511,6 +519,8 @@ pub struct Terminal { shell_fd: u32, foreground_process_info: Option, scroll_px: f32, + next_link_id: usize, + selection_phase: SelectionPhase, } impl Terminal { @@ -654,7 +664,7 @@ impl Terminal { } InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), InternalEvent::Hyperlink(position, open) => { - self.last_content.last_hovered_hyperlink = None; + let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); let point = grid_point( *position, @@ -668,13 +678,37 @@ impl Terminal { if *open { open_uri(&url).log_err(); } else { - self.last_content.last_hovered_hyperlink = Some((url, url_match)); + self.update_hyperlink(prev_hyperlink, url, url_match); } } } } } + fn update_hyperlink( + &mut self, + prev_hyperlink: Option<(String, RangeInclusive, usize)>, + url: String, + url_match: RangeInclusive, + ) { + if let Some(prev_hyperlink) = prev_hyperlink { + if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match { + self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2)); + } else { + self.last_content.last_hovered_hyperlink = + Some((url, url_match, self.next_link_id())); + } + } else { + self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id())); + } + } + + fn next_link_id(&mut self) -> usize { + let res = self.next_link_id; + self.next_link_id = self.next_link_id.wrapping_add(1); + res + } + pub fn last_content(&self) -> &TerminalContent { &self.last_content } @@ -846,7 +880,8 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - self.last_content.last_hovered_hyperlink = None; + let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); + let position = e.position.sub(origin); self.last_mouse_position = Some(position); if self.mouse_mode(e.shift) { @@ -862,19 +897,26 @@ impl Terminal { self.pty_tx.notify(bytes); } } - } else if e.cmd { - self.fill_hyperlink(Some(position)); + } else { + self.fill_hyperlink(Some(position), prev_hyperlink); } } - fn fill_hyperlink(&mut self, position: Option) { - if let Some(position) = position { + fn fill_hyperlink( + &mut self, + position: Option, + prev_hyperlink: Option<(String, RangeInclusive, usize)>, + ) { + if self.selection_phase == SelectionPhase::Selecting { + self.last_content.last_hovered_hyperlink = None; + } else if let Some(position) = position { let content_index = content_index_for_mouse(position, &self.last_content); let link = self.last_content.cells[content_index].hyperlink(); if link.is_some() { let mut min_index = content_index; loop { - if self.last_content.cells[min_index - 1].hyperlink() == link { + if min_index >= 1 && self.last_content.cells[min_index - 1].hyperlink() == link + { min_index = min_index - 1; } else { break; @@ -882,21 +924,24 @@ impl Terminal { } let mut max_index = content_index; + let len = self.last_content.cells.len(); loop { - if self.last_content.cells[max_index + 1].hyperlink() == link { + if max_index < len - 1 + && self.last_content.cells[max_index + 1].hyperlink() == link + { max_index = max_index + 1; } else { break; } } - self.last_content.last_hovered_hyperlink = link.map(|link| { - ( - link.uri().to_owned(), - self.last_content.cells[min_index].point - ..=self.last_content.cells[max_index].point, - ) - }); + if let Some(link) = link { + let url = link.uri().to_owned(); + let url_match = self.last_content.cells[min_index].point + ..=self.last_content.cells[max_index].point; + + self.update_hyperlink(prev_hyperlink, url, url_match); + }; } else { self.events .push_back(InternalEvent::Hyperlink(position, false)); @@ -909,6 +954,7 @@ impl Terminal { self.last_mouse_position = Some(position); if !self.mouse_mode(e.shift) { + self.selection_phase = SelectionPhase::Selecting; // Alacritty has the same ordering, of first updating the selection // then scrolling 15ms later self.events @@ -969,7 +1015,9 @@ impl Terminal { pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { - if e.cmd { + if self.last_content.last_hovered_hyperlink.is_some() + && self.last_content.selection.is_none() + { let mouse_cell_index = content_index_for_mouse(position, &self.last_content); if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { open_uri(link.uri()).log_err(); @@ -1021,6 +1069,7 @@ impl Terminal { // so let's do that here self.copy(); } + self.selection_phase = SelectionPhase::Ended; self.last_mouse = None; } @@ -1061,10 +1110,10 @@ impl Terminal { } pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool { - self.last_content.last_hovered_hyperlink = None; + let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); if cmd { - self.fill_hyperlink(self.last_mouse_position); + self.fill_hyperlink(self.last_mouse_position, prev_hyperlink); true } else { false diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 9c47e33da3f61372105ee2e7fc43b4c8189110b4..10d6d279c4fca52bbe78747350180441f3c1a9d7 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -7,7 +7,7 @@ use alacritty_terminal::{ use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ color::Color, - elements::{Overlay, Tooltip}, + elements::{Empty, Overlay}, fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight}, geometry::{ rect::RectF, @@ -15,7 +15,7 @@ use gpui::{ }, serde_json::json, text_layout::{Line, RunStyle}, - Axis, Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, + Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache, WeakModelHandle, WeakViewHandle, }; @@ -324,7 +324,6 @@ impl TerminalElement { .unwrap_or_default(); if indexed.cell.hyperlink().is_some() { - underline.squiggly = true; if underline.thickness == OrderedFloat(0.) { underline.thickness = OrderedFloat(1.); } @@ -594,51 +593,34 @@ impl Element for TerminalElement { }; let terminal_handle = self.terminal.upgrade(cx).unwrap(); - let (last_hovered_hyperlink, last_mouse) = - terminal_handle.update(cx.app, |terminal, cx| { - terminal.set_size(dimensions); - terminal.try_sync(cx); - ( - terminal.last_content.last_hovered_hyperlink.clone(), - terminal.last_mouse_position, - ) - }); + let last_hovered_hyperlink = terminal_handle.update(cx.app, |terminal, cx| { + terminal.set_size(dimensions); + terminal.try_sync(cx); + terminal.last_content.last_hovered_hyperlink.clone() + }); let view_handle = self.view.clone(); - let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _)| { - last_mouse.and_then(|last_mouse| { - view_handle.upgrade(cx).map(|handle| { - let mut tooltip = cx.render(&handle, |_, cx| { - // TODO: Use the correct dynamic line height - // let mut collapsed_tooltip = Tooltip::render_tooltip( - // uri.clone(), - // tooltip_style.clone(), - // None, - // false, - // ) - // .boxed(); - - Overlay::new( - Tooltip::render_tooltip(uri, tooltip_style, None, false) - .constrained() - .with_height(text_style.line_height(cx.font_cache())) - // .dynamically(move |constraint, cx| { - // SizeConstraint::strict_along( - // Axis::Vertical, - // collapsed_tooltip.layout(constraint, cx).y(), - // ) - // }) - .boxed(), - ) - .with_fit_mode(gpui::elements::OverlayFitMode::SwitchAnchor) - .with_anchor_position(last_mouse) - .boxed() - }); + let hyperlink_tooltip = last_hovered_hyperlink.and_then(|(uri, _, id)| { + // last_mouse.and_then(|_last_mouse| { + view_handle.upgrade(cx).map(|handle| { + let mut tooltip = cx.render(&handle, |_, cx| { + Overlay::new( + Empty::new() + .contained() + .constrained() + .with_width(dimensions.width()) + .with_height(dimensions.height()) + .with_tooltip::(id, uri, None, tooltip_style, cx) + .boxed(), + ) + .with_position_mode(gpui::elements::OverlayPositionMode::Local) + .boxed() + }); - tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx); - tooltip - }) + tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx); + tooltip }) + // }) }); let TerminalContent { @@ -672,7 +654,7 @@ impl Element for TerminalElement { self.modal, last_hovered_hyperlink .as_ref() - .map(|(_, range)| (link_style, range)), + .map(|(_, range, _)| (link_style, range)), ); //Layout cursor. Rectangle is used for IME, so we should lay it out even @@ -822,12 +804,7 @@ impl Element for TerminalElement { } if let Some(element) = &mut layout.hyperlink_tooltip { - element.paint( - visible_bounds.lower_left() - - vec2f(-layout.size.cell_width, layout.size.line_height), - visible_bounds, - cx, - ) + element.paint(origin, visible_bounds, cx) } }); } From 550ae40ff5a0dac472d88cd657e27450f7b7d105 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 16:36:08 -0700 Subject: [PATCH 28/32] Slightly improved left click handling --- crates/terminal/src/terminal.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 56617001cfc264a8bf20f7c50fbae239cac1b800..0a518d2b15a7decf564488250b32cad11f000414 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1015,8 +1015,7 @@ impl Terminal { pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { - if self.last_content.last_hovered_hyperlink.is_some() - && self.last_content.selection.is_none() + //Hyperlinks { let mouse_cell_index = content_index_for_mouse(position, &self.last_content); if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { @@ -1025,7 +1024,10 @@ impl Terminal { self.events .push_back(InternalEvent::Hyperlink(position, true)); } - } else { + } + + // Selections + { let point = grid_point( position, self.last_content.size, From a8e05c946e40cdfadaa92eb9aee6f2d2c0941f6f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 17:46:19 -0700 Subject: [PATCH 29/32] Fixed bug where hyperlinks would not be refreshed when the page scrolled --- crates/terminal/src/terminal.rs | 125 +++++++++++------------- crates/terminal/src/terminal_element.rs | 18 +--- 2 files changed, 61 insertions(+), 82 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0a518d2b15a7decf564488250b32cad11f000414..333be558fa49d7f8972fb4ebd3c045876001739c 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -41,7 +41,7 @@ use std::{ collections::{HashMap, VecDeque}, fmt::Display, io, - ops::{Deref, RangeInclusive, Sub}, + ops::{Deref, Index, RangeInclusive, Sub}, os::unix::{prelude::AsRawFd, process::CommandExt}, path::PathBuf, process::Command, @@ -110,7 +110,7 @@ enum InternalEvent { SetSelection(Option<(Selection, Point)>), UpdateSelection(Vector2F), // Adjusted mouse position, should open - Hyperlink(Vector2F, bool), + FindHyperlink(Vector2F, bool), Copy, } @@ -617,12 +617,6 @@ impl Terminal { self.pty_tx.0.send(Msg::Resize((new_size).into())).ok(); - // When this resize happens - // We go from 737px -> 703px height - // This means there is 1 less line - // that means the delta is 1 - // That means the selection is rotated by -1 - term.resize(new_size); } InternalEvent::Clear => { @@ -631,6 +625,7 @@ impl Terminal { } InternalEvent::Scroll(scroll) => { term.scroll_display(*scroll); + self.refresh_hyperlink(); } InternalEvent::SetSelection(selection) => { term.selection = selection.as_ref().map(|(sel, _)| sel.clone()); @@ -662,19 +657,61 @@ impl Terminal { cx.write_to_clipboard(ClipboardItem::new(txt)) } } - InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), - InternalEvent::Hyperlink(position, open) => { + InternalEvent::ScrollToPoint(point) => { + term.scroll_to_point(*point); + self.refresh_hyperlink(); + } + InternalEvent::FindHyperlink(position, open) => { let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); let point = grid_point( *position, self.last_content.size, term.grid().display_offset(), - ); + ) + .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor); + + let link = term.grid().index(point).hyperlink(); + let found_url = if link.is_some() { + let mut min_index = point; + loop { + let new_min_index = + min_index.sub(term, alacritty_terminal::index::Boundary::Cursor, 1); + if new_min_index == min_index { + break; + } else if term.grid().index(new_min_index).hyperlink() != link { + break; + } else { + min_index = new_min_index + } + } + + let mut max_index = point; + loop { + let new_max_index = + max_index.add(term, alacritty_terminal::index::Boundary::Cursor, 1); + if new_max_index == max_index { + break; + } else if term.grid().index(new_max_index).hyperlink() != link { + break; + } else { + max_index = new_max_index + } + } + + let url = link.unwrap().uri().to_owned(); + let url_match = min_index..=max_index; - if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) { + Some((url, url_match)) + } else if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) { let url = term.bounds_to_string(*url_match.start(), *url_match.end()); + Some((url, url_match)) + } else { + None + }; + + if let Some((url, url_match)) = found_url { if *open { open_uri(&url).log_err(); } else { @@ -774,7 +811,8 @@ impl Terminal { } else { text.replace("\r\n", "\r").replace('\n', "\r") }; - self.input(paste_text) + + self.input(paste_text); } pub fn try_sync(&mut self, cx: &mut ModelContext) { @@ -880,8 +918,6 @@ impl Terminal { } pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) { - let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); - let position = e.position.sub(origin); self.last_mouse_position = Some(position); if self.mouse_mode(e.shift) { @@ -898,54 +934,16 @@ impl Terminal { } } } else { - self.fill_hyperlink(Some(position), prev_hyperlink); + self.hyperlink_from_position(Some(position)); } } - fn fill_hyperlink( - &mut self, - position: Option, - prev_hyperlink: Option<(String, RangeInclusive, usize)>, - ) { + fn hyperlink_from_position(&mut self, position: Option) { if self.selection_phase == SelectionPhase::Selecting { self.last_content.last_hovered_hyperlink = None; } else if let Some(position) = position { - let content_index = content_index_for_mouse(position, &self.last_content); - let link = self.last_content.cells[content_index].hyperlink(); - if link.is_some() { - let mut min_index = content_index; - loop { - if min_index >= 1 && self.last_content.cells[min_index - 1].hyperlink() == link - { - min_index = min_index - 1; - } else { - break; - } - } - - let mut max_index = content_index; - let len = self.last_content.cells.len(); - loop { - if max_index < len - 1 - && self.last_content.cells[max_index + 1].hyperlink() == link - { - max_index = max_index + 1; - } else { - break; - } - } - - if let Some(link) = link { - let url = link.uri().to_owned(); - let url_match = self.last_content.cells[min_index].point - ..=self.last_content.cells[max_index].point; - - self.update_hyperlink(prev_hyperlink, url, url_match); - }; - } else { - self.events - .push_back(InternalEvent::Hyperlink(position, false)); - } + self.events + .push_back(InternalEvent::FindHyperlink(position, false)); } } @@ -1022,7 +1020,7 @@ impl Terminal { open_uri(link.uri()).log_err(); } else { self.events - .push_back(InternalEvent::Hyperlink(position, true)); + .push_back(InternalEvent::FindHyperlink(position, true)); } } @@ -1111,15 +1109,8 @@ impl Terminal { } } - pub fn refresh_hyperlink(&mut self, cmd: bool) -> bool { - let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); - - if cmd { - self.fill_hyperlink(self.last_mouse_position, prev_hyperlink); - true - } else { - false - } + pub fn refresh_hyperlink(&mut self) { + self.hyperlink_from_position(self.last_mouse_position); } fn determine_scroll_lines( diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 10d6d279c4fca52bbe78747350180441f3c1a9d7..a945c18c437b68f5ec35b908957e16986901880b 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -15,9 +15,9 @@ use gpui::{ }, serde_json::json, text_layout::{Line, RunStyle}, - Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, - ModifiersChangedEvent, MouseButton, MouseRegion, PaintContext, Quad, SizeConstraint, - TextLayoutCache, WeakModelHandle, WeakViewHandle, + Element, ElementBox, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, + MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache, WeakModelHandle, + WeakViewHandle, }; use itertools::Itertools; use ordered_float::OrderedFloat; @@ -844,18 +844,6 @@ impl Element for TerminalElement { }) }) .unwrap_or(false) - } else if let Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) = event { - self.terminal - .upgrade(cx.app) - .map(|model_handle| { - if model_handle.update(cx.app, |term, _| term.refresh_hyperlink(*cmd)) { - cx.notify(); - true - } else { - false - } - }) - .unwrap_or(false) } else { false } From 2ae3fbd6b254c6a7ec089e1db3ba5521e218ad27 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 19:37:55 -0700 Subject: [PATCH 30/32] Improved terminal selection ergonomics --- crates/terminal/src/terminal.rs | 15 ++++----------- crates/terminal/src/terminal_element.rs | 11 ----------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 333be558fa49d7f8972fb4ebd3c045876001739c..473bbd4f52aa2ebbf4885830cded6d44252fa75c 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -53,9 +53,7 @@ use thiserror::Error; use gpui::{ geometry::vector::{vec2f, Vector2F}, keymap::Keystroke, - scene::{ - ClickRegionEvent, DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent, - }, + scene::{DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent}, ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task, }; @@ -969,8 +967,6 @@ impl Terminal { self.events .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines))); - self.events - .push_back(InternalEvent::UpdateSelection(position)) } } } @@ -996,21 +992,18 @@ impl Terminal { self.last_content.size, self.last_content.display_offset, ); - let side = mouse_side(position, self.last_content.size); + // 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) { self.pty_tx.notify(bytes); } } else if e.button == MouseButton::Left { - self.events.push_back(InternalEvent::SetSelection(Some(( - Selection::new(SelectionType::Simple, point, side), - point, - )))); + self.left_click(e, origin) } } - pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) { + pub fn left_click(&mut self, e: &DownRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { //Hyperlinks diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index a945c18c437b68f5ec35b908957e16986901880b..8cb193555d9a6974edc2514a8553705289931ae3 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -429,17 +429,6 @@ impl TerminalElement { }, ), ) - // Handle click based selections - .on_click( - MouseButton::Left, - TerminalElement::generic_button_handler( - connection, - origin, - move |terminal, origin, e, _cx| { - terminal.left_click(&e, origin); - }, - ), - ) // Context menu .on_click(MouseButton::Right, move |e, cx| { let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) { From 24cc9859c7d28aaf35cf461839145314c4d9586f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 20:01:05 -0700 Subject: [PATCH 31/32] Added terminal::SendText command, for sending text to the terminal --- Cargo.lock | 1 + crates/terminal/Cargo.toml | 2 ++ crates/terminal/src/terminal_view.rs | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 9d2c6a35fa530cb442e2400e7d465482834d4ce4..3c8a7ce1a68743b9340320844a7f9aff539cba19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5489,6 +5489,7 @@ dependencies = [ "procinfo", "project", "rand 0.8.5", + "serde", "settings", "shellexpand", "smallvec", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index da59979145955131c50f4614d9444f8c00cf3934..a0b5231501228d699631e4146b2acb3051656903 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -30,6 +30,8 @@ libc = "0.2" anyhow = "1" thiserror = "1.0" lazy_static = "1.4.0" +serde = { version = "1.0", features = ["derive"] } + diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index 33d573a76a2755e60548c42d389d1099f19d5694..856ed3af41c893f7e0eff73ca839d391ecf8dd60 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -6,11 +6,12 @@ use gpui::{ actions, elements::{AnchorCorner, ChildView, ParentElement, Stack}, geometry::vector::Vector2F, - impl_internal_actions, + impl_actions, impl_internal_actions, keymap::Keystroke, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; +use serde::Deserialize; use settings::{Settings, TerminalBlink}; use smol::Timer; use workspace::pane; @@ -28,6 +29,9 @@ pub struct DeployContextMenu { pub position: Vector2F, } +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct SendText(String); + actions!( terminal, [ @@ -43,6 +47,9 @@ actions!( SearchTest ] ); + +impl_actions!(terminal, [SendText]); + impl_internal_actions!(project_panel, [DeployContextMenu]); pub fn init(cx: &mut MutableAppContext) { @@ -53,6 +60,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(TerminalView::escape); cx.add_action(TerminalView::enter); //Useful terminal views + cx.add_action(TerminalView::send_text); cx.add_action(TerminalView::deploy_context_menu); cx.add_action(TerminalView::copy); cx.add_action(TerminalView::paste); @@ -283,6 +291,13 @@ impl TerminalView { } } + fn send_text(&mut self, text: &SendText, cx: &mut ViewContext) { + self.clear_bel(cx); + self.terminal.update(cx, |term, _| { + term.input(text.0.to_string()); + }); + } + ///Synthesize the keyboard event corresponding to 'up' fn up(&mut self, _: &Up, cx: &mut ViewContext) { self.clear_bel(cx); From 9a596030651ba89a3e48da2083bcf2d3c737e9f8 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 20:39:40 -0700 Subject: [PATCH 32/32] Added a SendKeystroke action and rewrote terminal actions to remove duplication --- assets/keymaps/default.json | 44 +++++++++++++++---- crates/terminal/src/terminal_view.rs | 64 +++++++++------------------- 2 files changed, 55 insertions(+), 53 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 7a25dc19d3bcd0ff491f31d9ae1609b1ba267f3e..80ec6301c5015331f11df69c3bfcc8b06d03d41c 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -427,17 +427,45 @@ { "context": "Terminal", "bindings": { - // Overrides for global bindings, remove at your own risk: - "up": "terminal::Up", - "down": "terminal::Down", - "escape": "terminal::Escape", - "enter": "terminal::Enter", - "ctrl-c": "terminal::CtrlC", - // Useful terminal actions: "ctrl-cmd-space": "terminal::ShowCharacterPalette", "cmd-c": "terminal::Copy", "cmd-v": "terminal::Paste", - "cmd-k": "terminal::Clear" + "cmd-k": "terminal::Clear", + // Some nice conveniences + "cmd-backspace": [ + "terminal::SendText", + "\u0015" + ], + "cmd-right": [ + "terminal::SendText", + "\u0005" + ], + "cmd-left": [ + "terminal::SendText", + "\u0001" + ], + // There are conflicting bindings for these keys in the global context. + // these bindings override them, remove at your own risk: + "up": [ + "terminal::SendKeystroke", + "up" + ], + "down": [ + "terminal::SendKeystroke", + "down" + ], + "escape": [ + "terminal::SendKeystroke", + "escape" + ], + "enter": [ + "terminal::SendKeystroke", + "enter" + ], + "ctrl-c": [ + "terminal::SendKeystroke", + "ctrl-c" + ] } } ] \ No newline at end of file diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index 856ed3af41c893f7e0eff73ca839d391ecf8dd60..274207604507d36029b799bad08d19dbbd57cc29 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -14,6 +14,7 @@ use gpui::{ use serde::Deserialize; use settings::{Settings, TerminalBlink}; use smol::Timer; +use util::ResultExt; use workspace::pane; use crate::{terminal_element::TerminalElement, Event, Terminal}; @@ -32,6 +33,9 @@ pub struct DeployContextMenu { #[derive(Clone, Default, Deserialize, PartialEq)] pub struct SendText(String); +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct SendKeystroke(String); + actions!( terminal, [ @@ -48,19 +52,14 @@ actions!( ] ); -impl_actions!(terminal, [SendText]); +impl_actions!(terminal, [SendText, SendKeystroke]); impl_internal_actions!(project_panel, [DeployContextMenu]); pub fn init(cx: &mut MutableAppContext) { - //Global binding overrrides - cx.add_action(TerminalView::ctrl_c); - cx.add_action(TerminalView::up); - cx.add_action(TerminalView::down); - cx.add_action(TerminalView::escape); - cx.add_action(TerminalView::enter); //Useful terminal views cx.add_action(TerminalView::send_text); + cx.add_action(TerminalView::send_keystroke); cx.add_action(TerminalView::deploy_context_menu); cx.add_action(TerminalView::copy); cx.add_action(TerminalView::paste); @@ -298,44 +297,19 @@ impl TerminalView { }); } - ///Synthesize the keyboard event corresponding to 'up' - fn up(&mut self, _: &Up, cx: &mut ViewContext) { - self.clear_bel(cx); - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("up").unwrap(), false) - }); - } - - ///Synthesize the keyboard event corresponding to 'down' - fn down(&mut self, _: &Down, cx: &mut ViewContext) { - self.clear_bel(cx); - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("down").unwrap(), false) - }); - } - - ///Synthesize the keyboard event corresponding to 'ctrl-c' - fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext) { - self.clear_bel(cx); - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap(), false) - }); - } - - ///Synthesize the keyboard event corresponding to 'escape' - fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { - self.clear_bel(cx); - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("escape").unwrap(), false) - }); - } - - ///Synthesize the keyboard event corresponding to 'enter' - fn enter(&mut self, _: &Enter, cx: &mut ViewContext) { - self.clear_bel(cx); - self.terminal.update(cx, |term, _| { - term.try_keystroke(&Keystroke::parse("enter").unwrap(), false) - }); + fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext) { + if let Some(keystroke) = Keystroke::parse(&text.0).log_err() { + self.clear_bel(cx); + self.terminal.update(cx, |term, cx| { + term.try_keystroke( + &keystroke, + cx.global::() + .terminal_overrides + .option_as_meta + .unwrap_or(false), + ); + }); + } } }