From 471810a3c2ce1ebf2b4ebdbaaa5e94e43e52ac5b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 22 Aug 2023 15:27:44 -0400 Subject: [PATCH 01/67] WIP Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- assets/settings/default.json | 18 ++++++++---- crates/project/src/terminals.rs | 49 +++++++++++++++++++++++++++++++++ crates/terminal/src/terminal.rs | 14 ++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 24412b883bf0be12cb2639dd54dec7f70adf6882..f4d77f02cf5425888b5b238a2284586fefd4d09b 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -284,8 +284,6 @@ // "directory": "~/zed/projects/" // } // } - // - // "working_directory": "current_project_directory", // Set the cursor blinking behavior in the terminal. // May take 4 values: @@ -334,13 +332,23 @@ // "line_height": { // "custom": 2 // }, - "line_height": "comfortable" + "line_height": "comfortable", // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. - // "font_size": "15" + // "font_size": "15", // Set the terminal's font family. If this option is not included, // the terminal will default to matching the buffer's font family. - // "font_family": "Zed Mono" + // "font_family": "Zed Mono", + // --- + // Whether or not to automatically search for, and activate, Python virtual + // environments. + // Current limitations: + // - Only ".env", "env", ".venv", and "venv" are searched for at the + // root of the project + // - Only works with Posix-complaint shells + // - Only activates the first virtual environment it finds, regardless + // of the nunber of projects in the workspace. + "automatically_activate_python_virtual_environment": false }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index db5996829fa278db04e793d751d02ace086594e3..e585b659ee3ca0533c801ba7b7c2840402e82a08 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -3,6 +3,9 @@ use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; use std::path::PathBuf; use terminal::{Terminal, TerminalBuilder, TerminalSettings}; +#[cfg(target_os = "macos")] +use std::os::unix::ffi::OsStrExt; + pub struct Terminals { pub(crate) local_handles: Vec>, } @@ -47,6 +50,12 @@ impl Project { }) .detach(); + let setting = settings::get::(cx); + + if setting.automatically_activate_python_virtual_environment { + self.set_up_python_virtual_environment(&terminal_handle, cx); + } + terminal_handle }); @@ -54,6 +63,46 @@ impl Project { } } + fn set_up_python_virtual_environment( + &mut self, + terminal_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + let virtual_environment = self.find_python_virtual_environment(cx); + if let Some(virtual_environment) = virtual_environment { + // Paths are not strings so we need to jump through some hoops to format the command without `format!` + let mut command = Vec::from("source ".as_bytes()); + command.extend_from_slice(virtual_environment.as_os_str().as_bytes()); + command.push(b'\n'); + + terminal_handle.update(cx, |this, _| this.input_bytes(command)); + } + } + + pub fn find_python_virtual_environment( + &mut self, + cx: &mut ModelContext, + ) -> Option { + const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; + + let worktree_paths = self + .worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()); + + for worktree_path in worktree_paths { + for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES { + let mut path = worktree_path.join(virtual_environment_name); + path.push("bin/activate"); + + if path.exists() { + return Some(path); + } + } + } + + None + } + pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3bae06a86dc754126effb1c3c3302a31315d246c..9b0f0bbc860b113de7c2e933bfdab76b25c27e02 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -158,6 +158,7 @@ pub struct TerminalSettings { pub dock: TerminalDockPosition, pub default_width: f32, pub default_height: f32, + pub automatically_activate_python_virtual_environment: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -176,6 +177,7 @@ pub struct TerminalSettingsContent { pub dock: Option, pub default_width: Option, pub default_height: Option, + pub automatically_activate_python_virtual_environment: Option, } impl TerminalSettings { @@ -1018,6 +1020,10 @@ impl Terminal { self.pty_tx.notify(input.into_bytes()); } + fn write_bytes_to_pty(&self, input: Vec) { + self.pty_tx.notify(input); + } + pub fn input(&mut self, input: String) { self.events .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); @@ -1026,6 +1032,14 @@ impl Terminal { self.write_to_pty(input); } + pub fn input_bytes(&mut self, input: Vec) { + self.events + .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); + self.events.push_back(InternalEvent::SetSelection(None)); + + self.write_bytes_to_pty(input); + } + pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool { let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta); if let Some(esc) = esc { From 711f156308322edeeb195978e5773e020ecdf918 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 04:04:36 -0400 Subject: [PATCH 02/67] WIP --- crates/project/src/terminals.rs | 72 +++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index e585b659ee3ca0533c801ba7b7c2840402e82a08..0fa525a82ce0a754d572d9f8ccf82c9530c9fcb2 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,7 @@ use crate::Project; use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; use std::path::PathBuf; -use terminal::{Terminal, TerminalBuilder, TerminalSettings}; +use terminal::{Shell, Terminal, TerminalBuilder, TerminalSettings}; #[cfg(target_os = "macos")] use std::os::unix::ffi::OsStrExt; @@ -23,10 +23,14 @@ impl Project { )); } else { let settings = settings::get::(cx); + let automatically_activate_python_virtual_environment = settings + .automatically_activate_python_virtual_environment + .clone(); + let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( working_directory.clone(), - settings.shell.clone(), + shell.clone(), settings.env.clone(), Some(settings.blinking.clone()), settings.alternate_scroll, @@ -50,10 +54,13 @@ impl Project { }) .detach(); - let setting = settings::get::(cx); - - if setting.automatically_activate_python_virtual_environment { - self.set_up_python_virtual_environment(&terminal_handle, cx); + if automatically_activate_python_virtual_environment { + let activate_script_path = self.find_activate_script_path(&shell, cx); + self.activate_python_virtual_environment( + activate_script_path, + &terminal_handle, + cx, + ); } terminal_handle @@ -63,36 +70,35 @@ impl Project { } } - fn set_up_python_virtual_environment( - &mut self, - terminal_handle: &ModelHandle, - cx: &mut ModelContext, - ) { - let virtual_environment = self.find_python_virtual_environment(cx); - if let Some(virtual_environment) = virtual_environment { - // Paths are not strings so we need to jump through some hoops to format the command without `format!` - let mut command = Vec::from("source ".as_bytes()); - command.extend_from_slice(virtual_environment.as_os_str().as_bytes()); - command.push(b'\n'); - - terminal_handle.update(cx, |this, _| this.input_bytes(command)); - } - } - - pub fn find_python_virtual_environment( + pub fn find_activate_script_path( &mut self, + shell: &Shell, cx: &mut ModelContext, ) -> Option { - const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; + let program = match shell { + terminal::Shell::System => "Figure this out", + terminal::Shell::Program(program) => program, + terminal::Shell::WithArguments { program, args } => program, + }; + + // This is so hacky - find a better way to do this + let script_name = if program.contains("fish") { + "activate.fish" + } else { + "activate" + }; let worktree_paths = self .worktrees(cx) .map(|worktree| worktree.read(cx).abs_path()); + const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; + for worktree_path in worktree_paths { for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES { let mut path = worktree_path.join(virtual_environment_name); - path.push("bin/activate"); + path.push("bin/"); + path.push(script_name); if path.exists() { return Some(path); @@ -103,6 +109,22 @@ impl Project { None } + fn activate_python_virtual_environment( + &mut self, + activate_script: Option, + terminal_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + if let Some(activate_script) = activate_script { + // Paths are not strings so we need to jump through some hoops to format the command without `format!` + let mut command = Vec::from("source ".as_bytes()); + command.extend_from_slice(activate_script.as_os_str().as_bytes()); + command.push(b'\n'); + + terminal_handle.update(cx, |this, _| this.input_bytes(command)); + } + } + pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } From 7b170304dfe1ece11ba193867c43a90050026d10 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 04:07:10 -0400 Subject: [PATCH 03/67] Shorten setting name --- assets/settings/default.json | 2 +- crates/project/src/terminals.rs | 7 +++---- crates/terminal/src/terminal.rs | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f4d77f02cf5425888b5b238a2284586fefd4d09b..27be6ae5d28204eb29049ea2eaa650e55892fa4c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -348,7 +348,7 @@ // - Only works with Posix-complaint shells // - Only activates the first virtual environment it finds, regardless // of the nunber of projects in the workspace. - "automatically_activate_python_virtual_environment": false + "activate_python_virtual_environment": false }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 0fa525a82ce0a754d572d9f8ccf82c9530c9fcb2..539d120e971ecb562eaaa28fdccb6d084c49bba6 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -23,9 +23,8 @@ impl Project { )); } else { let settings = settings::get::(cx); - let automatically_activate_python_virtual_environment = settings - .automatically_activate_python_virtual_environment - .clone(); + let activate_python_virtual_environment = + settings.activate_python_virtual_environment.clone(); let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( @@ -54,7 +53,7 @@ impl Project { }) .detach(); - if automatically_activate_python_virtual_environment { + if activate_python_virtual_environment { let activate_script_path = self.find_activate_script_path(&shell, cx); self.activate_python_virtual_environment( activate_script_path, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9b0f0bbc860b113de7c2e933bfdab76b25c27e02..73ff09225f62c5c09185d074163f9eea631decf1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -158,7 +158,7 @@ pub struct TerminalSettings { pub dock: TerminalDockPosition, pub default_width: f32, pub default_height: f32, - pub automatically_activate_python_virtual_environment: bool, + pub activate_python_virtual_environment: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -177,7 +177,7 @@ pub struct TerminalSettingsContent { pub dock: Option, pub default_width: Option, pub default_height: Option, - pub automatically_activate_python_virtual_environment: Option, + pub activate_python_virtual_environment: Option, } impl TerminalSettings { From 9fe580acb675e573588e9451617310e26be8e9ea Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 25 Aug 2023 01:50:54 -0400 Subject: [PATCH 04/67] WIP --- crates/project/src/terminals.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 539d120e971ecb562eaaa28fdccb6d084c49bba6..2fb66a6c4c95b06acf4a305ec6652d2f2ee3a653 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -77,7 +77,7 @@ impl Project { let program = match shell { terminal::Shell::System => "Figure this out", terminal::Shell::Program(program) => program, - terminal::Shell::WithArguments { program, args } => program, + terminal::Shell::WithArguments { program, args: _ } => program, }; // This is so hacky - find a better way to do this From d34491e822e6344d8dc186a7a937aac7be4957ca Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 16 Aug 2023 14:21:26 +0300 Subject: [PATCH 05/67] Draft inlay hint part hover detection --- crates/editor/src/display_map.rs | 19 +++- crates/editor/src/element.rs | 122 +++++++++++++++++++++----- crates/editor/src/inlay_hint_cache.rs | 11 +++ 3 files changed, 125 insertions(+), 27 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aee41e6c53f0c1e35e68e3716db55f609a44fb08..9159253142e56c55ed1243e41a12ea79e7ca26ff 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -27,7 +27,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; -pub use self::inlay_map::Inlay; +pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FoldStatus { @@ -387,12 +387,25 @@ impl DisplaySnapshot { } fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point { + self.inlay_snapshot + .to_buffer_point(self.display_point_to_inlay_point(point, bias)) + } + + pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset { + self.inlay_snapshot + .to_offset(self.display_point_to_inlay_point(point, bias)) + } + + pub fn inlay_point_to_inlay_offset(&self, point: InlayPoint) -> InlayOffset { + self.inlay_snapshot.to_offset(point) + } + + fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { let block_point = point.0; let wrap_point = self.block_snapshot.to_wrap_point(block_point); let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0; - let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); - self.inlay_snapshot.to_buffer_point(inlay_point) + fold_point.to_inlay_point(&self.fold_snapshot) } pub fn max_point(&self) -> DisplayPoint { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9f74eed790fa6b025bdd12125858ad02ec44d8ac..e2aaa3a3bbfc60a6cc954dd93a261e878fdb2557 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayPoint, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -42,7 +42,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - ProjectPath, + InlayHintLabelPart, ProjectPath, }; use smallvec::SmallVec; use std::{ @@ -455,11 +455,67 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu - let point = position_to_display_point(position, text_bounds, position_map); - - update_go_to_definition_link(editor, point, cmd, shift, cx); - hover_at(editor, point, cx); + if text_bounds.contains_point(position) { + let (nearest_valid_position, unclipped_position) = + position_map.point_for_position(text_bounds, position); + if nearest_valid_position == unclipped_position { + update_go_to_definition_link(editor, Some(nearest_valid_position), cmd, shift, cx); + hover_at(editor, Some(nearest_valid_position), cx); + return true; + } else { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_position = position_map + .snapshot + .clip_point(unclipped_position, Bias::Left) + .to_point(&position_map.snapshot.display_snapshot); + let previous_valid_anchor = snapshot.anchor_at(previous_valid_position, Bias::Left); + let next_valid_position = position_map + .snapshot + .clip_point(unclipped_position, Bias::Right) + .to_point(&position_map.snapshot.display_snapshot); + let next_valid_anchor = snapshot.anchor_at(next_valid_position, Bias::Right); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) + { + if let Some(cached_hint) = editor + .inlay_hint_cache() + .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.label { + project::InlayHintLabel::String(regular_label) => { + // TODO kb remove + check for tooltip for hover and resolve, if needed + eprintln!("regular string: {regular_label}"); + } + project::InlayHintLabel::LabelParts(label_parts) => { + // TODO kb how to properly convert it? + let unclipped_inlay_position = InlayPoint::new( + unclipped_position.row(), + unclipped_position.column(), + ); + if let Some(hovered_hint_part) = find_hovered_hint_part( + &position_map.snapshot, + &label_parts, + previous_valid_position, + next_valid_position, + unclipped_inlay_position, + ) { + // TODO kb remove + check for tooltip and location and resolve, if needed + eprintln!("hint_part: {hovered_hint_part:?}"); + } + } + }; + } + } + } + }; + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); true } @@ -909,7 +965,7 @@ impl EditorElement { &text, cursor_row_layout.font_size(), &[( - text.len(), + text.chars().count(), RunStyle { font_id, color: style.background, @@ -1804,6 +1860,40 @@ impl EditorElement { } } +fn find_hovered_hint_part<'a>( + snapshot: &EditorSnapshot, + label_parts: &'a [InlayHintLabelPart], + hint_start: Point, + hint_end: Point, + hovered_position: InlayPoint, +) -> Option<&'a InlayHintLabelPart> { + let hint_start_offset = + snapshot.display_point_to_inlay_offset(hint_start.to_display_point(&snapshot), Bias::Left); + let hint_end_offset = + snapshot.display_point_to_inlay_offset(hint_end.to_display_point(&snapshot), Bias::Right); + dbg!(( + "~~~~~~~~~", + hint_start, + hint_start_offset, + hint_end, + hint_end_offset, + hovered_position + )); + let hovered_offset = snapshot.inlay_point_to_inlay_offset(hovered_position); + if hovered_offset >= hint_start_offset && hovered_offset <= hint_end_offset { + let mut hovered_character = (hovered_offset - hint_start_offset).0; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character >= part_len { + hovered_character -= part_len; + } else { + return Some(part); + } + } + } + None +} + struct HighlightedChunk<'a> { chunk: &'a str, style: Option, @@ -2663,6 +2753,7 @@ impl PositionMap { let mut target_point = DisplayPoint::new(row, column); let point = self.snapshot.clip_point(target_point, Bias::Left); + // TODO kb looks wrong, need to construct inlay point instead? operate offsets? *target_point.column_mut() += (x_overshoot / self.em_advance) as u32; (point, target_point) @@ -2919,23 +3010,6 @@ impl HighlightedRange { } } -fn position_to_display_point( - position: Vector2F, - text_bounds: RectF, - position_map: &PositionMap, -) -> Option { - if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } - } else { - None - } -} - fn range_to_bounds( range: &Range, content_origin: Vector2F, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 70cccf21da391d9308ece455d0de09768f68f280..5b9bdd08ec25d1d2c872fa74bce9d6ab865d2781 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -386,6 +386,17 @@ impl InlayHintCache { self.hints.clear(); } + pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { + self.hints + .get(&excerpt_id)? + .read() + .hints + .iter() + .find(|&(id, _)| id == &hint_id) + .map(|(_, hint)| hint) + .cloned() + } + pub fn hints(&self) -> Vec { let mut hints = Vec::new(); for excerpt_hints in self.hints.values() { From d1cb0b3c27fcbe1b02df3e4eae5426fb15802622 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 11:56:38 +0300 Subject: [PATCH 06/67] Properly detect hovered inlay hint label part --- crates/editor/src/element.rs | 221 +++++++++++++++++++---------------- 1 file changed, 119 insertions(+), 102 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e2aaa3a3bbfc60a6cc954dd93a261e878fdb2557..57384195519f801c78b841608f772bbde1994782 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayPoint, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayOffset, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -287,13 +287,13 @@ impl EditorElement { return false; } - let (position, target_position) = position_map.point_for_position(text_bounds, position); - + let point_for_position = position_map.point_for_position(text_bounds, position); + let position = point_for_position.previous_valid; if shift && alt { editor.select( SelectPhase::BeginColumnar { position, - goal_column: target_position.column(), + goal_column: point_for_position.exact_unclipped.column(), }, cx, ); @@ -329,9 +329,13 @@ impl EditorElement { if !text_bounds.contains_point(position) { return false; } - - let (point, _) = position_map.point_for_position(text_bounds, position); - mouse_context_menu::deploy_context_menu(editor, position, point, cx); + let point_for_position = position_map.point_for_position(text_bounds, position); + mouse_context_menu::deploy_context_menu( + editor, + position, + point_for_position.previous_valid, + cx, + ); true } @@ -353,9 +357,10 @@ impl EditorElement { } if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - - if point == target_point { + if let Some(point) = position_map + .point_for_position(text_bounds, position) + .as_valid() + { if shift { go_to_fetched_type_definition(editor, point, alt, cx); } else { @@ -383,12 +388,9 @@ impl EditorElement { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu let point = if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } + position_map + .point_for_position(text_bounds, position) + .as_valid() } else { None }; @@ -422,13 +424,12 @@ impl EditorElement { )) } - let (position, target_position) = - position_map.point_for_position(text_bounds, position); + let point_for_position = position_map.point_for_position(text_bounds, position); editor.select( SelectPhase::Update { - position, - goal_column: target_position.column(), + position: point_for_position.previous_valid, + goal_column: point_for_position.exact_unclipped.column(), scroll_position: (position_map.snapshot.scroll_position() + scroll_delta) .clamp(Vector2F::zero(), position_map.scroll_max), }, @@ -456,59 +457,74 @@ impl EditorElement { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu if text_bounds.contains_point(position) { - let (nearest_valid_position, unclipped_position) = - position_map.point_for_position(text_bounds, position); - if nearest_valid_position == unclipped_position { - update_go_to_definition_link(editor, Some(nearest_valid_position), cmd, shift, cx); - hover_at(editor, Some(nearest_valid_position), cx); + let point_for_position = position_map.point_for_position(text_bounds, position); + if let Some(point) = point_for_position.as_valid() { + update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + hover_at(editor, Some(point), cx); return true; } else { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let previous_valid_position = position_map + let hint_start_offset = position_map .snapshot - .clip_point(unclipped_position, Bias::Left) - .to_point(&position_map.snapshot.display_snapshot); - let previous_valid_anchor = snapshot.anchor_at(previous_valid_position, Bias::Left); - let next_valid_position = position_map + .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = position_map .snapshot - .clip_point(unclipped_position, Bias::Right) - .to_point(&position_map.snapshot.display_snapshot); - let next_valid_anchor = snapshot.anchor_at(next_valid_position, Bias::Right); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - if let Some(cached_hint) = editor - .inlay_hint_cache() - .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some(position_map.snapshot.display_point_to_inlay_offset( + point_for_position.exact_unclipped, + Bias::Left, + )) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + if let Some(hovered_offset) = hovered_offset { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_anchor = snapshot.anchor_at( + point_for_position + .previous_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Left, + ); + let next_valid_anchor = snapshot.anchor_at( + point_for_position + .next_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt() + }) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) { - match &cached_hint.label { - project::InlayHintLabel::String(regular_label) => { - // TODO kb remove + check for tooltip for hover and resolve, if needed - eprintln!("regular string: {regular_label}"); - } - project::InlayHintLabel::LabelParts(label_parts) => { - // TODO kb how to properly convert it? - let unclipped_inlay_position = InlayPoint::new( - unclipped_position.row(), - unclipped_position.column(), - ); - if let Some(hovered_hint_part) = find_hovered_hint_part( - &position_map.snapshot, - &label_parts, - previous_valid_position, - next_valid_position, - unclipped_inlay_position, - ) { - // TODO kb remove + check for tooltip and location and resolve, if needed - eprintln!("hint_part: {hovered_hint_part:?}"); + if let Some(cached_hint) = editor + .inlay_hint_cache() + .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.label { + project::InlayHintLabel::String(regular_label) => { + // TODO kb remove + check for tooltip for hover and resolve, if needed + eprintln!("regular string: {regular_label}"); } - } - }; + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some(hovered_hint_part) = find_hovered_hint_part( + &label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) { + // TODO kb remove + check for tooltip and location and resolve, if needed + eprintln!("hint_part: {hovered_hint_part:?}"); + } + } + }; + } } } } @@ -1861,27 +1877,12 @@ impl EditorElement { } fn find_hovered_hint_part<'a>( - snapshot: &EditorSnapshot, label_parts: &'a [InlayHintLabelPart], - hint_start: Point, - hint_end: Point, - hovered_position: InlayPoint, + hint_range: Range, + hovered_offset: InlayOffset, ) -> Option<&'a InlayHintLabelPart> { - let hint_start_offset = - snapshot.display_point_to_inlay_offset(hint_start.to_display_point(&snapshot), Bias::Left); - let hint_end_offset = - snapshot.display_point_to_inlay_offset(hint_end.to_display_point(&snapshot), Bias::Right); - dbg!(( - "~~~~~~~~~", - hint_start, - hint_start_offset, - hint_end, - hint_end_offset, - hovered_position - )); - let hovered_offset = snapshot.inlay_point_to_inlay_offset(hovered_position); - if hovered_offset >= hint_start_offset && hovered_offset <= hint_end_offset { - let mut hovered_character = (hovered_offset - hint_start_offset).0; + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { @@ -2722,22 +2723,32 @@ struct PositionMap { snapshot: EditorSnapshot, } +#[derive(Debug)] +struct PointForPosition { + previous_valid: DisplayPoint, + next_valid: DisplayPoint, + exact_unclipped: DisplayPoint, + column_overshoot_after_line_end: u32, +} + +impl PointForPosition { + fn as_valid(&self) -> Option { + if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { + Some(self.previous_valid) + } else { + None + } + } +} + impl PositionMap { - /// Returns two display points: - /// 1. The nearest *valid* position in the editor - /// 2. An unclipped, potentially *invalid* position that maps directly to - /// the given pixel position. - fn point_for_position( - &self, - text_bounds: RectF, - position: Vector2F, - ) -> (DisplayPoint, DisplayPoint) { + fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition { let scroll_position = self.snapshot.scroll_position(); let position = position - text_bounds.origin(); let y = position.y().max(0.0).min(self.size.y()); let x = position.x() + (scroll_position.x() * self.em_width); let row = (y / self.line_height + scroll_position.y()) as u32; - let (column, x_overshoot) = if let Some(line) = self + let (column, x_overshoot_after_line_end) = if let Some(line) = self .line_layouts .get(row as usize - scroll_position.y() as usize) .map(|line_with_spaces| &line_with_spaces.line) @@ -2751,12 +2762,18 @@ impl PositionMap { (0, x) }; - let mut target_point = DisplayPoint::new(row, column); - let point = self.snapshot.clip_point(target_point, Bias::Left); - // TODO kb looks wrong, need to construct inlay point instead? operate offsets? - *target_point.column_mut() += (x_overshoot / self.em_advance) as u32; - - (point, target_point) + let mut exact_unclipped = DisplayPoint::new(row, column); + let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left); + let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right); + + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32; + *exact_unclipped.column_mut() += column_overshoot_after_line_end; + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end, + } } } From e4b78e322edfea72039d3b2afce0646c4946d9fd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 12:37:05 +0300 Subject: [PATCH 07/67] Revert "Strip off inlay hints data that should be resolved" Without holding all hints in host's cache, this is impossile. Currenly, we keep hint caches separate and isolated, so this will not work when we actually resolve. --- crates/lsp/src/lsp.rs | 4 +-- crates/project/src/lsp_command.rs | 56 +++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e0ae64d8069c08b12e11b8b12155892dc974ae0d..78c858a90c46e2af349027ed3f4f361fe8805752 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,9 +434,7 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: Some(InlayHintResolveClientCapabilities { - properties: vec!["textEdits".to_string(), "tooltip".to_string()], - }), + resolve_support: None, dynamic_registration: Some(false), }), ..Default::default() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a8692257d8032fdca5667c2089249e806b241e34..08261b64f17c8714d1305fbff303080ddc82f0ab 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1954,7 +1954,7 @@ impl LspCommand for InlayHints { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - _: &mut AppContext, + cx: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response @@ -1963,17 +1963,51 @@ impl LspCommand for InlayHints { position: Some(language::proto::serialize_anchor(&response_hint.position)), padding_left: response_hint.padding_left, padding_right: response_hint.padding_right, - kind: response_hint.kind.map(|kind| kind.name().to_string()), - // Do not pass extra data such as tooltips to clients: host can put tooltip data from the cache during resolution. - tooltip: None, - // Similarly, do not pass label parts to clients: host can return a detailed list during resolution. label: Some(proto::InlayHintLabel { - label: Some(proto::inlay_hint_label::Label::Value( - match response_hint.label { - InlayHintLabel::String(s) => s, - InlayHintLabel::LabelParts(_) => response_hint.text(), - }, - )), + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } }), }) .collect(), From 3434990b70cf79772d7f3f9d9f47a2b385fc2cb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 15:54:30 +0300 Subject: [PATCH 08/67] Store inlay hint resolve data --- crates/editor/src/display_map/inlay_map.rs | 6 +- crates/project/src/lsp_command.rs | 149 ++++++++++++++------- crates/project/src/project.rs | 16 +++ crates/rpc/proto/zed.proto | 16 +++ 4 files changed, 134 insertions(+), 53 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 9794ac45c1190ec88ccb471ee61630ec50d320ca..3cea513dea6c0c647d56afc9ff144b274dca350e 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1109,7 +1109,7 @@ mod tests { use super::*; use crate::{InlayId, MultiBuffer}; use gpui::AppContext; - use project::{InlayHint, InlayHintLabel}; + use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; @@ -1131,6 +1131,7 @@ mod tests { padding_right: false, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1151,6 +1152,7 @@ mod tests { padding_right: true, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1171,6 +1173,7 @@ mod tests { padding_right: false, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1191,6 +1194,7 @@ mod tests { padding_right: true, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 08261b64f17c8714d1305fbff303080ddc82f0ab..d46ba4f5f71fec1f471882b4b8e4a05f6ebc1400 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,7 +1,7 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, - MarkupContent, Project, ProjectTransaction, + MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; @@ -1817,7 +1817,8 @@ impl LspCommand for InlayHints { server_id: LanguageServerId, mut cx: AsyncAppContext, ) -> Result> { - let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + let (lsp_adapter, lsp_server) = + language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; // `typescript-language-server` adds padding to the left for type hints, turning // `const foo: boolean` into `const foo : boolean` which looks odd. // `rust-analyzer` does not have the padding for this case, and we have to accomodate both. @@ -1833,6 +1834,15 @@ impl LspCommand for InlayHints { .unwrap_or_default() .into_iter() .map(|lsp_hint| { + let resolve_state = match lsp_server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( + lsp::InlayHintOptions { + resolve_provider: Some(true), + .. + }, + ))) => ResolveState::CanResolve(lsp_hint.data), + _ => ResolveState::Resolved, + }; let kind = lsp_hint.kind.and_then(|kind| match kind { lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), @@ -1910,6 +1920,7 @@ impl LspCommand for InlayHints { }) } }), + resolve_state, } }) .collect()) @@ -1959,57 +1970,69 @@ impl LspCommand for InlayHints { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| proto::InlayHint { - position: Some(language::proto::serialize_anchor(&response_hint.position)), - padding_left: response_hint.padding_left, - padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() - }) - } + .map(|response_hint| { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::CanResolve(resolve_data) => { + (0, resolve_data.map(|json_data| serde_json::to_string(&json_data).expect("failed to serialize resolve json data")).map(|value| proto::resolve_state::LspResolveState{ value })) + } + ResolveState::Resolved => (1, None), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, lsp_resolve_state + }); + proto::InlayHint { + position: Some(language::proto::serialize_anchor(&response_hint.position)), + padding_left: response_hint.padding_left, + padding_right: response_hint.padding_right, + label: Some(proto::InlayHintLabel { + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), }), - }), - kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }, - ) + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } - }), - }) + }), + resolve_state, + }}) .collect(), version: serialize_version(buffer_version), } @@ -2021,7 +2044,7 @@ impl LspCommand for InlayHints { project: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, - ) -> Result> { + ) -> anyhow::Result> { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) @@ -2035,6 +2058,27 @@ impl LspCommand for InlayHints { .as_ref() .and_then(|location| location.buffer_id) .context("missing buffer id")?; + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!( + "incorrect proto inlay hint message: no resolve state in hint {message_hint:?}", + ) + }); + + let lsp_resolve_state = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => ResolveState::CanResolve(lsp_resolve_state), + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; let hint = InlayHint { buffer_id, position: message_hint @@ -2103,6 +2147,7 @@ impl LspCommand for InlayHints { } }) }), + resolve_state, }; hints.push(hint); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 49074268f21a11cc3b57b43ae2e4409c749df6d2..1628234f98e077a92aae72c5ebd0a2c77b42e5c1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -342,6 +342,22 @@ pub struct InlayHint { pub padding_left: bool, pub padding_right: bool, pub tooltip: Option, + pub resolve_state: ResolveState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolveState { + Resolved, + CanResolve(Option), + Resolving, +} + +impl Hash for ResolveState { + fn hash(&self, state: &mut H) { + // Regular `lsp::LSPAny` is not hashable, so we can't hash it. + // LSP expects this data to not to change between requests, so we only hash the discriminant. + std::mem::discriminant(self).hash(state); + } } impl InlayHint { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f032ccce513de5feacae35e9980acb18c864d6c8..d0964064ab1e471f5150b53a4a23e96311e95798 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -754,6 +754,7 @@ message InlayHint { bool padding_left = 4; bool padding_right = 5; InlayHintTooltip tooltip = 6; + ResolveState resolve_state = 7; } message InlayHintLabel { @@ -787,6 +788,21 @@ message InlayHintLabelPartTooltip { } } +message ResolveState { + State state = 1; + LspResolveState lsp_resolve_state = 2; + + enum State { + Resolved = 0; + CanResolve = 1; + Resolving = 2; + } + + message LspResolveState { + string value = 1; + } +} + message RefreshInlayHints { uint64 project_id = 1; } From 80e871424194fb8c018d5d85d0d5182492a75238 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 21:09:04 +0300 Subject: [PATCH 09/67] Send inlay hint resolve requests --- crates/editor/src/display_map/inlay_map.rs | 4 - crates/editor/src/element.rs | 177 +++--- crates/editor/src/inlay_hint_cache.rs | 81 ++- crates/project/src/lsp_command.rs | 624 +++++++++++++-------- crates/project/src/project.rs | 140 ++++- crates/rpc/proto/zed.proto | 16 +- crates/rpc/src/proto.rs | 4 + crates/rpc/src/rpc.rs | 2 +- 8 files changed, 705 insertions(+), 343 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 3cea513dea6c0c647d56afc9ff144b274dca350e..026f1fc2c20a789319a4834dbd181bc61b7420ee 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1125,7 +1125,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: false, padding_right: false, @@ -1146,7 +1145,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: true, padding_right: true, @@ -1167,7 +1165,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: false, padding_right: false, @@ -1188,7 +1185,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: true, padding_right: true, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 57384195519f801c78b841608f772bbde1994782..2d2191e77fbacbcfaa2df13e79f4c7568e511a23 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -42,7 +42,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, ProjectPath, + InlayHintLabelPart, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -456,82 +456,21 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu + let mut go_to_definition_point = None; + let mut hover_at_point = None; if text_bounds.contains_point(position) { let point_for_position = position_map.point_for_position(text_bounds, position); if let Some(point) = point_for_position.as_valid() { - update_go_to_definition_link(editor, Some(point), cmd, shift, cx); - hover_at(editor, Some(point), cx); - return true; + go_to_definition_point = Some(point); + hover_at_point = Some(point); } else { - let hint_start_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); - let hint_end_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { - Some(position_map.snapshot.display_point_to_inlay_offset( - point_for_position.exact_unclipped, - Bias::Left, - )) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) - } else { - None - }; - if let Some(hovered_offset) = hovered_offset { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let previous_valid_anchor = snapshot.anchor_at( - point_for_position - .previous_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Left, - ); - let next_valid_anchor = snapshot.anchor_at( - point_for_position - .next_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Right, - ); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| { - hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt() - }) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - if let Some(cached_hint) = editor - .inlay_hint_cache() - .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) - { - match &cached_hint.label { - project::InlayHintLabel::String(regular_label) => { - // TODO kb remove + check for tooltip for hover and resolve, if needed - eprintln!("regular string: {regular_label}"); - } - project::InlayHintLabel::LabelParts(label_parts) => { - if let Some(hovered_hint_part) = find_hovered_hint_part( - &label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) { - // TODO kb remove + check for tooltip and location and resolve, if needed - eprintln!("hint_part: {hovered_hint_part:?}"); - } - } - }; - } - } - } + (go_to_definition_point, hover_at_point) = + inlay_link_and_hover_points(position_map, point_for_position, editor, cx); } }; - update_go_to_definition_link(editor, None, cmd, shift, cx); - hover_at(editor, None, cx); + update_go_to_definition_link(editor, go_to_definition_point, cmd, shift, cx); + hover_at(editor, hover_at_point, cx); true } @@ -1876,6 +1815,104 @@ impl EditorElement { } } +fn inlay_link_and_hover_points( + position_map: &PositionMap, + point_for_position: PointForPosition, + editor: &mut Editor, + cx: &mut ViewContext<'_, '_, Editor>, +) -> (Option, Option) { + let hint_start_offset = position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some( + position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left), + ) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + let mut go_to_definition_point = None; + let mut hover_at_point = None; + if let Some(hovered_offset) = hovered_offset { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_anchor = snapshot.anchor_at( + point_for_position + .previous_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Left, + ); + let next_valid_anchor = snapshot.anchor_at( + point_for_position + .next_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + if let Some(cached_hint) = + inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + previous_valid_anchor.excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + match &cached_hint.label { + project::InlayHintLabel::String(_) => { + if cached_hint.tooltip.is_some() { + dbg!(&cached_hint.tooltip); // TODO kb + // hover_at_point = Some(hovered_offset); + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some(hovered_hint_part) = find_hovered_hint_part( + &label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) { + if hovered_hint_part.tooltip.is_some() { + dbg!(&hovered_hint_part.tooltip); // TODO kb + // hover_at_point = Some(hovered_offset); + } + if let Some(location) = &hovered_hint_part.location { + dbg!(location); // TODO kb + // go_to_definition_point = Some(location); + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + } + + (go_to_definition_point, hover_at_point) +} + fn find_hovered_hint_part<'a>( label_parts: &'a [InlayHintLabelPart], hint_range: Range, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 5b9bdd08ec25d1d2c872fa74bce9d6ab865d2781..52a4039a76e6b31dd9e46f13de627591e5b916f8 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -13,7 +13,7 @@ use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; use parking_lot::RwLock; -use project::InlayHint; +use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; @@ -60,7 +60,7 @@ struct ExcerptHintsUpdate { excerpt_id: ExcerptId, remove_from_visible: Vec, remove_from_cache: HashSet, - add_to_cache: HashSet, + add_to_cache: Vec, } #[derive(Debug, Clone, Copy)] @@ -409,6 +409,79 @@ impl InlayHintCache { pub fn version(&self) -> usize { self.version } + + pub fn spawn_hint_resolve( + &self, + buffer_id: u64, + excerpt_id: ExcerptId, + id: InlayId, + cx: &mut ViewContext<'_, '_, Editor>, + ) { + if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { + let hint_to_resolve = cached_hint.clone(); + let server_id = *server_id; + cached_hint.resolve_state = ResolveState::Resolving; + drop(guard); + cx.spawn(|editor, mut cx| async move { + let resolved_hint_task = editor.update(&mut cx, |editor, cx| { + editor + .buffer() + .read(cx) + .buffer(buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.resolve_inlay_hint( + hint_to_resolve, + buffer, + server_id, + cx, + ) + })) + }) + })?; + if let Some(resolved_hint_task) = resolved_hint_task { + if let Some(mut resolved_hint) = + resolved_hint_task.await.context("hint resolve task")? + { + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if cached_hint.resolve_state == ResolveState::Resolving + { + resolved_hint.resolve_state = + ResolveState::Resolved; + *cached_hint = resolved_hint; + } + } + } + })?; + } + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + } + } } fn spawn_new_update_tasks( @@ -632,7 +705,7 @@ fn calculate_hint_updates( cached_excerpt_hints: Option>>, visible_hints: &[Inlay], ) -> Option { - let mut add_to_cache: HashSet = HashSet::default(); + let mut add_to_cache = Vec::::new(); let mut excerpt_hints_to_persist = HashMap::default(); for new_hint in new_excerpt_hints { if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) { @@ -659,7 +732,7 @@ fn calculate_hint_updates( None => true, }; if missing_from_cache { - add_to_cache.insert(new_hint); + add_to_cache.push(new_hint); } } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d46ba4f5f71fec1f471882b4b8e4a05f6ebc1400..7b4d689a81142857ee5153028f7521a65f7388c8 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,6 +1,6 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, - InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, + InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Item, Location, LocationLink, MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; @@ -12,8 +12,9 @@ use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, - Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, + CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1776,6 +1777,371 @@ impl LspCommand for OnTypeFormatting { } } +impl InlayHints { + pub fn lsp_to_project_hint( + lsp_hint: lsp::InlayHint, + buffer_handle: &ModelHandle, + resolve_state: ResolveState, + force_no_type_left_padding: bool, + cx: &AppContext, + ) -> InlayHint { + let kind = lsp_hint.kind.and_then(|kind| match kind { + lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), + lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), + _ => None, + }); + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { + false + } else { + lsp_hint.padding_left.unwrap_or(false) + }; + InlayHint { + position: if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + }, + padding_left, + padding_right: lsp_hint.padding_right.unwrap_or(false), + label: match lsp_hint.label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => InlayHintLabel::LabelParts( + lsp_parts + .into_iter() + .map(|label_part| InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location: label_part.location.map(|lsp_location| { + let target_start = buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + Location { + buffer: buffer_handle.clone(), + range: buffer.anchor_after(target_start) + ..buffer.anchor_before(target_end), + } + }), + }) + .collect(), + ), + }, + kind, + tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), + lsp::InlayHintTooltip::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + resolve_state, + } + } + + pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::CanResolve(server_id, resolve_data) => ( + 0, + resolve_data + .map(|json_data| { + serde_json::to_string(&json_data) + .expect("failed to serialize resolve json data") + }) + .map(|value| proto::resolve_state::LspResolveState { + server_id: server_id.0 as u64, + value, + }), + ), + ResolveState::Resolved => (1, None), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, + lsp_resolve_state, + }); + proto::InlayHint { + position: Some(language::proto::serialize_anchor(&response_hint.position)), + padding_left: response_hint.padding_left, + padding_right: response_hint.padding_right, + label: Some(proto::InlayHintLabel { + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } + }), + resolve_state, + } + } + + pub async fn proto_to_project_hint( + message_hint: proto::InlayHint, + project: &ModelHandle, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let buffer_id = message_hint + .position + .as_ref() + .and_then(|location| location.buffer_id) + .context("missing buffer id")?; + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) + }); + let resolve_state_data = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::>(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + .map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state)) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => { + let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| { + format!( + "No lsp resolve data for the hint that can be resolved: {message_hint:?}" + ) + })?; + ResolveState::CanResolve(server_id, lsp_resolve_state) + } + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; + Ok(InlayHint { + position: message_hint + .position + .and_then(language::proto::deserialize_anchor) + .context("invalid position")?, + label: match message_hint + .label + .and_then(|label| label.label) + .context("missing label")? + { + proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), + proto::inlay_hint_label::Label::LabelParts(parts) => { + let mut label_parts = Vec::new(); + for part in parts.parts { + let buffer = project + .update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx)) + .await?; + label_parts.push(InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.map(|tooltip| match tooltip.content { + Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => { + InlayHintLabelPartTooltip::String(s) + } + Some( + proto::inlay_hint_label_part_tooltip::Content::MarkupContent( + markup_content, + ), + ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }), + None => InlayHintLabelPartTooltip::String(String::new()), + }), + location: match part.location { + Some(location) => Some(Location { + range: location + .start + .and_then(language::proto::deserialize_anchor) + .context("invalid start")? + ..location + .end + .and_then(language::proto::deserialize_anchor) + .context("invalid end")?, + buffer, + }), + None => None, + }, + }); + } + + InlayHintLabel::LabelParts(label_parts) + } + }, + padding_left: message_hint.padding_left, + padding_right: message_hint.padding_right, + kind: message_hint + .kind + .as_deref() + .and_then(InlayHintKind::from_name), + tooltip: message_hint.tooltip.and_then(|tooltip| { + Some(match tooltip.content? { + proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), + proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }) + } + }) + }), + resolve_state, + }) + } + + // TODO kb instead, store all LSP data inside the project::InlayHint? + pub fn project_to_lsp_hint( + hint: InlayHint, + project: &ModelHandle, + snapshot: &BufferSnapshot, + cx: &AsyncAppContext, + ) -> lsp::InlayHint { + lsp::InlayHint { + position: point_to_lsp(hint.position.to_point_utf16(snapshot)), + kind: hint.kind.map(|kind| match kind { + InlayHintKind::Type => lsp::InlayHintKind::TYPE, + InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER, + }), + text_edits: None, + tooltip: hint.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s), + InlayHintTooltip::MarkupContent(markup_content) => { + lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => lsp::MarkupKind::PlainText, + HoverBlockKind::Markdown => lsp::MarkupKind::Markdown, + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }) + } + }) + }), + label: match hint.label { + InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s), + InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts( + label_parts + .into_iter() + .map(|part| lsp::InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintLabelPartTooltip::String(s) => { + lsp::InlayHintLabelPartTooltip::String(s) + } + InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => { + lsp::MarkupKind::PlainText + } + HoverBlockKind::Markdown => { + lsp::MarkupKind::Markdown + } + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }, + ) + } + }) + }), + location: part.location.and_then(|location| { + let path = cx.read(|cx| { + let project_path = location.buffer.read(cx).project_path(cx)?; + project.read(cx).absolute_path(&project_path, cx) + })?; + Some(lsp::Location::new( + lsp::Url::from_file_path(path).unwrap(), + range_to_lsp( + location.range.start.to_point_utf16(snapshot) + ..location.range.end.to_point_utf16(snapshot), + ), + )) + }), + command: None, + }) + .collect(), + ), + }, + padding_left: Some(hint.padding_left), + padding_right: Some(hint.padding_right), + data: match hint.resolve_state { + ResolveState::CanResolve(_, data) => data, + ResolveState::Resolving | ResolveState::Resolved => None, + }, + } + } +} + #[async_trait(?Send)] impl LspCommand for InlayHints { type Response = Vec; @@ -1829,7 +2195,6 @@ impl LspCommand for InlayHints { let force_no_type_left_padding = lsp_adapter.name.0.as_ref() == "typescript-language-server"; cx.read(|cx| { - let origin_buffer = buffer.read(cx); Ok(message .unwrap_or_default() .into_iter() @@ -1840,88 +2205,18 @@ impl LspCommand for InlayHints { resolve_provider: Some(true), .. }, - ))) => ResolveState::CanResolve(lsp_hint.data), + ))) => { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } _ => ResolveState::Resolved, }; - let kind = lsp_hint.kind.and_then(|kind| match kind { - lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), - lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), - _ => None, - }); - let position = origin_buffer - .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); - let padding_left = - if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { - false - } else { - lsp_hint.padding_left.unwrap_or(false) - }; - InlayHint { - buffer_id: origin_buffer.remote_id(), - position: if kind == Some(InlayHintKind::Parameter) { - origin_buffer.anchor_before(position) - } else { - origin_buffer.anchor_after(position) - }, - padding_left, - padding_right: lsp_hint.padding_right.unwrap_or(false), - label: match lsp_hint.label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => { - InlayHintLabel::LabelParts( - lsp_parts - .into_iter() - .map(|label_part| InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map( - |tooltip| { - match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent( - markup_content, - ) => InlayHintLabelPartTooltip::MarkupContent( - MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }, - ), - } - }, - ), - location: label_part.location.map(|lsp_location| { - let target_start = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(target_start) - ..origin_buffer.anchor_before(target_end), - } - }), - }) - .collect(), - ) - } - }, - kind, - tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), - lsp::InlayHintTooltip::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }) - } - }), + InlayHints::lsp_to_project_hint( + lsp_hint, + &buffer, resolve_state, - } + force_no_type_left_padding, + cx, + ) }) .collect()) }) @@ -1970,69 +2265,7 @@ impl LspCommand for InlayHints { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| { - let (state, lsp_resolve_state) = match response_hint.resolve_state { - ResolveState::CanResolve(resolve_data) => { - (0, resolve_data.map(|json_data| serde_json::to_string(&json_data).expect("failed to serialize resolve json data")).map(|value| proto::resolve_state::LspResolveState{ value })) - } - ResolveState::Resolved => (1, None), - ResolveState::Resolving => (2, None), - }; - let resolve_state = Some(proto::ResolveState { - state, lsp_resolve_state - }); - proto::InlayHint { - position: Some(language::proto::serialize_anchor(&response_hint.position)), - padding_left: response_hint.padding_left, - padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() - }) - } - }), - }), - kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }, - ) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } - }), - resolve_state, - }}) + .map(|response_hint| InlayHints::project_to_proto_hint(response_hint, cx)) .collect(), version: serialize_version(buffer_version), } @@ -2053,104 +2286,7 @@ impl LspCommand for InlayHints { let mut hints = Vec::new(); for message_hint in message.hints { - let buffer_id = message_hint - .position - .as_ref() - .and_then(|location| location.buffer_id) - .context("missing buffer id")?; - let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { - panic!( - "incorrect proto inlay hint message: no resolve state in hint {message_hint:?}", - ) - }); - - let lsp_resolve_state = resolve_state - .lsp_resolve_state.as_ref() - .map(|lsp_resolve_state| { - serde_json::from_str::(&lsp_resolve_state.value) - .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) - }) - .transpose()?; - let resolve_state = match resolve_state.state { - 0 => ResolveState::Resolved, - 1 => ResolveState::CanResolve(lsp_resolve_state), - 2 => ResolveState::Resolving, - invalid => { - anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") - } - }; - let hint = InlayHint { - buffer_id, - position: message_hint - .position - .and_then(language::proto::deserialize_anchor) - .context("invalid position")?, - label: match message_hint - .label - .and_then(|label| label.label) - .context("missing label")? - { - proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), - proto::inlay_hint_label::Label::LabelParts(parts) => { - let mut label_parts = Vec::new(); - for part in parts.parts { - label_parts.push(InlayHintLabelPart { - value: part.value, - tooltip: part.tooltip.map(|tooltip| match tooltip.content { - Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s), - Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - None => InlayHintLabelPartTooltip::String(String::new()), - }), - location: match part.location { - Some(location) => { - let target_buffer = project - .update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(location.buffer_id, cx) - }) - .await?; - Some(Location { - range: location - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")? - ..location - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?, - buffer: target_buffer, - })}, - None => None, - }, - }); - } - - InlayHintLabel::LabelParts(label_parts) - } - }, - padding_left: message_hint.padding_left, - padding_right: message_hint.padding_right, - kind: message_hint - .kind - .as_deref() - .and_then(InlayHintKind::from_name), - tooltip: message_hint.tooltip.and_then(|tooltip| { - Some(match tooltip.content? { - proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), - proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }) - } - }) - }), - resolve_state, - }; - - hints.push(hint); + hints.push(InlayHints::proto_to_project_hint(message_hint, &project, &mut cx).await?); } Ok(hints) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1628234f98e077a92aae72c5ebd0a2c77b42e5c1..65be7cceb10ef3a1fea39ba908d239b3948085bf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -333,9 +333,8 @@ pub struct Location { pub range: Range, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { - pub buffer_id: u64, pub position: language::Anchor, pub label: InlayHintLabel, pub kind: Option, @@ -348,18 +347,10 @@ pub struct InlayHint { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResolveState { Resolved, - CanResolve(Option), + CanResolve(LanguageServerId, Option), Resolving, } -impl Hash for ResolveState { - fn hash(&self, state: &mut H) { - // Regular `lsp::LSPAny` is not hashable, so we can't hash it. - // LSP expects this data to not to change between requests, so we only hash the discriminant. - std::mem::discriminant(self).hash(state); - } -} - impl InlayHint { pub fn text(&self) -> String { match &self.label { @@ -369,34 +360,34 @@ impl InlayHint { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabel { String(String), LabelParts(Vec), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHintLabelPart { pub value: String, pub tooltip: Option, pub location: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabelPartTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct MarkupContent { - pub kind: String, + pub kind: HoverBlockKind, pub value: String, } @@ -430,7 +421,7 @@ pub struct HoverBlock { pub kind: HoverBlockKind, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum HoverBlockKind { PlainText, Markdown, @@ -567,6 +558,7 @@ impl Project { client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_on_type_formatting); client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_resolve_inlay_hint); client.add_model_request_handler(Self::handle_refresh_inlay_hints); client.add_model_request_handler(Self::handle_reload_buffers); client.add_model_request_handler(Self::handle_synchronize_buffers); @@ -4985,7 +4977,7 @@ impl Project { buffer_handle: ModelHandle, range: Range, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); let range_start = range.start; @@ -5035,6 +5027,79 @@ impl Project { } } + pub fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: ModelHandle, + server_id: LanguageServerId, + cx: &mut ModelContext, + ) -> Task>> { + if self.is_local() { + let buffer = buffer_handle.read(cx); + let (_, lang_server) = if let Some((adapter, server)) = + self.language_server_for_buffer(buffer, server_id, cx) + { + (adapter.clone(), server.clone()) + } else { + return Task::ready(Ok(None)); + }; + let can_resolve = lang_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { + return Task::ready(Ok(None)); + } + + let buffer_snapshot = buffer.snapshot(); + cx.spawn(|project, cx| async move { + let resolve_task = lang_server.request::( + InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), + ); + let resolved_hint = resolve_task + .await + .context("inlay hint resolve LSP request")?; + let resolved_hint = cx.read(|cx| { + InlayHints::lsp_to_project_hint( + resolved_hint, + &buffer_handle, + ResolveState::Resolved, + false, + cx, + ) + }); + Ok(Some(resolved_hint)) + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::ResolveInlayHint { + project_id, + buffer_id: buffer_handle.read(cx).remote_id(), + language_server_id: server_id.0 as u64, + hint: Some(InlayHints::project_to_proto_hint(hint, cx)), + }; + cx.spawn(|project, mut cx| async move { + let response = client + .request(request) + .await + .context("inlay hints proto request")?; + match response.hint { + Some(resolved_hint) => { + InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) + .await + .map(Some) + .context("inlay hints proto response conversion") + } + None => Ok(None), + } + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + #[allow(clippy::type_complexity)] pub fn search( &self, @@ -6832,6 +6897,43 @@ impl Project { })) } + async fn handle_resolve_inlay_hint( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let proto_hint = envelope + .payload + .hint + .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); + let hint = InlayHints::proto_to_project_hint(proto_hint, &this, &mut cx) + .await + .context("resolved proto inlay hint conversion")?; + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade(cx)) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) + })?; + let resolved_hint = this + .update(&mut cx, |project, cx| { + project.resolve_inlay_hint( + hint, + buffer, + LanguageServerId(envelope.payload.language_server_id as usize), + cx, + ) + }) + .await + .context("inlay hints fetch")? + .map(|hint| cx.read(|cx| InlayHints::project_to_proto_hint(hint, cx))); + + Ok(proto::ResolveInlayHintResponse { + hint: resolved_hint, + }) + } + async fn handle_refresh_inlay_hints( this: ModelHandle, _: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d0964064ab1e471f5150b53a4a23e96311e95798..dbd700e264af7c1c3f89b014bbf9ad9476fd5536 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -128,6 +128,8 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; + ResolveInlayHint resolve_inlay_hint = 131; + ResolveInlayHintResponse resolve_inlay_hint_response = 132; RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; @@ -800,15 +802,27 @@ message ResolveState { message LspResolveState { string value = 1; + uint64 server_id = 2; } } +message ResolveInlayHint { + uint64 project_id = 1; + uint64 buffer_id = 2; + uint64 language_server_id = 3; + InlayHint hint = 4; +} + +message ResolveInlayHintResponse { + InlayHint hint = 1; +} + message RefreshInlayHints { uint64 project_id = 1; } message MarkupContent { - string kind = 1; + bool is_markdown = 1; string value = 2; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f0f49c6230c0229c2067c9c8fcd49ba9bf850795..2e4dce01e1a3bf5789206c80b3a4574f6e198c0d 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -197,6 +197,8 @@ messages!( (OnTypeFormattingResponse, Background), (InlayHints, Background), (InlayHintsResponse, Background), + (ResolveInlayHint, Background), + (ResolveInlayHintResponse, Background), (RefreshInlayHints, Foreground), (Ping, Foreground), (PrepareRename, Background), @@ -299,6 +301,7 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (OnTypeFormatting, OnTypeFormattingResponse), (InlayHints, InlayHintsResponse), + (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), @@ -355,6 +358,7 @@ entity_messages!( PerformRename, OnTypeFormatting, InlayHints, + ResolveInlayHint, RefreshInlayHints, PrepareRename, ReloadBuffers, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 3cb8b6bffa2ca1549ca854db39e46ef8fc8634a7..bc9dd6f80ba039bb705e3d1518c737ba56c969b9 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 = 60; +pub const PROTOCOL_VERSION: u32 = 61; From ac86bbac75b7f0347b9f1619f247c388bf8aea19 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 16:25:01 +0300 Subject: [PATCH 10/67] Prepare for hover functionality refactoring --- crates/editor/src/element.rs | 37 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2d2191e77fbacbcfaa2df13e79f4c7568e511a23..405a6e1a08348dbeb78b60c106d6e3080f9f81dd 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -456,21 +456,27 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu - let mut go_to_definition_point = None; - let mut hover_at_point = None; if text_bounds.contains_point(position) { let point_for_position = position_map.point_for_position(text_bounds, position); - if let Some(point) = point_for_position.as_valid() { - go_to_definition_point = Some(point); - hover_at_point = Some(point); - } else { - (go_to_definition_point, hover_at_point) = - inlay_link_and_hover_points(position_map, point_for_position, editor, cx); + match point_for_position.as_valid() { + Some(point) => { + update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + hover_at(editor, Some(point), cx); + } + None => { + update_inlay_link_and_hover_points( + position_map, + point_for_position, + editor, + cx, + ); + } } - }; + } else { + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); + } - update_go_to_definition_link(editor, go_to_definition_point, cmd, shift, cx); - hover_at(editor, hover_at_point, cx); true } @@ -1815,12 +1821,13 @@ impl EditorElement { } } -fn inlay_link_and_hover_points( +// TODO kb implement +fn update_inlay_link_and_hover_points( position_map: &PositionMap, point_for_position: PointForPosition, editor: &mut Editor, cx: &mut ViewContext<'_, '_, Editor>, -) -> (Option, Option) { +) { let hint_start_offset = position_map .snapshot .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); @@ -1839,8 +1846,6 @@ fn inlay_link_and_hover_points( } else { None }; - let mut go_to_definition_point = None; - let mut hover_at_point = None; if let Some(hovered_offset) = hovered_offset { let buffer = editor.buffer().read(cx); let snapshot = buffer.snapshot(cx); @@ -1909,8 +1914,6 @@ fn inlay_link_and_hover_points( } } } - - (go_to_definition_point, hover_at_point) } fn find_hovered_hint_part<'a>( From 7eab18ec897738b7e6e5a8b71f15bcc3a3af4fe5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 17:54:34 +0300 Subject: [PATCH 11/67] Pass inlay go to definition data --- crates/editor/src/editor.rs | 13 +- crates/editor/src/element.rs | 82 ++++++-- crates/editor/src/items.rs | 2 +- crates/editor/src/link_go_to_definition.rs | 216 +++++++++++++++------ 4 files changed, 229 insertions(+), 84 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 775f3c07ece735c781cd60f9600bf7027320d25d..fb1e87580aa8bed7b994e8c8bc92659ffd096d71 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -64,9 +64,7 @@ use language::{ Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; -use link_go_to_definition::{ - hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState, -}; +use link_go_to_definition::{hide_link_definition, show_link_definition, LinkGoToDefinitionState}; use log::error; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -8307,14 +8305,11 @@ impl View for Editor { ) -> bool { let pending_selection = self.has_pending_selection(); - if let Some(point) = self.link_go_to_definition_state.last_mouse_location.clone() { + if let Some(point) = &self.link_go_to_definition_state.last_trigger_point { if event.cmd && !pending_selection { + let point = point.clone(); let snapshot = self.snapshot(cx); - let kind = if event.shift { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; + let kind = point.definition_kind(event.shift); show_link_definition(kind, self, point, snapshot, cx); return false; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 405a6e1a08348dbeb78b60c106d6e3080f9f81dd..15b28914af059cbd238973ae4c5ba359aee32b35 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,6 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, + GoToDefinitionTrigger, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -42,7 +43,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, ProjectPath, ResolveState, + InlayHintLabelPart, Location, LocationLink, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -395,7 +396,15 @@ impl EditorElement { None }; - update_go_to_definition_link(editor, point, cmd, shift, cx); + update_go_to_definition_link( + editor, + point + .map(GoToDefinitionTrigger::Text) + .unwrap_or(GoToDefinitionTrigger::None), + cmd, + shift, + cx, + ); if editor.has_pending_selection() { let mut scroll_delta = Vector2F::zero(); @@ -460,7 +469,13 @@ impl EditorElement { let point_for_position = position_map.point_for_position(text_bounds, position); match point_for_position.as_valid() { Some(point) => { - update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(point), + cmd, + shift, + cx, + ); hover_at(editor, Some(point), cx); } None => { @@ -468,12 +483,13 @@ impl EditorElement { position_map, point_for_position, editor, + (cmd, shift), cx, ); } } } else { - update_go_to_definition_link(editor, None, cmd, shift, cx); + update_go_to_definition_link(editor, GoToDefinitionTrigger::None, cmd, shift, cx); hover_at(editor, None, cx); } @@ -1821,11 +1837,11 @@ impl EditorElement { } } -// TODO kb implement fn update_inlay_link_and_hover_points( position_map: &PositionMap, point_for_position: PointForPosition, editor: &mut Editor, + (cmd_held, shift_held): (bool, bool), cx: &mut ViewContext<'_, '_, Editor>, ) { let hint_start_offset = position_map @@ -1861,6 +1877,9 @@ fn update_inlay_link_and_hover_points( .to_point(&position_map.snapshot.display_snapshot), Bias::Right, ); + + let mut go_to_definition_updated = false; + let mut hover_updated = false; if let Some(hovered_hint) = editor .visible_inlay_hints(cx) .into_iter() @@ -1872,7 +1891,7 @@ fn update_inlay_link_and_hover_points( if let Some(cached_hint) = inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) { - match &cached_hint.resolve_state { + match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { if let Some(buffer_id) = previous_valid_anchor.buffer_id { inlay_hint_cache.spawn_hint_resolve( @@ -1884,7 +1903,7 @@ fn update_inlay_link_and_hover_points( } } ResolveState::Resolved => { - match &cached_hint.label { + match cached_hint.label { project::InlayHintLabel::String(_) => { if cached_hint.tooltip.is_some() { dbg!(&cached_hint.tooltip); // TODO kb @@ -1893,7 +1912,7 @@ fn update_inlay_link_and_hover_points( } project::InlayHintLabel::LabelParts(label_parts) => { if let Some(hovered_hint_part) = find_hovered_hint_part( - &label_parts, + label_parts, hint_start_offset..hint_end_offset, hovered_offset, ) { @@ -1901,9 +1920,31 @@ fn update_inlay_link_and_hover_points( dbg!(&hovered_hint_part.tooltip); // TODO kb // hover_at_point = Some(hovered_offset); } - if let Some(location) = &hovered_hint_part.location { - dbg!(location); // TODO kb - // go_to_definition_point = Some(location); + if let Some(location) = hovered_hint_part.location { + if let Some(buffer) = cached_hint + .position + .buffer_id + .and_then(|buffer_id| buffer.buffer(buffer_id)) + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + hovered_hint.position, + LocationLink { + origin: Some(Location { + buffer, + range: cached_hint.position + ..cached_hint.position, + }), + target: location, + }, + ), + cmd_held, + shift_held, + cx, + ); + } } } } @@ -1913,14 +1954,27 @@ fn update_inlay_link_and_hover_points( } } } + + if !go_to_definition_updated { + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::None, + cmd_held, + shift_held, + cx, + ); + } + if !hover_updated { + hover_at(editor, None, cx); + } } } -fn find_hovered_hint_part<'a>( - label_parts: &'a [InlayHintLabelPart], +fn find_hovered_hint_part( + label_parts: Vec, hint_range: Range, hovered_offset: InlayOffset, -) -> Option<&'a InlayHintLabelPart> { +) -> Option { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; for part in label_parts { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 477eab41ac9cc7a0c7b38e0ec07f3eb41f46963e..30ed56af476547503d95afb563635ad9bddcbec6 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -615,7 +615,7 @@ impl Item for Editor { fn workspace_deactivated(&mut self, cx: &mut ViewContext) { hide_link_definition(self, cx); - self.link_go_to_definition_state.last_mouse_location = None; + self.link_go_to_definition_state.last_trigger_point = None; } fn is_dirty(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 31df11a01959e4795738658d813ce4b23bfcafe8..a0014337277905d347b42487e9194577ce4e81c4 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -7,16 +7,50 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_mouse_location: Option, + pub last_trigger_point: Option, pub symbol_range: Option>, pub kind: Option, pub definitions: Vec, pub task: Option>>, } +pub enum GoToDefinitionTrigger { + Text(DisplayPoint), + InlayHint(Anchor, LocationLink), + None, +} + +#[derive(Debug, Clone)] +pub enum TriggerPoint { + Text(Anchor), + InlayHint(Anchor, LocationLink), +} + +impl TriggerPoint { + fn anchor(&self) -> &Anchor { + match self { + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(anchor, _) => anchor, + } + } + + pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { + match self { + TriggerPoint::Text(_) => { + if shift { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + } + } + TriggerPoint::InlayHint(_, link) => LinkDefinitionKind::Type, + } + } +} + pub fn update_go_to_definition_link( editor: &mut Editor, - point: Option, + origin: GoToDefinitionTrigger, cmd_held: bool, shift_held: bool, cx: &mut ViewContext, @@ -25,23 +59,30 @@ pub fn update_go_to_definition_link( // Store new mouse point as an anchor let snapshot = editor.snapshot(cx); - let point = point.map(|point| { - snapshot - .buffer_snapshot - .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)) - }); + let trigger_point = match origin { + GoToDefinitionTrigger::Text(p) => { + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + p.to_offset(&snapshot.display_snapshot, Bias::Left), + ))) + } + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::None => None, + }; // If the new point is the same as the previously stored one, return early if let (Some(a), Some(b)) = ( - &point, - &editor.link_go_to_definition_state.last_mouse_location, + &trigger_point, + &editor.link_go_to_definition_state.last_trigger_point, ) { - if a.cmp(b, &snapshot.buffer_snapshot).is_eq() { + if a.anchor() + .cmp(b.anchor(), &snapshot.buffer_snapshot) + .is_eq() + { return; } } - editor.link_go_to_definition_state.last_mouse_location = point.clone(); + editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone(); if pending_nonempty_selection { hide_link_definition(editor, cx); @@ -49,14 +90,9 @@ pub fn update_go_to_definition_link( } if cmd_held { - if let Some(point) = point { - let kind = if shift_held { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; - - show_link_definition(kind, editor, point, snapshot, cx); + if let Some(trigger_point) = trigger_point { + let kind = trigger_point.definition_kind(shift_held); + show_link_definition(kind, editor, trigger_point, snapshot, cx); return; } } @@ -73,7 +109,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: Anchor, + trigger_point: TriggerPoint, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -86,10 +122,11 @@ pub fn show_link_definition( return; } + let trigger_anchor = trigger_point.anchor().clone(); let (buffer, buffer_position) = if let Some(output) = editor .buffer .read(cx) - .text_anchor_for_position(trigger_point.clone(), cx) + .text_anchor_for_position(trigger_anchor.clone(), cx) { output } else { @@ -99,7 +136,7 @@ pub fn show_link_definition( let excerpt_id = if let Some((excerpt_id, _, _)) = editor .buffer() .read(cx) - .excerpt_containing(trigger_point.clone(), cx) + .excerpt_containing(trigger_anchor.clone(), cx) { excerpt_id } else { @@ -116,12 +153,12 @@ pub fn show_link_definition( if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { let point_after_start = symbol_range .start - .cmp(&trigger_point, &snapshot.buffer_snapshot) + .cmp(&trigger_anchor, &snapshot.buffer_snapshot) .is_le(); let point_before_end = symbol_range .end - .cmp(&trigger_point, &snapshot.buffer_snapshot) + .cmp(&trigger_anchor, &snapshot.buffer_snapshot) .is_ge(); let point_within_range = point_after_start && point_before_end; @@ -132,34 +169,45 @@ pub fn show_link_definition( let task = cx.spawn(|this, mut cx| { async move { - // query the LSP for definition info - let definition_request = cx.update(|cx| { - project.update(cx, |project, cx| match definition_kind { - LinkDefinitionKind::Symbol => project.definition(&buffer, buffer_position, cx), - - LinkDefinitionKind::Type => { - project.type_definition(&buffer, buffer_position, cx) - } - }) - }); + let result = match trigger_point { + TriggerPoint::Text(_) => { + // query the LSP for definition info + cx.update(|cx| { + project.update(cx, |project, cx| match definition_kind { + LinkDefinitionKind::Symbol => { + project.definition(&buffer, buffer_position, cx) + } - let result = definition_request.await.ok().map(|definition_result| { - ( - definition_result.iter().find_map(|link| { - link.origin.as_ref().map(|origin| { - let start = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), origin.range.start); - let end = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - - start..end + LinkDefinitionKind::Type => { + project.type_definition(&buffer, buffer_position, cx) + } }) - }), - definition_result, - ) - }); + }) + .await + .ok() + .map(|definition_result| { + ( + definition_result.iter().find_map(|link| { + link.origin.as_ref().map(|origin| { + let start = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.start); + let end = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); + + start..end + }) + }), + definition_result, + ) + }) + } + TriggerPoint::InlayHint(trigger_source, trigger_target) => { + // TODO kb range is wrong, should be in inlay coordinates + Some((Some(trigger_source..trigger_source), vec![trigger_target])) + } + }; this.update(&mut cx, |this, cx| { // Clear any existing highlights @@ -202,7 +250,7 @@ pub fn show_link_definition( // If no symbol range returned from language server, use the surrounding word. let highlight_range = symbol_range.unwrap_or_else(|| { let snapshot = &snapshot.buffer_snapshot; - let (offset_range, _) = snapshot.surrounding_word(trigger_point); + let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end) @@ -355,7 +403,13 @@ mod tests { // Press cmd+shift to trigger highlight cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, true, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + true, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -461,7 +515,13 @@ mod tests { }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -482,7 +542,7 @@ mod tests { "}); // Response without source range still highlights word - cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None); + cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None); let mut requests = cx.handle_request::(move |url, _, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ lsp::LocationLink { @@ -495,7 +555,13 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -517,7 +583,13 @@ mod tests { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -534,7 +606,13 @@ mod tests { fn do_work() { teˇst(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), false, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + false, + false, + cx, + ); }); cx.foreground().run_until_parked(); @@ -593,7 +671,13 @@ mod tests { // Moving the mouse restores the highlights. cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -607,7 +691,13 @@ mod tests { fn do_work() { tesˇt(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -703,7 +793,13 @@ mod tests { }); }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); assert!(requests.try_next().is_err()); From 477fc865f5b6f2b1bf43cb4350f49eced80e4e29 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 20:21:38 +0300 Subject: [PATCH 12/67] Properly resolve inlay label parts' locations and buffers --- crates/editor/src/display_map/inlay_map.rs | 1 + crates/editor/src/element.rs | 33 +-- crates/editor/src/link_go_to_definition.rs | 43 ++-- crates/project/src/lsp_command.rs | 256 ++++++++++++++------- crates/project/src/project.rs | 21 +- 5 files changed, 222 insertions(+), 132 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 026f1fc2c20a789319a4834dbd181bc61b7420ee..34181a576932d2aecc2cda538297fda365df4726 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1010,6 +1010,7 @@ impl InlaySnapshot { }) { Ok(i) | Err(i) => i, }; + // TODO kb add a way to highlight inlay hints through here. for range in &ranges[start_ix..] { if range.start.cmp(&transform_end, &self.buffer).is_ge() { break; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 15b28914af059cbd238973ae4c5ba359aee32b35..c28f14d98a916ab93a57be52fb20f69a487ee2ad 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -358,18 +358,15 @@ impl EditorElement { } if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { - if let Some(point) = position_map - .point_for_position(text_bounds, position) - .as_valid() - { - if shift { - go_to_fetched_type_definition(editor, point, alt, cx); - } else { - go_to_fetched_definition(editor, point, alt, cx); - } - - return true; + let point = position_map.point_for_position(text_bounds, position); + let could_be_inlay = point.as_valid().is_none(); + if shift || could_be_inlay { + go_to_fetched_type_definition(editor, point, alt, cx); + } else { + go_to_fetched_definition(editor, point, alt, cx); } + + return true; } end_selection @@ -2818,14 +2815,24 @@ struct PositionMap { } #[derive(Debug)] -struct PointForPosition { +pub struct PointForPosition { previous_valid: DisplayPoint, - next_valid: DisplayPoint, + pub next_valid: DisplayPoint, exact_unclipped: DisplayPoint, column_overshoot_after_line_end: u32, } impl PointForPosition { + #[cfg(test)] + pub fn valid(valid: DisplayPoint) -> Self { + Self { + previous_valid: valid, + next_valid: valid, + exact_unclipped: valid, + column_overshoot_after_line_end: 0, + } + } + fn as_valid(&self) -> Option { if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { Some(self.previous_valid) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index a0014337277905d347b42487e9194577ce4e81c4..b043af613279e44068d483c25ec17a2f752354dd 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,4 +1,4 @@ -use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; +use crate::{element::PointForPosition, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; @@ -7,7 +7,7 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_trigger_point: Option, + pub last_trigger_point: Option, pub symbol_range: Option>, pub kind: Option, pub definitions: Vec, @@ -21,29 +21,29 @@ pub enum GoToDefinitionTrigger { } #[derive(Debug, Clone)] -pub enum TriggerPoint { +pub enum TriggerAnchor { Text(Anchor), InlayHint(Anchor, LocationLink), } -impl TriggerPoint { +impl TriggerAnchor { fn anchor(&self) -> &Anchor { match self { - TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(anchor, _) => anchor, + TriggerAnchor::Text(anchor) => anchor, + TriggerAnchor::InlayHint(anchor, _) => anchor, } } pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { match self { - TriggerPoint::Text(_) => { + TriggerAnchor::Text(_) => { if shift { LinkDefinitionKind::Type } else { LinkDefinitionKind::Symbol } } - TriggerPoint::InlayHint(_, link) => LinkDefinitionKind::Type, + TriggerAnchor::InlayHint(_, _) => LinkDefinitionKind::Type, } } } @@ -61,11 +61,11 @@ pub fn update_go_to_definition_link( let snapshot = editor.snapshot(cx); let trigger_point = match origin { GoToDefinitionTrigger::Text(p) => { - Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + Some(TriggerAnchor::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerAnchor::InlayHint(p, target)), GoToDefinitionTrigger::None => None, }; @@ -109,7 +109,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: TriggerPoint, + trigger_point: TriggerAnchor, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -170,7 +170,7 @@ pub fn show_link_definition( let task = cx.spawn(|this, mut cx| { async move { let result = match trigger_point { - TriggerPoint::Text(_) => { + TriggerAnchor::Text(_) => { // query the LSP for definition info cx.update(|cx| { project.update(cx, |project, cx| match definition_kind { @@ -203,8 +203,9 @@ pub fn show_link_definition( ) }) } - TriggerPoint::InlayHint(trigger_source, trigger_target) => { - // TODO kb range is wrong, should be in inlay coordinates + TriggerAnchor::InlayHint(trigger_source, trigger_target) => { + // TODO kb range is wrong, should be in inlay coordinates have a proper inlay range. + // Or highlight inlays differently, in their layer? Some((Some(trigger_source..trigger_source), vec![trigger_target])) } }; @@ -293,7 +294,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { pub fn go_to_fetched_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -302,7 +303,7 @@ pub fn go_to_fetched_definition( pub fn go_to_fetched_type_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -312,7 +313,7 @@ pub fn go_to_fetched_type_definition( fn go_to_fetched_definition_of_kind( kind: LinkDefinitionKind, editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -330,7 +331,7 @@ fn go_to_fetched_definition_of_kind( } else { editor.select( SelectPhase::Begin { - position: point, + position: point.next_valid, add: false, click_count: 1, }, @@ -460,7 +461,7 @@ mod tests { }); cx.update_editor(|editor, cx| { - go_to_fetched_type_definition(editor, hover_point, false, cx); + go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); @@ -707,7 +708,7 @@ mod tests { // Cmd click with existing definition doesn't re-request and dismisses highlight cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); // Assert selection moved to to definition cx.lsp @@ -748,7 +749,7 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 7b4d689a81142857ee5153028f7521a65f7388c8..20bb302b5b8963e996a1da47564f26ad191b56ad 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -7,14 +7,15 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; use fs::LineEnding; +use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, - CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, - Unclipped, + CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, + Transaction, Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1432,7 +1433,7 @@ impl LspCommand for GetCompletions { }) }); - Ok(futures::future::join_all(completions).await) + Ok(future::join_all(completions).await) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions { @@ -1500,7 +1501,7 @@ impl LspCommand for GetCompletions { let completions = message.completions.into_iter().map(|completion| { language::proto::deserialize_completion(completion, language.clone()) }); - futures::future::try_join_all(completions).await + future::try_join_all(completions).await } fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 { @@ -1778,73 +1779,50 @@ impl LspCommand for OnTypeFormatting { } impl InlayHints { - pub fn lsp_to_project_hint( + pub async fn lsp_to_project_hint( lsp_hint: lsp::InlayHint, + project: &ModelHandle, buffer_handle: &ModelHandle, + server_id: LanguageServerId, resolve_state: ResolveState, force_no_type_left_padding: bool, - cx: &AppContext, - ) -> InlayHint { + cx: &mut AsyncAppContext, + ) -> anyhow::Result { let kind = lsp_hint.kind.and_then(|kind| match kind { lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), _ => None, }); - let buffer = buffer_handle.read(cx); - let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + + let position = cx.update(|cx| { + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + } + }); + let label = Self::lsp_inlay_label_to_project( + &buffer_handle, + project, + server_id, + lsp_hint.label, + cx, + ) + .await + .context("lsp to project inlay hint conversion")?; let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { false } else { lsp_hint.padding_left.unwrap_or(false) }; - InlayHint { - position: if kind == Some(InlayHintKind::Parameter) { - buffer.anchor_before(position) - } else { - buffer.anchor_after(position) - }, + + Ok(InlayHint { + position, padding_left, padding_right: lsp_hint.padding_right.unwrap_or(false), - label: match lsp_hint.label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => InlayHintLabel::LabelParts( - lsp_parts - .into_iter() - .map(|label_part| InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { - InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: match markup_content.kind { - lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, - }, - value: markup_content.value, - }) - } - }), - location: label_part.location.map(|lsp_location| { - let target_start = buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - Location { - buffer: buffer_handle.clone(), - range: buffer.anchor_after(target_start) - ..buffer.anchor_before(target_end), - } - }), - }) - .collect(), - ), - }, + label, kind, tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), @@ -1859,7 +1837,100 @@ impl InlayHints { } }), resolve_state, - } + }) + } + + async fn lsp_inlay_label_to_project( + buffer: &ModelHandle, + project: &ModelHandle, + server_id: LanguageServerId, + lsp_label: lsp::InlayHintLabel, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let label = match lsp_label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => { + let mut parts_data = Vec::with_capacity(lsp_parts.len()); + buffer.update(cx, |buffer, cx| { + for lsp_part in lsp_parts { + let location_buffer_task = match &lsp_part.location { + Some(lsp_location) => { + let location_buffer_task = project.update(cx, |project, cx| { + let language_server_name = project + .language_server_for_buffer(buffer, server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }); + Some(lsp_location.clone()).zip(location_buffer_task) + } + None => None, + }; + + parts_data.push((lsp_part, location_buffer_task)); + } + }); + + let mut parts = Vec::with_capacity(parts_data.len()); + for (lsp_part, location_buffer_task) in parts_data { + let location = match location_buffer_task { + Some((lsp_location, target_buffer_handle_task)) => { + let target_buffer_handle = target_buffer_handle_task + .await + .context("resolving location for label part buffer")?; + let range = cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }); + Some(Location { + buffer: target_buffer_handle, + range, + }) + } + None => None, + }; + + parts.push(InlayHintLabelPart { + value: lsp_part.value, + tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location, + }); + } + InlayHintLabel::LabelParts(parts) + } + }; + + Ok(label) } pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { @@ -2115,15 +2186,18 @@ impl InlayHints { }) }), location: part.location.and_then(|location| { - let path = cx.read(|cx| { - let project_path = location.buffer.read(cx).project_path(cx)?; - project.read(cx).absolute_path(&project_path, cx) + let (path, location_snapshot) = cx.read(|cx| { + let buffer = location.buffer.read(cx); + let project_path = buffer.project_path(cx)?; + let location_snapshot = buffer.snapshot(); + let path = project.read(cx).absolute_path(&project_path, cx); + path.zip(Some(location_snapshot)) })?; Some(lsp::Location::new( lsp::Url::from_file_path(path).unwrap(), range_to_lsp( - location.range.start.to_point_utf16(snapshot) - ..location.range.end.to_point_utf16(snapshot), + location.range.start.to_point_utf16(&location_snapshot) + ..location.range.end.to_point_utf16(&location_snapshot), ), )) }), @@ -2182,7 +2256,7 @@ impl LspCommand for InlayHints { buffer: ModelHandle, server_id: LanguageServerId, mut cx: AsyncAppContext, - ) -> Result> { + ) -> anyhow::Result> { let (lsp_adapter, lsp_server) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; // `typescript-language-server` adds padding to the left for type hints, turning @@ -2194,32 +2268,38 @@ impl LspCommand for InlayHints { // Hence let's use a heuristic first to handle the most awkward case and look for more. let force_no_type_left_padding = lsp_adapter.name.0.as_ref() == "typescript-language-server"; - cx.read(|cx| { - Ok(message - .unwrap_or_default() - .into_iter() - .map(|lsp_hint| { - let resolve_state = match lsp_server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( - lsp::InlayHintOptions { - resolve_provider: Some(true), - .. - }, - ))) => { - ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) - } - _ => ResolveState::Resolved, - }; - InlayHints::lsp_to_project_hint( - lsp_hint, - &buffer, - resolve_state, - force_no_type_left_padding, - cx, - ) - }) - .collect()) - }) + + let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { + let resolve_state = match lsp_server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( + lsp::InlayHintOptions { + resolve_provider: Some(true), + .. + }, + ))) => ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()), + _ => ResolveState::Resolved, + }; + + let project = project.clone(); + let buffer = buffer.clone(); + cx.spawn(|mut cx| async move { + InlayHints::lsp_to_project_hint( + lsp_hint, + &project, + &buffer, + server_id, + resolve_state, + force_no_type_left_padding, + &mut cx, + ) + .await + }) + }); + future::join_all(hints) + .await + .into_iter() + .collect::>() + .context("lsp to project inlay hints conversion") } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 65be7cceb10ef3a1fea39ba908d239b3948085bf..fbdbd04664fa4a76ebfa9e7992fd35987da8ad2f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5054,22 +5054,23 @@ impl Project { } let buffer_snapshot = buffer.snapshot(); - cx.spawn(|project, cx| async move { + cx.spawn(|project, mut cx| async move { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), ); let resolved_hint = resolve_task .await .context("inlay hint resolve LSP request")?; - let resolved_hint = cx.read(|cx| { - InlayHints::lsp_to_project_hint( - resolved_hint, - &buffer_handle, - ResolveState::Resolved, - false, - cx, - ) - }); + let resolved_hint = InlayHints::lsp_to_project_hint( + resolved_hint, + &project, + &buffer_handle, + server_id, + ResolveState::Resolved, + false, + &mut cx, + ) + .await?; Ok(Some(resolved_hint)) }) } else if let Some(project_id) = self.remote_id() { From 6c5761d05bccadfde383281560ad15e14f452c24 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 23:21:37 +0300 Subject: [PATCH 13/67] Pass inlay highlight information --- crates/editor/src/display_map.rs | 39 +++++- crates/editor/src/display_map/block_map.rs | 14 ++- crates/editor/src/display_map/fold_map.rs | 27 ++-- crates/editor/src/display_map/inlay_map.rs | 113 +++++++++-------- crates/editor/src/display_map/tab_map.rs | 36 ++++-- crates/editor/src/display_map/wrap_map.rs | 16 ++- crates/editor/src/editor.rs | 31 ++++- crates/editor/src/element.rs | 9 +- crates/editor/src/link_go_to_definition.rs | 139 +++++++++++++-------- 9 files changed, 279 insertions(+), 145 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 9159253142e56c55ed1243e41a12ea79e7ca26ff..9df2919351d7563d272172bd6dd262dfd20af97b 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -4,7 +4,10 @@ mod inlay_map; mod tab_map; mod wrap_map; -use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; +use crate::{ + link_go_to_definition::InlayCoordinates, Anchor, AnchorRangeExt, InlayId, MultiBuffer, + MultiBufferSnapshot, ToOffset, ToPoint, +}; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; use fold_map::FoldMap; @@ -40,6 +43,7 @@ pub trait ToDisplayPoint { } type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; +type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -50,6 +54,7 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, pub clip_at_line_ends: bool, } @@ -85,6 +90,7 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), + inlay_highlights: Default::default(), clip_at_line_ends: false, } } @@ -109,6 +115,7 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), + inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, } } @@ -215,6 +222,16 @@ impl DisplayMap { .insert(Some(type_id), Arc::new((style, ranges))); } + pub fn highlight_inlays( + &mut self, + type_id: TypeId, + ranges: Vec, + style: HighlightStyle, + ) { + self.inlay_highlights + .insert(Some(type_id), Arc::new((style, ranges))); + } + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) @@ -227,6 +244,13 @@ impl DisplayMap { self.text_highlights.remove(&Some(type_id)) } + pub fn clear_inlay_highlights( + &mut self, + type_id: TypeId, + ) -> Option)>> { + self.inlay_highlights.remove(&Some(type_id)) + } + pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) @@ -296,6 +320,7 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, clip_at_line_ends: bool, } @@ -421,6 +446,7 @@ impl DisplaySnapshot { None, None, None, + None, ) .map(|h| h.text) } @@ -429,7 +455,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None, None) + .chunks(row..row + 1, false, None, None, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -441,15 +467,16 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - hint_highlights, - suggestion_highlights, + Some(&self.inlay_highlights), + inlay_highlight_style, + suggestion_highlight_style, ) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 4b76ded3d50cc45d72385d70bbb424b139023f09..083d8be5f32e890b8593f5d090a94656f0813fb6 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,6 +1,6 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; @@ -579,6 +579,7 @@ impl BlockSnapshot { None, None, None, + None, ) .map(|chunk| chunk.text) .collect() @@ -589,8 +590,9 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); @@ -623,8 +625,9 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), transforms: cursor, @@ -1504,6 +1507,7 @@ mod tests { None, None, None, + None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 0b1523fe750326dea5c87f3ec4dcfa350f497185..800b51fcd850e767446c84408c99d53a70b2a0ab 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; @@ -475,7 +475,7 @@ pub struct FoldSnapshot { impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None, None, None) + self.chunks(FoldOffset(0)..self.len(), false, None, None, None, None) .map(|c| c.text) .collect() } @@ -652,8 +652,9 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -675,8 +676,9 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), inlay_chunk: None, inlay_offset: inlay_start, @@ -687,8 +689,15 @@ impl FoldSnapshot { } pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { - self.chunks(start.to_offset(self)..self.len(), false, None, None, None) - .flat_map(|chunk| chunk.text.chars()) + self.chunks( + start.to_offset(self)..self.len(), + false, + None, + None, + None, + None, + ) + .flat_map(|chunk| chunk.text.chars()) } #[cfg(test)] @@ -1496,7 +1505,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, None, None, None, None) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 34181a576932d2aecc2cda538297fda365df4726..5617b6a6b67cf7de7bd95d2b29f351feeaba843c 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -15,7 +15,7 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::TextHighlights; +use super::{InlayHighlights, TextHighlights}; pub struct InlayMap { snapshot: InlaySnapshot, @@ -973,8 +973,9 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); @@ -983,53 +984,50 @@ impl InlaySnapshot { if let Some(text_highlights) = text_highlights { if !text_highlights.is_empty() { while cursor.start().0 < range.end { - if true { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); - - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; - - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - // TODO kb add a way to highlight inlay hints through here. - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); - highlight_endpoints.push(HighlightEndpoint { - offset: self - .to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag: *tag, - style, - }); + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; + + for (tag, highlights) in text_highlights.iter() { + let style = highlights.0; + let ranges = &highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less } + }) { + Ok(i) | Err(i) => i, + }; + // TODO kb add a way to highlight inlay hints through here. + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); } } @@ -1050,8 +1048,8 @@ impl InlaySnapshot { buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style: hint_highlights, - suggestion_highlight_style: suggestion_highlights, + hint_highlight_style: inlay_highlight_style, + suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), snapshot: self, @@ -1060,9 +1058,16 @@ impl InlaySnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(Default::default()..self.len(), false, None, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + Default::default()..self.len(), + false, + None, + None, + None, + None, + ) + .map(|chunk| chunk.text) + .collect() } fn check_invariants(&self) { @@ -1636,6 +1641,8 @@ mod tests { InlayOffset(start)..InlayOffset(end), false, Some(&highlights), + // TODO kb add tests + None, None, None, ) diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index ca73f6a1a7a7e5bff4d19a32db548c9d2155f744..187a8de1d30aa450cacaf35711b5586a95ab9fb5 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,6 +1,6 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::fonts::HighlightStyle; @@ -71,6 +71,7 @@ impl TabMap { None, None, None, + None, ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -183,7 +184,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None, None) + .chunks(range.start..line_end, false, None, None, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -203,6 +204,7 @@ impl TabSnapshot { None, None, None, + None, ) .flat_map(|chunk| chunk.text.chars()) { @@ -223,9 +225,11 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, + // TODO kb extract into one struct text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); @@ -246,8 +250,9 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_column, column: expanded_char_column, @@ -270,9 +275,16 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + TabPoint::zero()..self.max_point(), + false, + None, + None, + None, + None, + ) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -600,6 +612,7 @@ mod tests { None, None, None, + None, ) .map(|c| c.text) .collect::(), @@ -674,7 +687,8 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None, None) + { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -743,7 +757,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, None, None, None, None) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f21c7151ad695b2567dc9cea7da5a5007be1696f..f8287af799890e59b1b4b1c72f845fd1d26a76d5 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,7 +1,7 @@ use super::{ fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::{ @@ -447,6 +447,7 @@ impl WrapSnapshot { None, None, None, + None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -576,8 +577,9 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); @@ -595,8 +597,9 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), output_position: output_start, @@ -1326,6 +1329,7 @@ mod tests { None, None, None, + None, ) .map(|h| h.text) } @@ -1350,7 +1354,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None, None) + .chunks(start_row..end_row, true, None, None, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fb1e87580aa8bed7b994e8c8bc92659ffd096d71..f10734a76c24c675016ca4c448ff4ebfe5df15b2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -64,7 +64,9 @@ use language::{ Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; -use link_go_to_definition::{hide_link_definition, show_link_definition, LinkGoToDefinitionState}; +use link_go_to_definition::{ + hide_link_definition, show_link_definition, InlayCoordinates, LinkGoToDefinitionState, +}; use log::error; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -7716,6 +7718,18 @@ impl Editor { cx.notify(); } + pub fn highlight_inlays( + &mut self, + ranges: Vec, + style: HighlightStyle, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), ranges, style) + }); + cx.notify(); + } + pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, @@ -7736,6 +7750,19 @@ impl Editor { highlights } + pub fn clear_highlights( + &mut self, + cx: &mut ViewContext, + ) -> Option>)>> { + let highlights = self + .display_map + .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); + if highlights.is_some() { + cx.notify(); + } + highlights + } + pub fn show_local_cursors(&self, cx: &AppContext) -> bool { self.blink_manager.read(cx).visible() && self.focused } @@ -8327,7 +8354,7 @@ impl View for Editor { self.link_go_to_definition_state.task = None; - self.clear_text_highlights::(cx); + self.clear_highlights::(cx); } false diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c28f14d98a916ab93a57be52fb20f69a487ee2ad..c448ba4f22bea1e90506044fbc97d457136f320d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,7 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, + GoToDefinitionTrigger, InlayCoordinates, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -1927,7 +1927,12 @@ fn update_inlay_link_and_hover_points( update_go_to_definition_link( editor, GoToDefinitionTrigger::InlayHint( - hovered_hint.position, + InlayCoordinates { + inlay_id: hovered_hint.id, + inlay_position: hovered_hint.position, + inlay_start: hint_start_offset, + highlight_end: hovered_offset, + }, LocationLink { origin: Some(Location { buffer, diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index b043af613279e44068d483c25ec17a2f752354dd..35e367497e65da8fb0e581f209e1c3e64537a7b9 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,4 +1,7 @@ -use crate::{element::PointForPosition, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; +use crate::{ + display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, + EditorSnapshot, InlayId, SelectPhase, +}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; @@ -7,8 +10,8 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_trigger_point: Option, - pub symbol_range: Option>, + pub last_trigger_point: Option, + pub symbol_range: Option, pub kind: Option, pub definitions: Vec, pub task: Option>>, @@ -16,34 +19,65 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(Anchor, LocationLink), + InlayHint(InlayCoordinates, LocationLink), None, } +#[derive(Debug, Clone, Copy)] +pub struct InlayCoordinates { + pub inlay_id: InlayId, + pub inlay_position: Anchor, + pub inlay_start: InlayOffset, + pub highlight_end: InlayOffset, +} + #[derive(Debug, Clone)] -pub enum TriggerAnchor { +pub enum TriggerPoint { Text(Anchor), - InlayHint(Anchor, LocationLink), + InlayHint(InlayCoordinates, LocationLink), +} + +#[derive(Debug, Clone)] +pub enum SymbolRange { + Text(Range), + Inlay(InlayCoordinates), } -impl TriggerAnchor { +impl SymbolRange { + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { + match (self, trigger_point) { + (SymbolRange::Text(range), TriggerPoint::Text(point)) => { + let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); + point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() + } + (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + range.inlay_start.cmp(&point.highlight_end).is_le() + && range.highlight_end.cmp(&point.highlight_end).is_ge() + } + (SymbolRange::Inlay(_), TriggerPoint::Text(_)) + | (SymbolRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + } + } +} + +impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { - TriggerAnchor::Text(anchor) => anchor, - TriggerAnchor::InlayHint(anchor, _) => anchor, + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(coordinates, _) => &coordinates.inlay_position, } } pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { match self { - TriggerAnchor::Text(_) => { + TriggerPoint::Text(_) => { if shift { LinkDefinitionKind::Type } else { LinkDefinitionKind::Symbol } } - TriggerAnchor::InlayHint(_, _) => LinkDefinitionKind::Type, + TriggerPoint::InlayHint(_, _) => LinkDefinitionKind::Type, } } } @@ -61,11 +95,11 @@ pub fn update_go_to_definition_link( let snapshot = editor.snapshot(cx); let trigger_point = match origin { GoToDefinitionTrigger::Text(p) => { - Some(TriggerAnchor::Text(snapshot.buffer_snapshot.anchor_before( + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerAnchor::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), GoToDefinitionTrigger::None => None, }; @@ -109,7 +143,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: TriggerAnchor, + trigger_point: TriggerPoint, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -122,7 +156,7 @@ pub fn show_link_definition( return; } - let trigger_anchor = trigger_point.anchor().clone(); + let trigger_anchor = trigger_point.anchor(); let (buffer, buffer_position) = if let Some(output) = editor .buffer .read(cx) @@ -151,26 +185,15 @@ pub fn show_link_definition( // Don't request again if the location is within the symbol region of a previous request with the same kind if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { - let point_after_start = symbol_range - .start - .cmp(&trigger_anchor, &snapshot.buffer_snapshot) - .is_le(); - - let point_before_end = symbol_range - .end - .cmp(&trigger_anchor, &snapshot.buffer_snapshot) - .is_ge(); - - let point_within_range = point_after_start && point_before_end; - if point_within_range && same_kind { + if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) { return; } } let task = cx.spawn(|this, mut cx| { async move { - let result = match trigger_point { - TriggerAnchor::Text(_) => { + let result = match &trigger_point { + TriggerPoint::Text(_) => { // query the LSP for definition info cx.update(|cx| { project.update(cx, |project, cx| match definition_kind { @@ -196,23 +219,22 @@ pub fn show_link_definition( .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - start..end + SymbolRange::Text(start..end) }) }), definition_result, ) }) } - TriggerAnchor::InlayHint(trigger_source, trigger_target) => { - // TODO kb range is wrong, should be in inlay coordinates have a proper inlay range. - // Or highlight inlays differently, in their layer? - Some((Some(trigger_source..trigger_source), vec![trigger_target])) - } + TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( + Some(SymbolRange::Inlay(trigger_source.clone())), + vec![trigger_target.clone()], + )), }; this.update(&mut cx, |this, cx| { // Clear any existing highlights - this.clear_text_highlights::(cx); + this.clear_highlights::(cx); this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() @@ -248,22 +270,37 @@ pub fn show_link_definition( }); if any_definition_does_not_contain_current_location { - // If no symbol range returned from language server, use the surrounding word. - let highlight_range = symbol_range.unwrap_or_else(|| { - let snapshot = &snapshot.buffer_snapshot; - let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - - snapshot.anchor_before(offset_range.start) - ..snapshot.anchor_after(offset_range.end) - }); - // Highlight symbol using theme link definition highlight style let style = theme::current(cx).editor.link_definition; - this.highlight_text::( - vec![highlight_range], - style, - cx, - ); + let highlight_range = symbol_range.unwrap_or_else(|| match trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); + SymbolRange::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(inlay_trigger, _) => { + SymbolRange::Inlay(inlay_trigger) + } + }); + + match highlight_range { + SymbolRange::Text(text_range) => this + .highlight_text::( + vec![text_range], + style, + cx, + ), + SymbolRange::Inlay(inlay_coordinates) => this + .highlight_inlays::( + vec![inlay_coordinates], + style, + cx, + ), + } } else { hide_link_definition(this, cx); } @@ -289,7 +326,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { editor.link_go_to_definition_state.task = None; - editor.clear_text_highlights::(cx); + editor.clear_highlights::(cx); } pub fn go_to_fetched_definition( From f8874a726cc4a3fcc09613d8faa7a215ad8dc915 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 01:02:25 +0300 Subject: [PATCH 14/67] Attempt to highlight inlays --- crates/editor/src/display_map/inlay_map.rs | 168 +++++++++++++++------ crates/editor/src/editor.rs | 13 +- crates/editor/src/element.rs | 23 +-- crates/editor/src/link_go_to_definition.rs | 11 +- 4 files changed, 143 insertions(+), 72 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 5617b6a6b67cf7de7bd95d2b29f351feeaba843c..23b39ca5ab0f021853ffe05017e3ec223076830f 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -2,7 +2,7 @@ use crate::{ multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; -use collections::{BTreeMap, BTreeSet}; +use collections::{BTreeMap, BTreeSet, HashSet}; use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use std::{ @@ -183,7 +183,7 @@ pub struct InlayBufferRows<'a> { max_buffer_row: u32, } -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] struct HighlightEndpoint { offset: InlayOffset, is_start: bool, @@ -243,6 +243,7 @@ impl<'a> Iterator for InlayChunks<'a> { return None; } + // TODO kb highlights are not displayed still let mut next_highlight_endpoint = InlayOffset(usize::MAX); while let Some(endpoint) = self.highlight_endpoints.peek().copied() { if endpoint.offset <= self.output_offset { @@ -980,62 +981,89 @@ impl InlaySnapshot { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); + let empty_text_highlights = TextHighlights::default(); + let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); + let empty_inlay_highlights = InlayHighlights::default(); + let inlay_highlights = inlay_highlights.unwrap_or_else(|| &empty_inlay_highlights); + let mut highlight_endpoints = Vec::new(); - if let Some(text_highlights) = text_highlights { - if !text_highlights.is_empty() { - while cursor.start().0 < range.end { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); + if !text_highlights.is_empty() || !inlay_highlights.is_empty() { + while cursor.start().0 < range.end { + let transform_start = self + .buffer + .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); + + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; + let mut covered_tags = HashSet::default(); + for (tag, text_highlights) in text_highlights.iter() { + covered_tags.insert(*tag); + let style = text_highlights.0; + let ranges = &text_highlights.1; - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - // TODO kb add a way to highlight inlay hints through here. - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); + } - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag: *tag, - style, - }); - } + if let Some(inlay_highlights) = inlay_highlights.get(tag) { + self.push_inlay_highlight_range( + inlay_highlights, + transform_start, + transform_end, + &mut highlight_endpoints, + tag, + ); } + } - cursor.next(&()); + for (tag, inlay_highlights) in inlay_highlights + .iter() + .filter(|(tag, _)| !covered_tags.contains(tag)) + { + self.push_inlay_highlight_range( + inlay_highlights, + transform_start, + transform_end, + &mut highlight_endpoints, + tag, + ); } - highlight_endpoints.sort(); - cursor.seek(&range.start, Bias::Right, &()); + + cursor.next(&()); } + highlight_endpoints.sort(); + cursor.seek(&range.start, Bias::Right, &()); } let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); @@ -1056,6 +1084,48 @@ impl InlaySnapshot { } } + fn push_inlay_highlight_range( + &self, + inlay_highlights: &std::sync::Arc<( + HighlightStyle, + Vec, + )>, + transform_start: Anchor, + transform_end: Anchor, + highlight_endpoints: &mut Vec, + tag: &Option, + ) { + let style = inlay_highlights.0; + let ranges = &inlay_highlights.1; + let start_ix = match ranges + .binary_search_by(|probe| probe.inlay_position.cmp(&transform_start, &self.buffer)) + { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .inlay_position + .cmp(&transform_end, &self.buffer) + .is_ge() + { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: range.highlight_start, + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.highlight_end, + is_start: false, + tag: *tag, + style, + }); + } + } + #[cfg(test)] pub fn text(&self) -> String { self.chunks( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f10734a76c24c675016ca4c448ff4ebfe5df15b2..6c5d1a45ac05b65f4a214e5a7453c3a642d776f3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7750,17 +7750,16 @@ impl Editor { highlights } - pub fn clear_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Option>)>> { - let highlights = self + pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if highlights.is_some() { + let inlay_highlights = self + .display_map + .update(cx, |map, _| map.clear_inlay_highlights(TypeId::of::())); + if text_highlights.is_some() || inlay_highlights.is_some() { cx.notify(); } - highlights } pub fn show_local_cursors(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c448ba4f22bea1e90506044fbc97d457136f320d..10809d896296bf5e85fb04ccdee5a564b7cfab25 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1908,11 +1908,13 @@ fn update_inlay_link_and_hover_points( } } project::InlayHintLabel::LabelParts(label_parts) => { - if let Some(hovered_hint_part) = find_hovered_hint_part( - label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) { + if let Some((hovered_hint_part, part_range)) = + find_hovered_hint_part( + label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) + { if hovered_hint_part.tooltip.is_some() { dbg!(&hovered_hint_part.tooltip); // TODO kb // hover_at_point = Some(hovered_offset); @@ -1928,10 +1930,9 @@ fn update_inlay_link_and_hover_points( editor, GoToDefinitionTrigger::InlayHint( InlayCoordinates { - inlay_id: hovered_hint.id, inlay_position: hovered_hint.position, - inlay_start: hint_start_offset, - highlight_end: hovered_offset, + highlight_start: part_range.start, + highlight_end: part_range.end, }, LocationLink { origin: Some(Location { @@ -1976,15 +1977,17 @@ fn find_hovered_hint_part( label_parts: Vec, hint_range: Range, hovered_offset: InlayOffset, -) -> Option { +) -> Option<(InlayHintLabelPart, Range)> { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; + part_start.0 += part_len; } else { - return Some(part); + return Some((part, part_start..InlayOffset(part_start.0 + part_len))); } } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 35e367497e65da8fb0e581f209e1c3e64537a7b9..ee2b536042662055e2e252990c9689caf59be705 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,6 +1,6 @@ use crate::{ display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, - EditorSnapshot, InlayId, SelectPhase, + EditorSnapshot, SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; @@ -25,9 +25,8 @@ pub enum GoToDefinitionTrigger { #[derive(Debug, Clone, Copy)] pub struct InlayCoordinates { - pub inlay_id: InlayId, pub inlay_position: Anchor, - pub inlay_start: InlayOffset, + pub highlight_start: InlayOffset, pub highlight_end: InlayOffset, } @@ -51,7 +50,7 @@ impl SymbolRange { point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { - range.inlay_start.cmp(&point.highlight_end).is_le() + range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } (SymbolRange::Inlay(_), TriggerPoint::Text(_)) @@ -282,8 +281,8 @@ pub fn show_link_definition( ..snapshot.anchor_after(offset_range.end), ) } - TriggerPoint::InlayHint(inlay_trigger, _) => { - SymbolRange::Inlay(inlay_trigger) + TriggerPoint::InlayHint(inlay_coordinates, _) => { + SymbolRange::Inlay(inlay_coordinates) } }); From 4cc9f2f525e9e2991f1c71cf5a2582bfa492ec83 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 11:34:40 +0300 Subject: [PATCH 15/67] Highlight inlay hint parts on cmd-hover Co-Authored-By: Antonio --- crates/editor/src/display_map/inlay_map.rs | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 23b39ca5ab0f021853ffe05017e3ec223076830f..19b3ec792425117f09b3dcc259642af15dcae743 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -210,6 +210,7 @@ pub struct InlayChunks<'a> { buffer_chunks: MultiBufferChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, + inlay_chunk: Option<&'a str>, output_offset: InlayOffset, max_output_offset: InlayOffset, hint_highlight_style: Option, @@ -298,13 +299,31 @@ impl<'a> Iterator for InlayChunks<'a> { - self.transforms.start().0; inlay.text.chunks_in_range(start.0..end.0) }); + let inlay_chunk = self + .inlay_chunk + .get_or_insert_with(|| inlay_chunks.next().unwrap()); + let (chunk, remainder) = inlay_chunk.split_at( + inlay_chunk + .len() + .min(next_highlight_endpoint.0 - self.output_offset.0), + ); + *inlay_chunk = remainder; + if inlay_chunk.is_empty() { + self.inlay_chunk = None; + } - let chunk = inlay_chunks.next().unwrap(); self.output_offset.0 += chunk.len(); - let highlight_style = match inlay.id { + let mut highlight_style = match inlay.id { InlayId::Suggestion(_) => self.suggestion_highlight_style, InlayId::Hint(_) => self.hint_highlight_style, }; + if !self.active_highlights.is_empty() { + for active_highlight in self.active_highlights.values() { + highlight_style + .get_or_insert(Default::default()) + .highlight(*active_highlight); + } + } Chunk { text: chunk, highlight_style, @@ -1073,6 +1092,7 @@ impl InlaySnapshot { transforms: cursor, buffer_chunks, inlay_chunks: None, + inlay_chunk: None, buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, From 420f8b7b1597e7ea132cba5ffd4f5459834aef9d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 22:06:48 +0300 Subject: [PATCH 16/67] Prepare for inlay and text highlight unification --- crates/editor/src/display_map.rs | 8 ++--- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/editor.rs | 21 +++--------- crates/editor/src/element.rs | 4 +-- crates/editor/src/link_go_to_definition.rs | 38 +++++++++++----------- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 9df2919351d7563d272172bd6dd262dfd20af97b..4f08be73b9400ce1c3bbdf59979a19cb90af1e22 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,7 +5,7 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayCoordinates, Anchor, AnchorRangeExt, InlayId, MultiBuffer, + link_go_to_definition::InlayRange, Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; @@ -43,7 +43,7 @@ pub trait ToDisplayPoint { } type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; -type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; +type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -225,7 +225,7 @@ impl DisplayMap { pub fn highlight_inlays( &mut self, type_id: TypeId, - ranges: Vec, + ranges: Vec, style: HighlightStyle, ) { self.inlay_highlights @@ -247,7 +247,7 @@ impl DisplayMap { pub fn clear_inlay_highlights( &mut self, type_id: TypeId, - ) -> Option)>> { + ) -> Option)>> { self.inlay_highlights.remove(&Some(type_id)) } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 19b3ec792425117f09b3dcc259642af15dcae743..cc730f933321cdc89b3b76d7ca5a436261b6d8a4 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1108,7 +1108,7 @@ impl InlaySnapshot { &self, inlay_highlights: &std::sync::Arc<( HighlightStyle, - Vec, + Vec, )>, transform_start: Anchor, transform_end: Anchor, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6c5d1a45ac05b65f4a214e5a7453c3a642d776f3..af352d2649e387a47125da14ae1ac335a8cb6800 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,7 +65,7 @@ use language::{ OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, InlayCoordinates, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, InlayRange, LinkGoToDefinitionState, }; use log::error; use multi_buffer::ToOffsetUtf16; @@ -7720,7 +7720,7 @@ impl Editor { pub fn highlight_inlays( &mut self, - ranges: Vec, + ranges: Vec, style: HighlightStyle, cx: &mut ViewContext, ) { @@ -7737,20 +7737,7 @@ impl Editor { self.display_map.read(cx).text_highlights(TypeId::of::()) } - pub fn clear_text_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Option>)>> { - let highlights = self - .display_map - .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if highlights.is_some() { - cx.notify(); - } - highlights - } - - pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + pub fn clear_text_highlights(&mut self, cx: &mut ViewContext) { let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); @@ -8353,7 +8340,7 @@ impl View for Editor { self.link_go_to_definition_state.task = None; - self.clear_highlights::(cx); + self.clear_text_highlights::(cx); } false diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 10809d896296bf5e85fb04ccdee5a564b7cfab25..2be5105b33493724e43d3a65b4a631b3551f2139 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,7 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, InlayCoordinates, + GoToDefinitionTrigger, InlayRange, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -1929,7 +1929,7 @@ fn update_inlay_link_and_hover_points( update_go_to_definition_link( editor, GoToDefinitionTrigger::InlayHint( - InlayCoordinates { + InlayRange { inlay_position: hovered_hint.position, highlight_start: part_range.start, highlight_end: part_range.end, diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ee2b536042662055e2e252990c9689caf59be705..5bc45720a2be411c88d312a2a368c6e0f08bd9c4 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -11,7 +11,7 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { pub last_trigger_point: Option, - pub symbol_range: Option, + pub symbol_range: Option, pub kind: Option, pub definitions: Vec, pub task: Option>>, @@ -19,12 +19,12 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(InlayCoordinates, LocationLink), + InlayHint(InlayRange, LocationLink), None, } #[derive(Debug, Clone, Copy)] -pub struct InlayCoordinates { +pub struct InlayRange { pub inlay_position: Anchor, pub highlight_start: InlayOffset, pub highlight_end: InlayOffset, @@ -33,28 +33,28 @@ pub struct InlayCoordinates { #[derive(Debug, Clone)] pub enum TriggerPoint { Text(Anchor), - InlayHint(InlayCoordinates, LocationLink), + InlayHint(InlayRange, LocationLink), } #[derive(Debug, Clone)] -pub enum SymbolRange { +pub enum DocumentRange { Text(Range), - Inlay(InlayCoordinates), + Inlay(InlayRange), } -impl SymbolRange { +impl DocumentRange { fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { match (self, trigger_point) { - (SymbolRange::Text(range), TriggerPoint::Text(point)) => { + (DocumentRange::Text(range), TriggerPoint::Text(point)) => { let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } - (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } - (SymbolRange::Inlay(_), TriggerPoint::Text(_)) - | (SymbolRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + (DocumentRange::Inlay(_), TriggerPoint::Text(_)) + | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, } } } @@ -218,7 +218,7 @@ pub fn show_link_definition( .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - SymbolRange::Text(start..end) + DocumentRange::Text(start..end) }) }), definition_result, @@ -226,14 +226,14 @@ pub fn show_link_definition( }) } TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( - Some(SymbolRange::Inlay(trigger_source.clone())), + Some(DocumentRange::Inlay(trigger_source.clone())), vec![trigger_target.clone()], )), }; this.update(&mut cx, |this, cx| { // Clear any existing highlights - this.clear_highlights::(cx); + this.clear_text_highlights::(cx); this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() @@ -276,24 +276,24 @@ pub fn show_link_definition( let snapshot = &snapshot.buffer_snapshot; // If no symbol range returned from language server, use the surrounding word. let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - SymbolRange::Text( + DocumentRange::Text( snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end), ) } TriggerPoint::InlayHint(inlay_coordinates, _) => { - SymbolRange::Inlay(inlay_coordinates) + DocumentRange::Inlay(inlay_coordinates) } }); match highlight_range { - SymbolRange::Text(text_range) => this + DocumentRange::Text(text_range) => this .highlight_text::( vec![text_range], style, cx, ), - SymbolRange::Inlay(inlay_coordinates) => this + DocumentRange::Inlay(inlay_coordinates) => this .highlight_inlays::( vec![inlay_coordinates], style, @@ -325,7 +325,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { editor.link_go_to_definition_state.task = None; - editor.clear_highlights::(cx); + editor.clear_text_highlights::(cx); } pub fn go_to_fetched_definition( From 12ffbe54fb2c07ceafd2722aea9ee767a31e8c72 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 22:38:49 +0300 Subject: [PATCH 17/67] Unify text and inlay highlights --- crates/editor/src/display_map.rs | 43 +++---- crates/editor/src/display_map/block_map.rs | 6 +- crates/editor/src/display_map/fold_map.rs | 19 +-- crates/editor/src/display_map/inlay_map.rs | 113 +++++------------- crates/editor/src/display_map/tab_map.rs | 29 ++--- crates/editor/src/display_map/wrap_map.rs | 8 +- crates/editor/src/editor.rs | 13 +- crates/editor/src/link_go_to_definition.rs | 7 ++ crates/editor/src/test/editor_test_context.rs | 1 + 9 files changed, 76 insertions(+), 163 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 4f08be73b9400ce1c3bbdf59979a19cb90af1e22..037854435b6ee588044c57a96b08c5b0b933fb67 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,8 +5,8 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayRange, Anchor, AnchorRangeExt, InlayId, MultiBuffer, - MultiBufferSnapshot, ToOffset, ToPoint, + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; @@ -42,8 +42,7 @@ pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } -type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; -type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; +type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -54,7 +53,6 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, - inlay_highlights: InlayHighlights, pub clip_at_line_ends: bool, } @@ -90,7 +88,6 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), - inlay_highlights: Default::default(), clip_at_line_ends: false, } } @@ -115,7 +112,6 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), - inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, } } @@ -218,8 +214,10 @@ impl DisplayMap { ranges: Vec>, style: HighlightStyle, ) { - self.text_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + self.text_highlights.insert( + Some(type_id), + Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())), + ); } pub fn highlight_inlays( @@ -228,11 +226,16 @@ impl DisplayMap { ranges: Vec, style: HighlightStyle, ) { - self.inlay_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + self.text_highlights.insert( + Some(type_id), + Arc::new(( + style, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + )), + ); } - pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) } @@ -240,17 +243,10 @@ impl DisplayMap { pub fn clear_text_highlights( &mut self, type_id: TypeId, - ) -> Option>)>> { + ) -> Option)>> { self.text_highlights.remove(&Some(type_id)) } - pub fn clear_inlay_highlights( - &mut self, - type_id: TypeId, - ) -> Option)>> { - self.inlay_highlights.remove(&Some(type_id)) - } - pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) @@ -320,7 +316,6 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, - inlay_highlights: InlayHighlights, clip_at_line_ends: bool, } @@ -446,7 +441,6 @@ impl DisplaySnapshot { None, None, None, - None, ) .map(|h| h.text) } @@ -455,7 +449,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None, None, None) + .chunks(row..row + 1, false, None, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -474,7 +468,6 @@ impl DisplaySnapshot { display_rows, language_aware, Some(&self.text_highlights), - Some(&self.inlay_highlights), inlay_highlight_style, suggestion_highlight_style, ) @@ -797,7 +790,7 @@ impl DisplaySnapshot { #[cfg(any(test, feature = "test-support"))] pub fn highlight_ranges( &self, - ) -> Option>)>> { + ) -> Option)>> { let type_id = TypeId::of::(); self.text_highlights.get(&Some(type_id)).cloned() } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 083d8be5f32e890b8593f5d090a94656f0813fb6..8577e928199a1af11612a813bd1c0648a2f409d4 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,6 +1,6 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; @@ -579,7 +579,6 @@ impl BlockSnapshot { None, None, None, - None, ) .map(|chunk| chunk.text) .collect() @@ -590,7 +589,6 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> BlockChunks<'a> { @@ -625,7 +623,6 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -1507,7 +1504,6 @@ mod tests { None, None, None, - None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 800b51fcd850e767446c84408c99d53a70b2a0ab..dcbc156c47f1dbc65d10c013095996898d4ca32b 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; @@ -475,7 +475,7 @@ pub struct FoldSnapshot { impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None, None, None, None) + self.chunks(FoldOffset(0)..self.len(), false, None, None, None) .map(|c| c.text) .collect() } @@ -652,7 +652,6 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> FoldChunks<'a> { @@ -676,7 +675,6 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -689,15 +687,8 @@ impl FoldSnapshot { } pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { - self.chunks( - start.to_offset(self)..self.len(), - false, - None, - None, - None, - None, - ) - .flat_map(|chunk| chunk.text.chars()) + self.chunks(start.to_offset(self)..self.len(), false, None, None, None) + .flat_map(|chunk| chunk.text.chars()) } #[cfg(test)] @@ -1505,7 +1496,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, None, None, None, None) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index cc730f933321cdc89b3b76d7ca5a436261b6d8a4..06d166b86cab6151cd4a92837813238c9457dcda 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,8 +1,9 @@ use crate::{ + link_go_to_definition::DocumentRange, multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; -use collections::{BTreeMap, BTreeSet, HashSet}; +use collections::{BTreeMap, BTreeSet}; use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use std::{ @@ -15,7 +16,7 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::{InlayHighlights, TextHighlights}; +use super::TextHighlights; pub struct InlayMap { snapshot: InlaySnapshot, @@ -244,7 +245,6 @@ impl<'a> Iterator for InlayChunks<'a> { return None; } - // TODO kb highlights are not displayed still let mut next_highlight_endpoint = InlayOffset(usize::MAX); while let Some(endpoint) = self.highlight_endpoints.peek().copied() { if endpoint.offset <= self.output_offset { @@ -993,7 +993,6 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> InlayChunks<'a> { @@ -1002,15 +1001,14 @@ impl InlaySnapshot { let empty_text_highlights = TextHighlights::default(); let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); - let empty_inlay_highlights = InlayHighlights::default(); - let inlay_highlights = inlay_highlights.unwrap_or_else(|| &empty_inlay_highlights); let mut highlight_endpoints = Vec::new(); - if !text_highlights.is_empty() || !inlay_highlights.is_empty() { + if !text_highlights.is_empty() { while cursor.start().0 < range.end { let transform_start = self .buffer .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); + let transform_start = self.to_inlay_offset(transform_start.to_offset(&self.buffer)); let transform_end = { let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); @@ -1019,15 +1017,17 @@ impl InlaySnapshot { cursor.start().0 + overshoot, ))) }; + let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); - let mut covered_tags = HashSet::default(); for (tag, text_highlights) in text_highlights.iter() { - covered_tags.insert(*tag); let style = text_highlights.0; let ranges = &text_highlights.1; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); + let cmp = self + .document_to_inlay_range(probe) + .end + .cmp(&transform_start); if cmp.is_gt() { cmp::Ordering::Greater } else { @@ -1037,46 +1037,24 @@ impl InlaySnapshot { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { + let range = self.document_to_inlay_range(range); + if range.start.cmp(&transform_end).is_ge() { break; } highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + offset: range.start, is_start: true, tag: *tag, style, }); highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + offset: range.end, is_start: false, tag: *tag, style, }); } - - if let Some(inlay_highlights) = inlay_highlights.get(tag) { - self.push_inlay_highlight_range( - inlay_highlights, - transform_start, - transform_end, - &mut highlight_endpoints, - tag, - ); - } - } - - for (tag, inlay_highlights) in inlay_highlights - .iter() - .filter(|(tag, _)| !covered_tags.contains(tag)) - { - self.push_inlay_highlight_range( - inlay_highlights, - transform_start, - transform_end, - &mut highlight_endpoints, - tag, - ); } cursor.next(&()); @@ -1104,60 +1082,23 @@ impl InlaySnapshot { } } - fn push_inlay_highlight_range( - &self, - inlay_highlights: &std::sync::Arc<( - HighlightStyle, - Vec, - )>, - transform_start: Anchor, - transform_end: Anchor, - highlight_endpoints: &mut Vec, - tag: &Option, - ) { - let style = inlay_highlights.0; - let ranges = &inlay_highlights.1; - let start_ix = match ranges - .binary_search_by(|probe| probe.inlay_position.cmp(&transform_start, &self.buffer)) - { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range - .inlay_position - .cmp(&transform_end, &self.buffer) - .is_ge() - { - break; + fn document_to_inlay_range(&self, range: &DocumentRange) -> Range { + match range { + DocumentRange::Text(text_range) => { + self.to_inlay_offset(text_range.start.to_offset(&self.buffer)) + ..self.to_inlay_offset(text_range.end.to_offset(&self.buffer)) + } + DocumentRange::Inlay(inlay_range) => { + inlay_range.highlight_start..inlay_range.highlight_end } - - highlight_endpoints.push(HighlightEndpoint { - offset: range.highlight_start, - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.highlight_end, - is_start: false, - tag: *tag, - style, - }); } } #[cfg(test)] pub fn text(&self) -> String { - self.chunks( - Default::default()..self.len(), - false, - None, - None, - None, - None, - ) - .map(|chunk| chunk.text) - .collect() + self.chunks(Default::default()..self.len(), false, None, None, None) + .map(|chunk| chunk.text) + .collect() } fn check_invariants(&self) { @@ -1651,6 +1592,8 @@ mod tests { .map(|range| { buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) }) + // TODO add inlay highlight tests + .map(DocumentRange::Text) .collect::>(); highlights.insert( @@ -1731,8 +1674,6 @@ mod tests { InlayOffset(start)..InlayOffset(end), false, Some(&highlights), - // TODO kb add tests - None, None, None, ) diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 187a8de1d30aa450cacaf35711b5586a95ab9fb5..cae9ccc91f4b0a9bb2c9d208f96b82157ce5a176 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,6 +1,6 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::fonts::HighlightStyle; @@ -71,7 +71,6 @@ impl TabMap { None, None, None, - None, ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -184,7 +183,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None, None, None) + .chunks(range.start..line_end, false, None, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -204,7 +203,6 @@ impl TabSnapshot { None, None, None, - None, ) .flat_map(|chunk| chunk.text.chars()) { @@ -225,9 +223,8 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, - // TODO kb extract into one struct + // TODO kb extract into one struct? text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> TabChunks<'a> { @@ -250,7 +247,6 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -275,16 +271,9 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks( - TabPoint::zero()..self.max_point(), - false, - None, - None, - None, - None, - ) - .map(|chunk| chunk.text) - .collect() + self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -612,7 +601,6 @@ mod tests { None, None, None, - None, ) .map(|c| c.text) .collect::(), @@ -687,8 +675,7 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None, None) - { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -757,7 +744,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None, None, None) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f8287af799890e59b1b4b1c72f845fd1d26a76d5..d4b52d5893328e5e5abf50c20c842cf8c4a5622b 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,7 +1,7 @@ use super::{ fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::{ @@ -447,7 +447,6 @@ impl WrapSnapshot { None, None, None, - None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -577,7 +576,6 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> WrapChunks<'a> { @@ -597,7 +595,6 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -1329,7 +1326,6 @@ mod tests { None, None, None, - None, ) .map(|h| h.text) } @@ -1354,7 +1350,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None, None, None) + .chunks(start_row..end_row, true, None, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index af352d2649e387a47125da14ae1ac335a8cb6800..99db78f1c2635950452ae6fafaf46be1cc41db8c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,7 +65,7 @@ use language::{ OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, InlayRange, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, DocumentRange, InlayRange, LinkGoToDefinitionState, }; use log::error; use multi_buffer::ToOffsetUtf16; @@ -7733,7 +7733,7 @@ impl Editor { pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, - ) -> Option<(HighlightStyle, &'a [Range])> { + ) -> Option<(HighlightStyle, &'a [DocumentRange])> { self.display_map.read(cx).text_highlights(TypeId::of::()) } @@ -7741,10 +7741,7 @@ impl Editor { let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - let inlay_highlights = self - .display_map - .update(cx, |map, _| map.clear_inlay_highlights(TypeId::of::())); - if text_highlights.is_some() || inlay_highlights.is_some() { + if text_highlights.is_some() { cx.notify(); } } @@ -7953,6 +7950,8 @@ impl Editor { Some( ranges .iter() + // TODO kb mark inlays too + .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) }) @@ -8406,6 +8405,8 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; + // TODO kb mark inlays too + let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 5bc45720a2be411c88d312a2a368c6e0f08bd9c4..30f273065a54623edc084534e60e18869db00187 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -43,6 +43,13 @@ pub enum DocumentRange { } impl DocumentRange { + pub fn as_text_range(&self) -> Option> { + match self { + Self::Text(range) => Some(range.clone()), + Self::Inlay(_) => None, + } + } + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { match (self, trigger_point) { (DocumentRange::Text(range), TriggerPoint::Text(point)) => { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 118cddaa9226a543ca479f577428237d77539d5d..8d7b6aa5c98700741dd5d17370bd9e3f97024a86 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -240,6 +240,7 @@ impl<'a> EditorTestContext<'a> { .map(|ranges| ranges.as_ref().clone().1) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect(); assert_set_eq!(actual_ranges, expected_ranges); From 4b78678923018f47dc80747dd9e826564edd9a21 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 13:24:17 +0300 Subject: [PATCH 18/67] Prepare background highlights for inlay highlights --- crates/editor/src/display_map.rs | 14 +- crates/editor/src/display_map/inlay_map.rs | 4 + crates/editor/src/editor.rs | 163 +++++++++--------- crates/editor/src/test/editor_test_context.rs | 1 + 4 files changed, 96 insertions(+), 86 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 037854435b6ee588044c57a96b08c5b0b933fb67..34e877a4483e70169b92d8a075d38bc116c0cef2 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -416,8 +416,18 @@ impl DisplaySnapshot { .to_offset(self.display_point_to_inlay_point(point, bias)) } - pub fn inlay_point_to_inlay_offset(&self, point: InlayPoint) -> InlayOffset { - self.inlay_snapshot.to_offset(point) + pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset { + self.inlay_snapshot + .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot)) + } + + pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint { + let inlay_point = self.inlay_snapshot.to_point(offset); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) } fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 06d166b86cab6151cd4a92837813238c9457dcda..5094d2fab9595b613f183f587de163d1b039df51 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -392,6 +392,10 @@ impl InlayPoint { pub fn row(self) -> u32 { self.0.row } + + pub fn column(self) -> u32 { + self.0.column + } } impl InlayMap { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 99db78f1c2635950452ae6fafaf46be1cc41db8c..2089fe0f5fa9ce839df6519f27bbea6ac51d21fa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -535,6 +535,8 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; +type BackgroundHighlight = (fn(&Theme) -> Color, Vec); + pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, @@ -564,8 +566,7 @@ pub struct Editor { show_wrap_guides: Option, placeholder_text: Option>, highlighted_rows: Option>, - #[allow(clippy::type_complexity)] - background_highlights: BTreeMap Color, Vec>)>, + background_highlights: BTreeMap, nav_history: Option, context_menu: Option, mouse_context_menu: ViewHandle, @@ -6758,10 +6759,18 @@ impl Editor { let rename_range = if let Some(range) = prepare_rename.await? { Some(range) } else { - this.read_with(&cx, |this, cx| { + this.update(&mut cx, |this, cx| { let buffer = this.buffer.read(cx).snapshot(cx); + let display_snapshot = this + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); let mut buffer_highlights = this - .document_highlights_for_position(selection.head(), &buffer) + .document_highlights_for_position( + selection.head(), + &buffer, + &display_snapshot, + ) + .filter_map(|highlight| highlight.as_text_range()) .filter(|highlight| { highlight.start.excerpt_id() == selection.head().excerpt_id() && highlight.end.excerpt_id() == selection.head().excerpt_id() @@ -6816,11 +6825,15 @@ impl Editor { let ranges = this .clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges) + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }) .chain( this.clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges), + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }), ) .collect(); @@ -7488,16 +7501,20 @@ impl Editor { color_fetcher: fn(&Theme) -> Color, cx: &mut ViewContext, ) { - self.background_highlights - .insert(TypeId::of::(), (color_fetcher, ranges)); + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Text).collect(), + ), + ); cx.notify(); } - #[allow(clippy::type_complexity)] pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, - ) -> Option<(fn(&Theme) -> Color, Vec>)> { + ) -> Option { let highlights = self.background_highlights.remove(&TypeId::of::()); if highlights.is_some() { cx.notify(); @@ -7522,7 +7539,8 @@ impl Editor { &'a self, position: Anchor, buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator> { + display_snapshot: &'a DisplaySnapshot, + ) -> impl 'a + Iterator { let read_highlights = self .background_highlights .get(&TypeId::of::()) @@ -7531,14 +7549,16 @@ impl Editor { .background_highlights .get(&TypeId::of::()) .map(|h| &h.1); - let left_position = position.bias_left(buffer); - let right_position = position.bias_right(buffer); + let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer)); + let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer)); read_highlights .into_iter() .chain(write_highlights) .flat_map(move |ranges| { let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&left_position, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&left_position); if cmp.is_ge() { Ordering::Greater } else { @@ -7549,9 +7569,12 @@ impl Editor { }; let right_position = right_position.clone(); - ranges[start_ix..] - .iter() - .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) + ranges[start_ix..].iter().take_while(move |range| { + document_to_inlay_range(range, &display_snapshot) + .start + .cmp(&right_position) + .is_le() + }) }) } @@ -7561,12 +7584,15 @@ impl Editor { display_snapshot: &DisplaySnapshot, theme: &Theme, ) -> Vec<(Range, Color)> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; for (color_fetcher, ranges) in self.background_highlights.values() { let color = color_fetcher(theme); let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7576,61 +7602,16 @@ impl Editor { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, color)) - } - } - results - } - pub fn background_highlights_in_range_for( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - theme: &Theme, - ) -> Vec<(Range, Color)> { - let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; - let Some((color_fetcher, ranges)) = self.background_highlights - .get(&TypeId::of::()) else { - return vec![]; - }; - let color = color_fetcher(theme); - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { - break; + let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); + let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); + results.push((start..end, color)) } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, color)) } - results } @@ -7640,15 +7621,18 @@ impl Editor { display_snapshot: &DisplaySnapshot, count: usize, ) -> Vec> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; let Some((_, ranges)) = self.background_highlights .get(&TypeId::of::()) else { return vec![]; }; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7657,30 +7641,28 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; - let mut push_region = |start: Option, end: Option| { + let mut push_region = |start: Option, end: Option| { if let (Some(start_display), Some(end_display)) = (start, end) { - results.push( - start_display.to_display_point(display_snapshot) - ..=end_display.to_display_point(display_snapshot), - ); + results.push(start_display..=end_display); } }; - let mut start_row: Option = None; - let mut end_row: Option = None; + let mut start_row: Option = None; + let mut end_row: Option = None; if ranges.len() > count { - return vec![]; + return Vec::new(); } for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let end = range.end.to_point(buffer); + let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); if let Some(current_row) = &end_row { - if end.row == current_row.row { + if end.row() == current_row.row() { continue; } } - let start = range.start.to_point(buffer); + let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); if start_row.is_none() { assert_eq!(end_row, None); @@ -7689,7 +7671,7 @@ impl Editor { continue; } if let Some(current_end) = end_row.as_mut() { - if start.row > current_end.row + 1 { + if start.row() > current_end.row() + 1 { push_region(start_row, end_row); start_row = Some(start); end_row = Some(end); @@ -8133,6 +8115,19 @@ impl Editor { } } +fn document_to_inlay_range( + range: &DocumentRange, + snapshot: &DisplaySnapshot, +) -> Range { + match range { + DocumentRange::Text(text_range) => { + snapshot.anchor_to_inlay_offset(text_range.start) + ..snapshot.anchor_to_inlay_offset(text_range.end) + } + DocumentRange::Inlay(inlay_range) => inlay_range.highlight_start..inlay_range.highlight_end, + } +} + fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 8d7b6aa5c98700741dd5d17370bd9e3f97024a86..033525395e17f0db865fff79c225338f282ec889 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -225,6 +225,7 @@ impl<'a> EditorTestContext<'a> { .map(|h| h.1.clone()) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect() }); From bcaff0a18a924412348dbf4dfee7fbb2c6176856 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 15:14:25 +0300 Subject: [PATCH 19/67] Propagate inlay background highlights to data storage --- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/display_map/tab_map.rs | 1 - crates/editor/src/editor.rs | 18 +++- crates/editor/src/element.rs | 87 ++++++++++++---- crates/editor/src/hover_popover.rs | 109 +++++++++++++++++++-- crates/project/src/lsp_command.rs | 1 - 6 files changed, 185 insertions(+), 33 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 5094d2fab9595b613f183f587de163d1b039df51..56df722f525d8b3909bc60fccbd3c873dcfd1597 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1596,7 +1596,7 @@ mod tests { .map(|range| { buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) }) - // TODO add inlay highlight tests + // TODO kb add inlay highlight tests .map(DocumentRange::Text) .collect::>(); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index cae9ccc91f4b0a9bb2c9d208f96b82157ce5a176..fcdef17a8b65f250a0e355ad10731cfdf8c3350b 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -223,7 +223,6 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, - // TODO kb extract into one struct? text_highlights: Option<&'a TextHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2089fe0f5fa9ce839df6519f27bbea6ac51d21fa..b21da05958fe2edbe75eb21b727cd3acba18135e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7511,6 +7511,22 @@ impl Editor { cx.notify(); } + pub fn highlight_inlay_background( + &mut self, + ranges: Vec, + color_fetcher: fn(&Theme) -> Color, + cx: &mut ViewContext, + ) { + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + ), + ); + cx.notify(); + } + pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, @@ -7932,7 +7948,6 @@ impl Editor { Some( ranges .iter() - // TODO kb mark inlays too .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) @@ -8400,7 +8415,6 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; - // TODO kb mark inlays too let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2be5105b33493724e43d3a65b4a631b3551f2139..e9a154ddb0185fb0f4485a5d72765a0cac0dcd61 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8,8 +8,8 @@ use crate::{ editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ - hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, - MIN_POPOVER_LINE_HEIGHT, + hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP, + MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, @@ -43,7 +43,8 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, Location, LocationLink, ProjectPath, ResolveState, + HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, + Location, LocationLink, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -1860,8 +1861,7 @@ fn update_inlay_link_and_hover_points( None }; if let Some(hovered_offset) = hovered_offset { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); + let snapshot = editor.buffer().read(cx).snapshot(cx); let previous_valid_anchor = snapshot.anchor_at( point_for_position .previous_valid @@ -1885,15 +1885,14 @@ fn update_inlay_link_and_hover_points( .max_by_key(|hint| hint.id) { let inlay_hint_cache = editor.inlay_hint_cache(); - if let Some(cached_hint) = - inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) - { + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { if let Some(buffer_id) = previous_valid_anchor.buffer_id { inlay_hint_cache.spawn_hint_resolve( buffer_id, - previous_valid_anchor.excerpt_id, + excerpt_id, hovered_hint.id, cx, ); @@ -1902,9 +1901,33 @@ fn update_inlay_link_and_hover_points( ResolveState::Resolved => { match cached_hint.label { project::InlayHintLabel::String(_) => { - if cached_hint.tooltip.is_some() { - dbg!(&cached_hint.tooltip); // TODO kb - // hover_at_point = Some(hovered_offset); + if let Some(tooltip) = cached_hint.tooltip { + hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: hint_start_offset, + highlight_end: hint_end_offset, + }, + }, + cx, + ); + hover_updated = true; } } project::InlayHintLabel::LabelParts(label_parts) => { @@ -1915,15 +1938,41 @@ fn update_inlay_link_and_hover_points( hovered_offset, ) { - if hovered_hint_part.tooltip.is_some() { - dbg!(&hovered_hint_part.tooltip); // TODO kb - // hover_at_point = Some(hovered_offset); + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + }, + cx, + ); + hover_updated = true; } if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = cached_hint - .position - .buffer_id - .and_then(|buffer_id| buffer.buffer(buffer_id)) + if let Some(buffer) = + cached_hint.position.buffer_id.and_then(|buffer_id| { + editor.buffer().read(cx).buffer(buffer_id) + }) { go_to_definition_updated = true; update_go_to_definition_link( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e4509a765cb82583188188eebd8a061e48feaf86..8e8babb44aa3afe47b90a2ef647ab70cbec148c8 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,8 @@ use crate::{ - display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, - EditorSnapshot, EditorStyle, RangeToAnchorExt, + display_map::{InlayOffset, ToDisplayPoint}, + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, + ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; use gpui::{ @@ -46,6 +48,84 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC } } +pub struct InlayHover { + pub excerpt: ExcerptId, + pub triggered_from: InlayOffset, + pub range: InlayRange, + pub tooltip: HoverBlock, +} + +pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { + if settings::get::(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } + + let Some(project) = editor.project.clone() else { + return; + }; + + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if let DocumentRange::Inlay(range) = symbol_range { + if (range.highlight_start..=range.highlight_end) + .contains(&inlay_hover.triggered_from) + { + // Hover triggered from same location as last time. Don't show again. + return; + } + } + hide_hover(editor, cx); + } + + let snapshot = editor.snapshot(cx); + // Don't request again if the location is the same as the previous request + if let Some(triggered_from) = editor.hover_state.triggered_from { + if inlay_hover.triggered_from + == snapshot + .display_snapshot + .anchor_to_inlay_offset(triggered_from) + { + return; + } + } + + let task = cx.spawn(|this, mut cx| { + async move { + cx.background() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) + .await; + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = None; + })?; + + let hover_popover = InfoPopover { + project: project.clone(), + symbol_range: DocumentRange::Inlay(inlay_hover.range), + blocks: vec![inlay_hover.tooltip], + language: None, + rendered_content: None, + }; + + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| theme.editor.hover_popover.highlight, + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + + editor.hover_state.info_task = Some(task); + } +} + /// Hides the type information popup. /// Triggered by the `Hover` action when the cursor is not over a symbol or when the /// selections changed. @@ -110,8 +190,13 @@ fn show_hover( if !ignore_timeout { if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { if symbol_range - .to_offset(&snapshot.buffer_snapshot) - .contains(&multibuffer_offset) + .as_text_range() + .map(|range| { + range + .to_offset(&snapshot.buffer_snapshot) + .contains(&multibuffer_offset) + }) + .unwrap_or(false) { // Hover triggered from same location as last time. Don't show again. return; @@ -219,7 +304,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: range, + symbol_range: DocumentRange::Text(range), blocks: hover_result.contents, language: hover_result.language, rendered_content: None, @@ -227,10 +312,13 @@ fn show_hover( }); this.update(&mut cx, |this, cx| { - if let Some(hover_popover) = hover_popover.as_ref() { + if let Some(symbol_range) = hover_popover + .as_ref() + .and_then(|hover_popover| hover_popover.symbol_range.as_text_range()) + { // Highlight the selected symbol using a background highlight this.highlight_background::( - vec![hover_popover.symbol_range.clone()], + vec![symbol_range], |theme| theme.editor.hover_popover.highlight, cx, ); @@ -497,7 +585,10 @@ impl HoverState { .or_else(|| { self.info_popover .as_ref() - .map(|info_popover| &info_popover.symbol_range.start) + .map(|info_popover| match &info_popover.symbol_range { + DocumentRange::Text(range) => &range.start, + DocumentRange::Inlay(range) => &range.inlay_position, + }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -522,7 +613,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - pub symbol_range: Range, + symbol_range: DocumentRange, pub blocks: Vec, language: Option>, rendered_content: Option, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 20bb302b5b8963e996a1da47564f26ad191b56ad..c057718bf3abdf6bde3a222b0fea32f7f727b36a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2126,7 +2126,6 @@ impl InlayHints { }) } - // TODO kb instead, store all LSP data inside the project::InlayHint? pub fn project_to_lsp_hint( hint: InlayHint, project: &ModelHandle, From dcf570bb03d3ce7e6290c161c390ddbdd19b6a20 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 19:43:05 +0300 Subject: [PATCH 20/67] Fix resolve status conversion --- crates/editor/src/display_map.rs | 4 +- crates/editor/src/display_map/block_map.rs | 4 +- crates/editor/src/display_map/fold_map.rs | 4 +- crates/editor/src/display_map/inlay_map.rs | 116 ++++++++++----------- crates/editor/src/display_map/tab_map.rs | 4 +- crates/editor/src/display_map/wrap_map.rs | 4 +- crates/editor/src/editor.rs | 3 +- crates/project/src/lsp_command.rs | 4 +- crates/project/src/project.rs | 2 +- 9 files changed, 72 insertions(+), 73 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 34e877a4483e70169b92d8a075d38bc116c0cef2..611866bcadeaef851ba081434fadc04a2d3031ae 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -471,14 +471,14 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 8577e928199a1af11612a813bd1c0648a2f409d4..741507004cc9bc0064ba682701310b832111438f 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -589,7 +589,7 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); @@ -623,7 +623,7 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_chunk: Default::default(), diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index dcbc156c47f1dbc65d10c013095996898d4ca32b..d5473027a6b0145bad28f21c1e91ce7491f9eb63 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -652,7 +652,7 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -675,7 +675,7 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), inlay_chunk: None, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 56df722f525d8b3909bc60fccbd3c873dcfd1597..748e1f0cd8dcb85cf218d314c2c6b2205920f097 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -997,74 +997,74 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); - let empty_text_highlights = TextHighlights::default(); - let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); - let mut highlight_endpoints = Vec::new(); - if !text_highlights.is_empty() { - while cursor.start().0 < range.end { - let transform_start = self - .buffer - .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); - let transform_start = self.to_inlay_offset(transform_start.to_offset(&self.buffer)); - - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; - let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); - - for (tag, text_highlights) in text_highlights.iter() { - let style = text_highlights.0; - let ranges = &text_highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = self - .document_to_inlay_range(probe) - .end - .cmp(&transform_start); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, + if let Some(text_highlights) = text_highlights { + if !text_highlights.is_empty() { + while cursor.start().0 < range.end { + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); + let transform_start = + self.to_inlay_offset(transform_start.to_offset(&self.buffer)); + + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) }; - for range in &ranges[start_ix..] { - let range = self.document_to_inlay_range(range); - if range.start.cmp(&transform_end).is_ge() { - break; - } + let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); + + for (tag, text_highlights) in text_highlights.iter() { + let style = text_highlights.0; + let ranges = &text_highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = self + .document_to_inlay_range(probe) + .end + .cmp(&transform_start); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + let range = self.document_to_inlay_range(range); + if range.start.cmp(&transform_end).is_ge() { + break; + } - highlight_endpoints.push(HighlightEndpoint { - offset: range.start, - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.end, - is_start: false, - tag: *tag, - style, - }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.start, + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.end, + is_start: false, + tag: *tag, + style, + }); + } } - } - cursor.next(&()); + cursor.next(&()); + } + highlight_endpoints.sort(); + cursor.seek(&range.start, Bias::Right, &()); } - highlight_endpoints.sort(); - cursor.seek(&range.start, Bias::Right, &()); } let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); @@ -1078,7 +1078,7 @@ impl InlaySnapshot { buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style: inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index fcdef17a8b65f250a0e355ad10731cfdf8c3350b..2cf0471b37889a5cf5d3db26cfe3d1de91dc8e20 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -224,7 +224,7 @@ impl TabSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = @@ -246,7 +246,7 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_column, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index d4b52d5893328e5e5abf50c20c842cf8c4a5622b..f3600936f9bf77df6773ad14fd39f4e465398e15 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -576,7 +576,7 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); @@ -595,7 +595,7 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_chunk: Default::default(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b21da05958fe2edbe75eb21b727cd3acba18135e..785d43f0b639efa4d23e84eacef197df3d2e150f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4882,7 +4882,6 @@ impl Editor { if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; to_insert = &clipboard_text[start_offset..end_offset]; - dbg!(start_offset, end_offset, &clipboard_text, &to_insert); entire_line = clipboard_selection.is_entire_line; start_offset = end_offset + 1; original_indent_column = @@ -7586,7 +7585,7 @@ impl Editor { let right_position = right_position.clone(); ranges[start_ix..].iter().take_while(move |range| { - document_to_inlay_range(range, &display_snapshot) + document_to_inlay_range(range, display_snapshot) .start .cmp(&right_position) .is_le() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c057718bf3abdf6bde3a222b0fea32f7f727b36a..9f7799c555940607a78b4faedd63bf89b16ddada 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1935,8 +1935,9 @@ impl InlayHints { pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::Resolved => (0, None), ResolveState::CanResolve(server_id, resolve_data) => ( - 0, + 1, resolve_data .map(|json_data| { serde_json::to_string(&json_data) @@ -1947,7 +1948,6 @@ impl InlayHints { value, }), ), - ResolveState::Resolved => (1, None), ResolveState::Resolving => (2, None), }; let resolve_state = Some(proto::ResolveState { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fbdbd04664fa4a76ebfa9e7992fd35987da8ad2f..1fe307eec2bc66f2b1f484fc98a984af7d14234c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5091,7 +5091,7 @@ impl Project { InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) .await .map(Some) - .context("inlay hints proto response conversion") + .context("inlay hints proto resolve response conversion") } None => Ok(None), } From 7cd60d6afb95d777644a27e9e3096129c19771c0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 20:22:38 +0300 Subject: [PATCH 21/67] Simplify and restore client resolve capabilities --- crates/editor/src/display_map/inlay_map.rs | 4 ---- crates/editor/src/editor.rs | 23 ++++++++++++++-------- crates/lsp/src/lsp.rs | 4 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 748e1f0cd8dcb85cf218d314c2c6b2205920f097..1b65719448a0c74df3f747b5e0e35ef1e605303a 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -392,10 +392,6 @@ impl InlayPoint { pub fn row(self) -> u32 { self.0.row } - - pub fn column(self) -> u32 { - self.0.column - } } impl InlayMap { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 785d43f0b639efa4d23e84eacef197df3d2e150f..681e1d48b277a60ef0bd4d53cfc9726caed9fb4a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7656,13 +7656,16 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; - let mut push_region = |start: Option, end: Option| { + let mut push_region = |start: Option, end: Option| { if let (Some(start_display), Some(end_display)) = (start, end) { - results.push(start_display..=end_display); + results.push( + start_display.to_display_point(display_snapshot) + ..=end_display.to_display_point(display_snapshot), + ); } }; - let mut start_row: Option = None; - let mut end_row: Option = None; + let mut start_row: Option = None; + let mut end_row: Option = None; if ranges.len() > count { return Vec::new(); } @@ -7671,13 +7674,17 @@ impl Editor { if range.start.cmp(&search_range.end).is_ge() { break; } - let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); + let end = display_snapshot + .inlay_offset_to_display_point(range.end, Bias::Right) + .to_point(display_snapshot); if let Some(current_row) = &end_row { - if end.row() == current_row.row() { + if end.row == current_row.row { continue; } } - let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); + let start = display_snapshot + .inlay_offset_to_display_point(range.start, Bias::Left) + .to_point(display_snapshot); if start_row.is_none() { assert_eq!(end_row, None); @@ -7686,7 +7693,7 @@ impl Editor { continue; } if let Some(current_end) = end_row.as_mut() { - if start.row() > current_end.row() + 1 { + if start.row > current_end.row + 1 { push_region(start_row, end_row); start_row = Some(start); end_row = Some(end); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 78c858a90c46e2af349027ed3f4f361fe8805752..e0ae64d8069c08b12e11b8b12155892dc974ae0d 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,7 +434,9 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: None, + resolve_support: Some(InlayHintResolveClientCapabilities { + properties: vec!["textEdits".to_string(), "tooltip".to_string()], + }), dynamic_registration: Some(false), }), ..Default::default() From 852427e87b0f6c7b56b7dca38536a255c25aef86 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:08:08 +0300 Subject: [PATCH 22/67] Use inlay highlights in randomized tests --- crates/editor/src/display_map/inlay_map.rs | 57 +++++++++++++--------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 1b65719448a0c74df3f747b5e0e35ef1e605303a..25b8d3aef6a28b959a6092e1cfba4adf031dd125 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1144,13 +1144,12 @@ fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { #[cfg(test)] mod tests { use super::*; - use crate::{InlayId, MultiBuffer}; + use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer}; use gpui::AppContext; use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; - use sum_tree::TreeMap; use text::Patch; use util::post_inc; @@ -1579,28 +1578,6 @@ mod tests { let mut buffer_snapshot = buffer.read(cx).snapshot(cx); let mut next_inlay_id = 0; log::info!("buffer text: {:?}", buffer_snapshot.text()); - - let mut highlights = TreeMap::default(); - let highlight_count = rng.gen_range(0_usize..10); - let mut highlight_ranges = (0..highlight_count) - .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) - .collect::>(); - highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); - log::info!("highlighting ranges {:?}", highlight_ranges); - let highlight_ranges = highlight_ranges - .into_iter() - .map(|range| { - buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) - }) - // TODO kb add inlay highlight tests - .map(DocumentRange::Text) - .collect::>(); - - highlights.insert( - Some(TypeId::of::<()>()), - Arc::new((HighlightStyle::default(), highlight_ranges)), - ); - let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); for _ in 0..operations { let mut inlay_edits = Patch::default(); @@ -1663,6 +1640,38 @@ mod tests { ); } + let mut highlights = TextHighlights::default(); + let highlight_count = rng.gen_range(0_usize..10); + let mut highlight_ranges = (0..highlight_count) + .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) + .collect::>(); + highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); + log::info!("highlighting ranges {:?}", highlight_ranges); + let highlight_ranges = if rng.gen_bool(0.5) { + highlight_ranges + .into_iter() + .map(|range| InlayRange { + inlay_position: buffer_snapshot.anchor_before(range.start), + highlight_start: inlay_snapshot.to_inlay_offset(range.start), + highlight_end: inlay_snapshot.to_inlay_offset(range.end), + }) + .map(DocumentRange::Inlay) + .collect::>() + } else { + highlight_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end) + }) + .map(DocumentRange::Text) + .collect::>() + }; + highlights.insert( + Some(TypeId::of::<()>()), + Arc::new((HighlightStyle::default(), highlight_ranges)), + ); + for _ in 0..5 { let mut end = rng.gen_range(0..=inlay_snapshot.len().0); end = expected_text.clip_offset(end, Bias::Right); From 3c55c933d4064d06f3d5d3e2cf8336c3deefaf40 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:37:53 +0300 Subject: [PATCH 23/67] Be more lenient with hint resolution, always return some hint --- crates/editor/src/inlay_hint_cache.rs | 38 ++++++++++++--------------- crates/project/src/project.rs | 21 +++++++-------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 52a4039a76e6b31dd9e46f13de627591e5b916f8..d4325e13d94ad1e173ed34b5adeae34a49fcaa25 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -449,30 +449,26 @@ impl InlayHintCache { }) })?; if let Some(resolved_hint_task) = resolved_hint_task { - if let Some(mut resolved_hint) = - resolved_hint_task.await.context("hint resolve task")? - { - editor.update(&mut cx, |editor, _| { - if let Some(excerpt_hints) = - editor.inlay_hint_cache.hints.get(&excerpt_id) + let mut resolved_hint = + resolved_hint_task.await.context("hint resolve task")?; + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) { - let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard - .hints - .iter_mut() - .find(|(hint_id, _)| hint_id == &id) - .map(|(_, hint)| hint) - { - if cached_hint.resolve_state == ResolveState::Resolving - { - resolved_hint.resolve_state = - ResolveState::Resolved; - *cached_hint = resolved_hint; - } + if cached_hint.resolve_state == ResolveState::Resolving { + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; } } - })?; - } + } + })?; } anyhow::Ok(()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1fe307eec2bc66f2b1f484fc98a984af7d14234c..0bbb61dfcb44065b105ac331a414f54ae12ed17d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5033,7 +5033,7 @@ impl Project { buffer_handle: ModelHandle, server_id: LanguageServerId, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task> { if self.is_local() { let buffer = buffer_handle.read(cx); let (_, lang_server) = if let Some((adapter, server)) = @@ -5041,7 +5041,7 @@ impl Project { { (adapter.clone(), server.clone()) } else { - return Task::ready(Ok(None)); + return Task::ready(Ok(hint)); }; let can_resolve = lang_server .capabilities() @@ -5050,7 +5050,7 @@ impl Project { .and_then(|options| options.resolve_provider) .unwrap_or(false); if !can_resolve { - return Task::ready(Ok(None)); + return Task::ready(Ok(hint)); } let buffer_snapshot = buffer.snapshot(); @@ -5071,7 +5071,7 @@ impl Project { &mut cx, ) .await?; - Ok(Some(resolved_hint)) + Ok(resolved_hint) }) } else if let Some(project_id) = self.remote_id() { let client = self.client.clone(); @@ -5079,7 +5079,7 @@ impl Project { project_id, buffer_id: buffer_handle.read(cx).remote_id(), language_server_id: server_id.0 as u64, - hint: Some(InlayHints::project_to_proto_hint(hint, cx)), + hint: Some(InlayHints::project_to_proto_hint(hint.clone(), cx)), }; cx.spawn(|project, mut cx| async move { let response = client @@ -5090,10 +5090,9 @@ impl Project { Some(resolved_hint) => { InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) .await - .map(Some) .context("inlay hints proto resolve response conversion") } - None => Ok(None), + None => Ok(hint), } }) } else { @@ -6917,7 +6916,7 @@ impl Project { .and_then(|buffer| buffer.upgrade(cx)) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })?; - let resolved_hint = this + let response_hint = this .update(&mut cx, |project, cx| { project.resolve_inlay_hint( hint, @@ -6927,11 +6926,11 @@ impl Project { ) }) .await - .context("inlay hints fetch")? - .map(|hint| cx.read(|cx| InlayHints::project_to_proto_hint(hint, cx))); + .context("inlay hints fetch")?; + let resolved_hint = cx.read(|cx| InlayHints::project_to_proto_hint(response_hint, cx)); Ok(proto::ResolveInlayHintResponse { - hint: resolved_hint, + hint: Some(resolved_hint), }) } From abd2d012b1914fcea6830116a5dea88fbd69963d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:50:53 +0300 Subject: [PATCH 24/67] Properly binary search cached inlay hints --- crates/editor/src/inlay_hint_cache.rs | 38 ++++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d4325e13d94ad1e173ed34b5adeae34a49fcaa25..71b65de676a0503cc9461bc1401eb5ca236e36a4 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -714,13 +714,21 @@ fn calculate_hint_updates( probe.1.position.cmp(&new_hint.position, buffer_snapshot) }) { Ok(ix) => { - let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix]; - if cached_hint == &new_hint { - excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); - false - } else { - true + let mut missing_from_cache = true; + for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] { + if new_hint + .position + .cmp(&cached_hint.position, buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint == &new_hint { + excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); + missing_from_cache = false; + } } + missing_from_cache } Err(_) => true, } @@ -820,11 +828,21 @@ fn apply_hint_update( .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot)) { Ok(i) => { - if cached_hints[i].1.text() == new_hint.text() { - None - } else { - Some(i) + let mut insert_position = Some(i); + for (_, cached_hint) in &cached_hints[i..] { + if new_hint + .position + .cmp(&cached_hint.position, &buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint.text() == new_hint.text() { + insert_position = None; + break; + } } + insert_position } Err(i) => Some(i), }; From f19c659ed6aa63e56effddfcf7d3952d325854da Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 11:45:07 +0300 Subject: [PATCH 25/67] Add link_go_to_definition test for inlays --- crates/editor/src/element.rs | 232 +----------- crates/editor/src/hover_popover.rs | 23 +- crates/editor/src/inlay_hint_cache.rs | 14 +- crates/editor/src/link_go_to_definition.rs | 413 ++++++++++++++++++++- 4 files changed, 447 insertions(+), 235 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e9a154ddb0185fb0f4485a5d72765a0cac0dcd61..3ba807308c6718e2fcbf7e8edf0f9c9f9d08824e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,16 +4,16 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayOffset, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ - hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP, - MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, + hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, + MIN_POPOVER_LINE_HEIGHT, }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, InlayRange, + update_inlay_link_and_hover_points, GoToDefinitionTrigger, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -43,8 +43,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, - Location, LocationLink, ProjectPath, ResolveState, + ProjectPath, }; use smallvec::SmallVec; use std::{ @@ -478,10 +477,11 @@ impl EditorElement { } None => { update_inlay_link_and_hover_points( - position_map, + &position_map.snapshot, point_for_position, editor, - (cmd, shift), + cmd, + shift, cx, ); } @@ -1835,214 +1835,6 @@ impl EditorElement { } } -fn update_inlay_link_and_hover_points( - position_map: &PositionMap, - point_for_position: PointForPosition, - editor: &mut Editor, - (cmd_held, shift_held): (bool, bool), - cx: &mut ViewContext<'_, '_, Editor>, -) { - let hint_start_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); - let hint_end_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { - Some( - position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left), - ) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) - } else { - None - }; - if let Some(hovered_offset) = hovered_offset { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let previous_valid_anchor = snapshot.anchor_at( - point_for_position - .previous_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Left, - ); - let next_valid_anchor = snapshot.anchor_at( - point_for_position - .next_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Right, - ); - - let mut go_to_definition_updated = false; - let mut hover_updated = false; - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - let inlay_hint_cache = editor.inlay_hint_cache(); - let excerpt_id = previous_valid_anchor.excerpt_id; - if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { - match cached_hint.resolve_state { - ResolveState::CanResolve(_, _) => { - if let Some(buffer_id) = previous_valid_anchor.buffer_id { - inlay_hint_cache.spawn_hint_resolve( - buffer_id, - excerpt_id, - hovered_hint.id, - cx, - ); - } - } - ResolveState::Resolved => { - match cached_hint.label { - project::InlayHintLabel::String(_) => { - if let Some(tooltip) = cached_hint.tooltip { - hover_at_inlay( - editor, - InlayHover { - excerpt: excerpt_id, - tooltip: match tooltip { - InlayHintTooltip::String(text) => HoverBlock { - text, - kind: HoverBlockKind::PlainText, - }, - InlayHintTooltip::MarkupContent(content) => { - HoverBlock { - text: content.value, - kind: content.kind, - } - } - }, - triggered_from: hovered_offset, - range: InlayRange { - inlay_position: hovered_hint.position, - highlight_start: hint_start_offset, - highlight_end: hint_end_offset, - }, - }, - cx, - ); - hover_updated = true; - } - } - project::InlayHintLabel::LabelParts(label_parts) => { - if let Some((hovered_hint_part, part_range)) = - find_hovered_hint_part( - label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) - { - if let Some(tooltip) = hovered_hint_part.tooltip { - hover_at_inlay( - editor, - InlayHover { - excerpt: excerpt_id, - tooltip: match tooltip { - InlayHintLabelPartTooltip::String(text) => { - HoverBlock { - text, - kind: HoverBlockKind::PlainText, - } - } - InlayHintLabelPartTooltip::MarkupContent( - content, - ) => HoverBlock { - text: content.value, - kind: content.kind, - }, - }, - triggered_from: hovered_offset, - range: InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - }, - cx, - ); - hover_updated = true; - } - if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = - cached_hint.position.buffer_id.and_then(|buffer_id| { - editor.buffer().read(cx).buffer(buffer_id) - }) - { - go_to_definition_updated = true; - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::InlayHint( - InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - LocationLink { - origin: Some(Location { - buffer, - range: cached_hint.position - ..cached_hint.position, - }), - target: location, - }, - ), - cmd_held, - shift_held, - cx, - ); - } - } - } - } - }; - } - ResolveState::Resolving => {} - } - } - } - - if !go_to_definition_updated { - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::None, - cmd_held, - shift_held, - cx, - ); - } - if !hover_updated { - hover_at(editor, None, cx); - } - } -} - -fn find_hovered_hint_part( - label_parts: Vec, - hint_range: Range, - hovered_offset: InlayOffset, -) -> Option<(InlayHintLabelPart, Range)> { - if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { - let mut hovered_character = (hovered_offset - hint_range.start).0; - let mut part_start = hint_range.start; - for part in label_parts { - let part_len = part.value.chars().count(); - if hovered_character >= part_len { - hovered_character -= part_len; - part_start.0 += part_len; - } else { - return Some((part, part_start..InlayOffset(part_start.0 + part_len))); - } - } - } - None -} - struct HighlightedChunk<'a> { chunk: &'a str, style: Option, @@ -2871,12 +2663,12 @@ struct PositionMap { snapshot: EditorSnapshot, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct PointForPosition { - previous_valid: DisplayPoint, + pub previous_valid: DisplayPoint, pub next_valid: DisplayPoint, - exact_unclipped: DisplayPoint, - column_overshoot_after_line_end: u32, + pub exact_unclipped: DisplayPoint, + pub column_overshoot_after_line_end: u32, } impl PointForPosition { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 8e8babb44aa3afe47b90a2ef647ab70cbec148c8..6eae470badc812f7d4889e6ff705244b46ae2300 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -13,7 +13,7 @@ use gpui::{ AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; -use project::{HoverBlock, HoverBlockKind, Project}; +use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -55,6 +55,27 @@ pub struct InlayHover { pub tooltip: HoverBlock, } +pub fn find_hovered_hint_part( + label_parts: Vec, + hint_range: Range, + hovered_offset: InlayOffset, +) -> Option<(InlayHintLabelPart, Range)> { + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character >= part_len { + hovered_character -= part_len; + part_start.0 += part_len; + } else { + return Some((part, part_start..InlayOffset(part_start.0 + part_len))); + } + } + } + None +} + pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { if settings::get::(cx).hover_popover_enabled { if editor.pending_rename.is_some() { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 71b65de676a0503cc9461bc1401eb5ca236e36a4..b0c7d9e0f1a7c5bd64758126b199425f59b77151 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -904,7 +904,7 @@ fn apply_hint_update( } #[cfg(test)] -mod tests { +pub mod tests { use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use crate::{ @@ -2989,15 +2989,11 @@ all hints should be invalidated and requeried for all of its visible excerpts" ("/a/main.rs", editor, fake_server) } - fn cached_hint_labels(editor: &Editor) -> Vec { + pub fn cached_hint_labels(editor: &Editor) -> Vec { let mut labels = Vec::new(); for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - for (_, inlay) in excerpt_hints.hints.iter() { - match &inlay.label { - project::InlayHintLabel::String(s) => labels.push(s.to_string()), - _ => unreachable!(), - } + for (_, inlay) in &excerpt_hints.read().hints { + labels.push(inlay.text()); } } @@ -3005,7 +3001,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" labels } - fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { let mut hints = editor .visible_inlay_hints(cx) .into_iter() diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 30f273065a54623edc084534e60e18869db00187..ea22ea5eae15ee686067d4f1119cb6edd0a5ac0a 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,10 +1,15 @@ use crate::{ - display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, - EditorSnapshot, SelectPhase, + display_map::{DisplaySnapshot, InlayOffset}, + element::PointForPosition, + hover_popover::{self, InlayHover}, + Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; -use project::LocationLink; +use project::{ + HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, + LocationLink, ResolveState, +}; use std::ops::Range; use util::TryFutureExt; @@ -23,7 +28,7 @@ pub enum GoToDefinitionTrigger { None, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InlayRange { pub inlay_position: Anchor, pub highlight_start: InlayOffset, @@ -140,6 +145,192 @@ pub fn update_go_to_definition_link( hide_link_definition(editor, cx); } +pub fn update_inlay_link_and_hover_points( + snapshot: &DisplaySnapshot, + point_for_position: PointForPosition, + editor: &mut Editor, + cmd_held: bool, + shift_held: bool, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let hint_start_offset = + snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = + snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + if let Some(hovered_offset) = hovered_offset { + let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + let previous_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.previous_valid.to_point(snapshot), + Bias::Left, + ); + let next_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.next_valid.to_point(snapshot), + Bias::Right, + ); + + let mut go_to_definition_updated = false; + let mut hover_updated = false; + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position + .cmp(&previous_valid_anchor, &buffer_snapshot) + .is_lt() + }) + .take_while(|hint| { + hint.position + .cmp(&next_valid_anchor, &buffer_snapshot) + .is_le() + }) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { + match cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + match cached_hint.label { + project::InlayHintLabel::String(_) => { + if let Some(tooltip) = cached_hint.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: hint_start_offset, + highlight_end: hint_end_offset, + }, + }, + cx, + ); + hover_updated = true; + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some((hovered_hint_part, part_range)) = + hover_popover::find_hovered_hint_part( + label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) + { + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + }, + cx, + ); + hover_updated = true; + } + if let Some(location) = hovered_hint_part.location { + if let Some(buffer) = + cached_hint.position.buffer_id.and_then(|buffer_id| { + editor.buffer().read(cx).buffer(buffer_id) + }) + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + LocationLink { + origin: Some(Location { + buffer, + range: cached_hint.position + ..cached_hint.position, + }), + target: location, + }, + ), + cmd_held, + shift_held, + cx, + ); + } + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + + if !go_to_definition_updated { + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::None, + cmd_held, + shift_held, + cx, + ); + } + if !hover_updated { + hover_popover::hover_at(editor, None, cx); + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum LinkDefinitionKind { Symbol, @@ -391,14 +582,21 @@ fn go_to_fetched_definition_of_kind( #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + display_map::ToDisplayPoint, + editor_tests::init_test, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + test::editor_lsp_test_context::EditorLspTestContext, + }; use futures::StreamExt; use gpui::{ platform::{self, Modifiers, ModifiersChangedEvent}, View, }; use indoc::indoc; + use language::language_settings::InlayHintSettings; use lsp::request::{GotoDefinition, GotoTypeDefinition}; + use util::assert_set_eq; #[gpui::test] async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { @@ -853,4 +1051,209 @@ mod tests { "}); cx.foreground().run_until_parked(); } + + #[gpui::test] + async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + cx.set_state(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "}); + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + fn main() { + let variable = TestStruct; + } + "}); + + let expected_uri = cx.buffer_lsp_url.clone(); + let inlay_label = ": TestStruct"; + cx.lsp + .handle_request::(move |params, _| { + let expected_uri = expected_uri.clone(); + async move { + assert_eq!(params.text_document.uri, expected_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: inlay_label.to_string(), + location: Some(lsp::Location { + uri: params.text_document.uri, + range: target_range, + }), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![inlay_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + fn main() { + let variable« »= TestStruct; + } + "}) + .get(0) + .cloned() + .unwrap(); + let hint_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (inlay_label.len() / 2) as u32, + } + }); + // Press cmd to trigger highlight + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let expected_highlight_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_ranges = vec![InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_highlight_start, + highlight_end: InlayOffset(expected_highlight_start.0 + inlay_label.len()), + }]; + assert_set_eq!(actual_ranges, expected_ranges); + }); + + // Unpress cmd causes highlight to go away + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: false, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + }); + // Assert no link highlights + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}"); + }); + + // Cmd+click without existing definition requests and jumps + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: true, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + go_to_fetched_type_definition(editor, hint_hover_position, false, cx); + }); + cx.foreground().run_until_parked(); + cx.assert_editor_state(indoc! {" + struct «TestStructˇ»; + + fn main() { + let variable = TestStruct; + } + "}); + } } From e44516cc6c8197f401934394d939d89fd414a634 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:26:04 +0300 Subject: [PATCH 26/67] Add hover tests --- crates/editor/src/hover_popover.rs | 318 ++++++++++++++++++++- crates/editor/src/link_go_to_definition.rs | 12 +- crates/project/src/lsp_command.rs | 30 +- crates/project/src/project.rs | 8 +- 4 files changed, 344 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6eae470badc812f7d4889e6ff705244b46ae2300..19020b643ace141a38812cb95c1a462cbf1feadb 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -804,10 +804,17 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + editor_tests::init_test, + element::PointForPosition, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + link_go_to_definition::update_inlay_link_and_hover_points, + test::editor_lsp_test_context::EditorLspTestContext, + }; + use collections::BTreeSet; use gpui::fonts::Weight; use indoc::indoc; - use language::{Diagnostic, DiagnosticSet}; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; @@ -1243,4 +1250,311 @@ mod tests { editor }); } + + #[gpui::test] + async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Right( + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { + resolve_provider: Some(true), + ..Default::default() + }), + )), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let new_type_target_range = cx.lsp_range(indoc! {" + struct TestStruct; + + // ================== + + struct «TestNewType»(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + + let uri = cx.buffer_lsp_url.clone(); + let new_type_label = "TestNewType"; + let struct_label = "TestStruct"; + let entire_hint_label = ": TestNewType"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(move |params, _| { + let task_uri = closure_uri.clone(); + async move { + assert_eq!(params.text_document.uri, task_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: entire_hint_label.to_string(), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![entire_hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable« »= TestNewType(TestStruct); + } + "}) + .get(0) + .cloned() + .unwrap(); + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (entire_hint_label.find(new_type_label).unwrap() + + new_type_label.len() / 2) + as u32, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + + let resolve_closure_uri = uri.clone(); + cx.lsp + .handle_request::( + move |mut hint_to_resolve, _| { + let mut resolved_hint_positions = BTreeSet::new(); + let task_uri = resolve_closure_uri.clone(); + async move { + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + + // `: TestNewType` + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ + lsp::InlayHintLabelPart { + value: ": ".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: new_type_label.to_string(), + location: Some(lsp::Location { + uri: task_uri.clone(), + range: new_type_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( + "A tooltip for `{new_type_label}`" + ))), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: "<".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: struct_label.to_string(), + location: Some(lsp::Location { + uri: task_uri, + range: struct_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: format!("A tooltip for `{struct_label}`"), + }, + )), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: ">".to_string(), + ..Default::default() + }, + ]); + + Ok(hint_to_resolve) + } + }, + ) + .next() + .await; + cx.foreground().run_until_parked(); + + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + + let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_new_type_label_start, + highlight_end: InlayOffset( + expected_new_type_label_start.0 + new_type_label.len() + ), + }), + "Popover range should match the new type label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for new type hint") + .text, + format!("A tooltip for `{new_type_label}`"), + "Rendered text should not anyhow alter backticks" + ); + }); + + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (entire_hint_label.find(struct_label).unwrap() + + struct_label.len() / 2) + as u32, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + struct_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_struct_label_start = + InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_struct_label_start, + highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), + }), + "Popover range should match the struct label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for struct hint") + .text, + format!("A tooltip for {struct_label}"), + "Rendered markdown element should remove backticks from text" + ); + }); + } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ea22ea5eae15ee686067d4f1119cb6edd0a5ac0a..926c0d6ddeb588bf133d032e9492c2249e9711eb 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -41,7 +41,7 @@ pub enum TriggerPoint { InlayHint(InlayRange, LocationLink), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DocumentRange { Text(Range), Inlay(InlayRange), @@ -1096,7 +1096,7 @@ mod tests { "}); let expected_uri = cx.buffer_lsp_url.clone(); - let inlay_label = ": TestStruct"; + let hint_label = ": TestStruct"; cx.lsp .handle_request::(move |params, _| { let expected_uri = expected_uri.clone(); @@ -1105,7 +1105,7 @@ mod tests { Ok(Some(vec![lsp::InlayHint { position: hint_position, label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { - value: inlay_label.to_string(), + value: hint_label.to_string(), location: Some(lsp::Location { uri: params.text_document.uri, range: target_range, @@ -1125,7 +1125,7 @@ mod tests { .await; cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let expected_layers = vec![inlay_label.to_string()]; + let expected_layers = vec![hint_label.to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor)); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); }); @@ -1147,7 +1147,7 @@ mod tests { previous_valid: inlay_range.start.to_display_point(&snapshot), next_valid: inlay_range.end.to_display_point(&snapshot), exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (inlay_label.len() / 2) as u32, + column_overshoot_after_line_end: (hint_label.len() / 2) as u32, } }); // Press cmd to trigger highlight @@ -1185,7 +1185,7 @@ mod tests { let expected_ranges = vec![InlayRange { inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), highlight_start: expected_highlight_start, - highlight_end: InlayOffset(expected_highlight_start.0 + inlay_label.len()), + highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()), }]; assert_set_eq!(actual_ranges, expected_ranges); }); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 9f7799c555940607a78b4faedd63bf89b16ddada..292f9a5226dfb4897c4faa94ab35e6108d4ec136 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -17,7 +17,7 @@ use language::{ CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, }; -use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; +use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { @@ -2213,6 +2213,22 @@ impl InlayHints { }, } } + + pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool { + capabilities + .inlay_hint_provider + .as_ref() + .and_then(|options| match options { + OneOf::Left(_is_supported) => None, + OneOf::Right(capabilities) => match capabilities { + lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider, + lsp::InlayHintServerCapabilities::RegistrationOptions(o) => { + o.inlay_hint_options.resolve_provider + } + }, + }) + .unwrap_or(false) + } } #[async_trait(?Send)] @@ -2269,14 +2285,10 @@ impl LspCommand for InlayHints { lsp_adapter.name.0.as_ref() == "typescript-language-server"; let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { - let resolve_state = match lsp_server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( - lsp::InlayHintOptions { - resolve_provider: Some(true), - .. - }, - ))) => ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()), - _ => ResolveState::Resolved, + let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } else { + ResolveState::Resolved }; let project = project.clone(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0bbb61dfcb44065b105ac331a414f54ae12ed17d..c7765bf55a70b4829cf43575b7679db94ff44065 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5043,13 +5043,7 @@ impl Project { } else { return Task::ready(Ok(hint)); }; - let can_resolve = lang_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false); - if !can_resolve { + if !InlayHints::can_resolve_inlays(lang_server.capabilities()) { return Task::ready(Ok(hint)); } From 8ed280a029c397b572b961db7aa892e1ed726562 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:30:07 +0300 Subject: [PATCH 27/67] Rebase fixes --- Cargo.lock | 2 +- crates/rpc/proto/zed.proto | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d669be5d9d2e7e154c5549c5d7eecfe99785e993..8197f883c0615e0e1293d16ec832c31e3c842031 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,7 +1220,7 @@ dependencies = [ "tempfile", "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index dbd700e264af7c1c3f89b014bbf9ad9476fd5536..ce47830af22e8203f33aaa86f3f953a86065ad94 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -128,8 +128,8 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; - ResolveInlayHint resolve_inlay_hint = 131; - ResolveInlayHintResponse resolve_inlay_hint_response = 132; + ResolveInlayHint resolve_inlay_hint = 137; + ResolveInlayHintResponse resolve_inlay_hint_response = 138; RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; From 0a18aa694f13f8cb0f5b3c015f317cb6c03d5dfe Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:46:39 +0300 Subject: [PATCH 28/67] Use stricter inlay range checks to avoid stuck highlights Often, hint ranges are separated by a single '<` char as in `Option>`. When moving the caret from left to right, avoid inclusive ranges to faster update the matching hint underline. --- crates/editor/src/hover_popover.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 19020b643ace141a38812cb95c1a462cbf1feadb..3ce936ae8275b03e4f75abe2cd39ae11f7938214 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -88,7 +88,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { if let DocumentRange::Inlay(range) = symbol_range { - if (range.highlight_start..=range.highlight_end) + if (range.highlight_start..range.highlight_end) .contains(&inlay_hover.triggered_from) { // Hover triggered from same location as last time. Don't show again. From a63e1571dc0d143e3facf7fd61456a31d4250ce5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 01:10:01 +0300 Subject: [PATCH 29/67] Defer querying inlay hints for invisible editor ranges This way, only the visible part gets frequently queried on typing (and hint /refresh requests that follow), with queries for invisible ranges cancelled eagerly. --- crates/editor/src/inlay_hint_cache.rs | 312 +++++++++++++++++--------- 1 file changed, 203 insertions(+), 109 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b0c7d9e0f1a7c5bd64758126b199425f59b77151..b7f04c68b1d3c9216f0533943b37b980ada366cc 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2,6 +2,7 @@ use std::{ cmp, ops::{ControlFlow, Range}, sync::Arc, + time::Duration, }; use crate::{ @@ -9,6 +10,7 @@ use crate::{ }; use anyhow::Context; use clock::Global; +use futures::future; use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; @@ -17,7 +19,7 @@ use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; -use sum_tree::Bias; +use text::ToOffset; use util::post_inc; pub struct InlayHintCache { @@ -81,7 +83,11 @@ impl InvalidationStrategy { } impl TasksForRanges { - fn new(sorted_ranges: Vec>, task: Task<()>) -> Self { + fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { + let mut sorted_ranges = Vec::new(); + sorted_ranges.extend(query_ranges.before_visible); + sorted_ranges.extend(query_ranges.visible); + sorted_ranges.extend(query_ranges.after_visible); Self { tasks: vec![task], sorted_ranges, @@ -91,82 +97,103 @@ impl TasksForRanges { fn update_cached_tasks( &mut self, buffer_snapshot: &BufferSnapshot, - query_range: Range, + query_ranges: QueryRanges, invalidate: InvalidationStrategy, - spawn_task: impl FnOnce(Vec>) -> Task<()>, + spawn_task: impl FnOnce(QueryRanges) -> Task<()>, ) { - let ranges_to_query = match invalidate { + let query_ranges = match invalidate { InvalidationStrategy::None => { - let mut ranges_to_query = Vec::new(); - let mut latest_cached_range = None::<&mut Range>; - for cached_range in self - .sorted_ranges - .iter_mut() - .skip_while(|cached_range| { - cached_range - .end - .cmp(&query_range.start, buffer_snapshot) - .is_lt() - }) - .take_while(|cached_range| { - cached_range - .start - .cmp(&query_range.end, buffer_snapshot) - .is_le() - }) - { - match latest_cached_range { - Some(latest_cached_range) => { - if latest_cached_range.end.offset.saturating_add(1) - < cached_range.start.offset - { - ranges_to_query.push(latest_cached_range.end..cached_range.start); - cached_range.start = latest_cached_range.end; - } - } - None => { - if query_range - .start - .cmp(&cached_range.start, buffer_snapshot) - .is_lt() - { - ranges_to_query.push(query_range.start..cached_range.start); - cached_range.start = query_range.start; - } - } - } - latest_cached_range = Some(cached_range); - } - - match latest_cached_range { - Some(latest_cached_range) => { - if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset - { - ranges_to_query.push(latest_cached_range.end..query_range.end); - latest_cached_range.end = query_range.end; - } - } - None => { - ranges_to_query.push(query_range.clone()); - self.sorted_ranges.push(query_range); - self.sorted_ranges.sort_by(|range_a, range_b| { - range_a.start.cmp(&range_b.start, buffer_snapshot) - }); - } - } - - ranges_to_query + let mut updated_ranges = query_ranges; + updated_ranges.before_visible = updated_ranges + .before_visible + .into_iter() + .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .collect(); + updated_ranges.visible = updated_ranges + .visible + .into_iter() + .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .collect(); + updated_ranges.after_visible = updated_ranges + .after_visible + .into_iter() + .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .collect(); + updated_ranges } InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { self.tasks.clear(); self.sorted_ranges.clear(); - vec![query_range] + query_ranges } }; - if !ranges_to_query.is_empty() { - self.tasks.push(spawn_task(ranges_to_query)); + if !query_ranges.is_empty() { + self.tasks.push(spawn_task(query_ranges)); + } + } + + fn remove_cached_ranges( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_range: Range, + ) -> Vec> { + let mut ranges_to_query = Vec::new(); + let mut latest_cached_range = None::<&mut Range>; + for cached_range in self + .sorted_ranges + .iter_mut() + .skip_while(|cached_range| { + cached_range + .end + .cmp(&query_range.start, buffer_snapshot) + .is_lt() + }) + .take_while(|cached_range| { + cached_range + .start + .cmp(&query_range.end, buffer_snapshot) + .is_le() + }) + { + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset + { + ranges_to_query.push(latest_cached_range.end..cached_range.start); + cached_range.start = latest_cached_range.end; + } + } + None => { + if query_range + .start + .cmp(&cached_range.start, buffer_snapshot) + .is_lt() + { + ranges_to_query.push(query_range.start..cached_range.start); + cached_range.start = query_range.start; + } + } + } + latest_cached_range = Some(cached_range); + } + + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset { + ranges_to_query.push(latest_cached_range.end..query_range.end); + latest_cached_range.end = query_range.end; + } + } + None => { + ranges_to_query.push(query_range.clone()); + self.sorted_ranges.push(query_range); + self.sorted_ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); + } } + + ranges_to_query } } @@ -515,11 +542,11 @@ fn spawn_new_update_tasks( } }; - let (multi_buffer_snapshot, Some(query_range)) = + let (multi_buffer_snapshot, Some(query_ranges)) = editor.buffer.update(cx, |multi_buffer, cx| { ( multi_buffer.snapshot(cx), - determine_query_range( + determine_query_ranges( multi_buffer, excerpt_id, &excerpt_buffer, @@ -535,10 +562,10 @@ fn spawn_new_update_tasks( invalidate, }; - let new_update_task = |fetch_ranges| { + let new_update_task = |query_ranges| { new_update_task( query, - fetch_ranges, + query_ranges, multi_buffer_snapshot, buffer_snapshot.clone(), Arc::clone(&visible_hints), @@ -551,57 +578,100 @@ fn spawn_new_update_tasks( hash_map::Entry::Occupied(mut o) => { o.get_mut().update_cached_tasks( &buffer_snapshot, - query_range, + query_ranges, invalidate, new_update_task, ); } hash_map::Entry::Vacant(v) => { v.insert(TasksForRanges::new( - vec![query_range.clone()], - new_update_task(vec![query_range]), + query_ranges.clone(), + new_update_task(query_ranges), )); } } } } -fn determine_query_range( +#[derive(Debug, Clone)] +struct QueryRanges { + before_visible: Vec>, + visible: Vec>, + after_visible: Vec>, +} + +impl QueryRanges { + fn is_empty(&self) -> bool { + self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty() + } +} + +fn determine_query_ranges( multi_buffer: &mut MultiBuffer, excerpt_id: ExcerptId, excerpt_buffer: &ModelHandle, excerpt_visible_range: Range, cx: &mut ModelContext<'_, MultiBuffer>, -) -> Option> { +) -> Option { let full_excerpt_range = multi_buffer .excerpts_for_buffer(excerpt_buffer, cx) .into_iter() .find(|(id, _)| id == &excerpt_id) .map(|(_, range)| range.context)?; - let buffer = excerpt_buffer.read(cx); + let snapshot = buffer.snapshot(); let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; - let start_offset = excerpt_visible_range - .start - .saturating_sub(excerpt_visible_len) - .max(full_excerpt_range.start.offset); - let start = buffer.anchor_before(buffer.clip_offset(start_offset, Bias::Left)); - let end_offset = excerpt_visible_range - .end - .saturating_add(excerpt_visible_len) - .min(full_excerpt_range.end.offset) - .min(buffer.len()); - let end = buffer.anchor_after(buffer.clip_offset(end_offset, Bias::Right)); - if start.cmp(&end, buffer).is_eq() { - None + + let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end { + return None; } else { - Some(start..end) - } + vec![ + buffer.anchor_before(excerpt_visible_range.start) + ..buffer.anchor_after(excerpt_visible_range.end), + ] + }; + + let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); + let after_visible_range = if excerpt_visible_range.end == full_excerpt_range_end_offset { + Vec::new() + } else { + let after_range_end_offset = excerpt_visible_range + .end + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range_end_offset) + .min(buffer.len()); + vec![ + buffer.anchor_before(excerpt_visible_range.end) + ..buffer.anchor_after(after_range_end_offset), + ] + }; + + let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); + let before_visible_range = if excerpt_visible_range.start == full_excerpt_range_start_offset { + Vec::new() + } else { + let before_range_start_offset = excerpt_visible_range + .start + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range_start_offset); + vec![ + buffer.anchor_before(before_range_start_offset) + ..buffer.anchor_after(excerpt_visible_range.start), + ] + }; + + Some(QueryRanges { + before_visible: before_visible_range, + visible: visible_range, + after_visible: after_visible_range, + }) } +const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 300; + fn new_update_task( query: ExcerptQuery, - hint_fetch_ranges: Vec>, + query_ranges: QueryRanges, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, @@ -609,24 +679,48 @@ fn new_update_task( cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { cx.spawn(|editor, cx| async move { - let task_update_results = - futures::future::join_all(hint_fetch_ranges.into_iter().map(|range| { - fetch_and_update_hints( - editor.clone(), - multi_buffer_snapshot.clone(), - buffer_snapshot.clone(), - Arc::clone(&visible_hints), - cached_excerpt_hints.as_ref().map(Arc::clone), - query, - range, - cx.clone(), - ) - })) + let fetch_and_update_hints = |range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + range, + cx.clone(), + ) + }; + let visible_range_update_results = future::join_all( + query_ranges + .visible + .into_iter() + .map(|visible_range| fetch_and_update_hints(visible_range)), + ) + .await; + for result in visible_range_update_results { + if let Err(e) = result { + error!("visible range inlay hint update task failed: {e:#}"); + } + } + + cx.background() + .timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )) .await; - for result in task_update_results { + let invisible_range_update_results = future::join_all( + query_ranges + .before_visible + .into_iter() + .chain(query_ranges.after_visible.into_iter()) + .map(|invisible_range| fetch_and_update_hints(invisible_range)), + ) + .await; + for result in invisible_range_update_results { if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); + error!("invisible range inlay hint update task failed: {e:#}"); } } }) @@ -1816,7 +1910,7 @@ pub mod tests { }); })); } - let _ = futures::future::join_all(edits).await; + let _ = future::join_all(edits).await; cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { From c10c3e2b544895b166d7d2f34f34f095a839f498 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 15:09:06 +0300 Subject: [PATCH 30/67] Only invalidate when doing first, visible range query --- crates/editor/src/inlay_hint_cache.rs | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b7f04c68b1d3c9216f0533943b37b980ada366cc..edd20bed3000f476b6be0c69923e3ba5b1a9e803 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -679,7 +679,7 @@ fn new_update_task( cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { cx.spawn(|editor, cx| async move { - let fetch_and_update_hints = |range| { + let fetch_and_update_hints = |invalidate, range| { fetch_and_update_hints( editor.clone(), multi_buffer_snapshot.clone(), @@ -687,17 +687,16 @@ fn new_update_task( Arc::clone(&visible_hints), cached_excerpt_hints.as_ref().map(Arc::clone), query, + invalidate, range, cx.clone(), ) }; - let visible_range_update_results = future::join_all( - query_ranges - .visible - .into_iter() - .map(|visible_range| fetch_and_update_hints(visible_range)), - ) - .await; + let visible_range_update_results = + future::join_all(query_ranges.visible.into_iter().map(|visible_range| { + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + })) + .await; for result in visible_range_update_results { if let Err(e) = result { error!("visible range inlay hint update task failed: {e:#}"); @@ -715,7 +714,7 @@ fn new_update_task( .before_visible .into_iter() .chain(query_ranges.after_visible.into_iter()) - .map(|invisible_range| fetch_and_update_hints(invisible_range)), + .map(|invisible_range| fetch_and_update_hints(false, invisible_range)), ) .await; for result in invisible_range_update_results { @@ -733,6 +732,7 @@ async fn fetch_and_update_hints( visible_hints: Arc>, cached_excerpt_hints: Option>>, query: ExcerptQuery, + invalidate: bool, fetch_range: Range, mut cx: gpui::AsyncAppContext, ) -> anyhow::Result<()> { @@ -761,7 +761,8 @@ async fn fetch_and_update_hints( .background() .spawn(async move { calculate_hint_updates( - query, + query.excerpt_id, + invalidate, backround_fetch_range, new_hints, &background_task_buffer_snapshot, @@ -788,7 +789,8 @@ async fn fetch_and_update_hints( } fn calculate_hint_updates( - query: ExcerptQuery, + excerpt_id: ExcerptId, + invalidate: bool, fetch_range: Range, new_excerpt_hints: Vec, buffer_snapshot: &BufferSnapshot, @@ -836,11 +838,11 @@ fn calculate_hint_updates( let mut remove_from_visible = Vec::new(); let mut remove_from_cache = HashSet::default(); - if query.invalidate.should_invalidate() { + if invalidate { remove_from_visible.extend( visible_hints .iter() - .filter(|hint| hint.position.excerpt_id == query.excerpt_id) + .filter(|hint| hint.position.excerpt_id == excerpt_id) .map(|inlay_hint| inlay_hint.id) .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), ); @@ -863,7 +865,7 @@ fn calculate_hint_updates( None } else { Some(ExcerptHintsUpdate { - excerpt_id: query.excerpt_id, + excerpt_id, remove_from_visible, remove_from_cache, add_to_cache, From 2b95f0580e340f54c129d0d4f88e6480298e5fb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 16:17:45 +0300 Subject: [PATCH 31/67] Fix the tests --- crates/editor/src/inlay_hint_cache.rs | 267 +++++++++++++++----------- 1 file changed, 158 insertions(+), 109 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index edd20bed3000f476b6be0c69923e3ba5b1a9e803..54ed8f8f1182610213b91c51d909adcd0a8f1df8 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -632,31 +632,38 @@ fn determine_query_ranges( }; let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); - let after_visible_range = if excerpt_visible_range.end == full_excerpt_range_end_offset { + let after_visible_range_start = excerpt_visible_range + .end + .saturating_add(1) + .min(full_excerpt_range_end_offset) + .min(buffer.len()); + let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset { Vec::new() } else { - let after_range_end_offset = excerpt_visible_range - .end + let after_range_end_offset = after_visible_range_start .saturating_add(excerpt_visible_len) .min(full_excerpt_range_end_offset) .min(buffer.len()); vec![ - buffer.anchor_before(excerpt_visible_range.end) + buffer.anchor_before(after_visible_range_start) ..buffer.anchor_after(after_range_end_offset), ] }; let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); - let before_visible_range = if excerpt_visible_range.start == full_excerpt_range_start_offset { + let before_visible_range_end = excerpt_visible_range + .start + .saturating_sub(1) + .max(full_excerpt_range_start_offset); + let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset { Vec::new() } else { - let before_range_start_offset = excerpt_visible_range - .start + let before_range_start_offset = before_visible_range_end .saturating_sub(excerpt_visible_len) .max(full_excerpt_range_start_offset); vec![ buffer.anchor_before(before_range_start_offset) - ..buffer.anchor_after(excerpt_visible_range.start), + ..buffer.anchor_after(before_visible_range_end), ] }; @@ -667,7 +674,7 @@ fn determine_query_ranges( }) } -const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 300; +const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; fn new_update_task( query: ExcerptQuery, @@ -1001,7 +1008,7 @@ fn apply_hint_update( #[cfg(test)] pub mod tests { - use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use crate::{ scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, @@ -1079,13 +1086,13 @@ pub mod tests { let mut edits_made = 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.allowed_hint_kinds, allowed_hint_kinds, @@ -1104,13 +1111,13 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string(), "1".to_string()]; + let expected_hints = vec!["0".to_string(), "1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get new hints after an edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.allowed_hint_kinds, allowed_hint_kinds, @@ -1129,13 +1136,13 @@ pub mod tests { edits_made += 1; cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get new hints after hint refresh/ request" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.allowed_hint_kinds, allowed_hint_kinds, @@ -1189,13 +1196,13 @@ pub mod tests { let mut edits_made = 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1220,13 +1227,13 @@ pub mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should not update hints while the work task is running" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1244,13 +1251,13 @@ pub mod tests { edits_made += 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "New hints should be queried after the work task is done" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1363,13 +1370,13 @@ pub mod tests { .await; cx.foreground().run_until_parked(); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, 1, @@ -1421,13 +1428,13 @@ pub mod tests { .await; cx.foreground().run_until_parked(); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should have a separate verison, repeating Rust editor rules" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); @@ -1437,13 +1444,13 @@ pub mod tests { }); cx.foreground().run_until_parked(); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Rust inlay cache should change after the edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, 2, @@ -1451,13 +1458,13 @@ pub mod tests { ); }); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should not be affected by Rust editor changes" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); @@ -1467,23 +1474,23 @@ pub mod tests { }); cx.foreground().run_until_parked(); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Rust editor should not be affected by Markdown editor changes" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 2); }); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should also change independently" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 2); }); } @@ -2009,7 +2016,7 @@ pub mod tests { .downcast::() .unwrap(); let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); - let lsp_request_count = Arc::new(AtomicU32::new(0)); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); let closure_lsp_request_count = Arc::clone(&lsp_request_count); fake_server @@ -2023,10 +2030,9 @@ pub mod tests { ); task_lsp_request_ranges.lock().push(params.range); - let query_start = params.range.start; let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; Ok(Some(vec![lsp::InlayHint { - position: query_start, + position: params.range.end, label: lsp::InlayHintLabel::String(i.to_string()), kind: None, text_edits: None, @@ -2063,28 +2069,51 @@ pub mod tests { }) } + // in large buffers, requests are made for more than visible range of a buffer. + // invisible parts are queried later, to avoid excessive requests on quick typing. + // wait the timeout needed to get all requests. + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); let initial_visible_range = editor_visible_range(&editor, cx); + let lsp_initial_visible_range = lsp::Range::new( + lsp::Position::new( + initial_visible_range.start.row, + initial_visible_range.start.column, + ), + lsp::Position::new( + initial_visible_range.end.row, + initial_visible_range.end.column, + ), + ); let expected_initial_query_range_end = - lsp::Position::new(initial_visible_range.end.row * 2, 1); - cx.foreground().run_until_parked(); + lsp::Position::new(initial_visible_range.end.row * 2, 2); + let mut expected_invisible_query_start = lsp_initial_visible_range.end; + expected_invisible_query_start.character += 1; editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - assert_eq!(ranges.len(), 1, - "When scroll is at the edge of a big document, double of its visible part range should be queried for hints in one single big request, but got: {ranges:?}"); - let query_range = &ranges[0]; - assert_eq!(query_range.start, lsp::Position::new(0, 0), "Should query initially from the beginning of the document"); - assert_eq!(query_range.end, expected_initial_query_range_end, "Should query initially for double lines of the visible part of the document"); + assert_eq!(ranges.len(), 2, + "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); + let visible_query_range = &ranges[0]; + assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); + assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); + let invisible_query_range = &ranges[1]; - assert_eq!(lsp_request_count.load(Ordering::Acquire), 1); - let expected_layers = vec!["1".to_string()]; + assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); + assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); + + let requests_count = lsp_request_count.load(Ordering::Acquire); + assert_eq!(requests_count, 2, "Visible + invisible request"); + let expected_hints = vec!["1".to_string(), "2".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should have hints from both LSP requests made for a big file" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); assert_eq!( - editor.inlay_hint_cache().version, 1, + editor.inlay_hint_cache().version, requests_count, "LSP queries should've bumped the cache version" ); }); @@ -2093,11 +2122,13 @@ pub mod tests { editor.scroll_screen(&ScrollAmount::Page(1.0), cx); editor.scroll_screen(&ScrollAmount::Page(1.0), cx); }); - + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); let visible_range_after_scrolls = editor_visible_range(&editor, cx); let visible_line_count = editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); - cx.foreground().run_until_parked(); let selection_in_cached_range = editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges .lock() @@ -2124,26 +2155,28 @@ pub mod tests { lsp::Position::new( visible_range_after_scrolls.end.row + visible_line_count.ceil() as u32, - 0 + 1, ), "Second scroll should query one more screen down after the end of the visible range" ); + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); + let expected_hints = vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + ]; assert_eq!( - lsp_request_count.load(Ordering::Acquire), - 3, - "Should query for hints after every scroll" - ); - let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; - assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should have hints from the new LSP response after the edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, - 3, + lsp_requests, "Should update the cache for every LSP response with hints added" ); @@ -2157,6 +2190,9 @@ pub mod tests { s.select_ranges([selection_in_cached_range..selection_in_cached_range]) }); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); editor.update(cx, |_, _| { let ranges = lsp_request_ranges @@ -2165,33 +2201,43 @@ pub mod tests { .sorted_by_key(|r| r.start) .collect::>(); assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 3); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); }); editor.update(cx, |editor, cx| { editor.handle_input("++++more text++++", cx); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - assert_eq!(ranges.len(), 1, - "On edit, should scroll to selection and query a range around it. Instead, got query ranges {ranges:?}"); - let query_range = &ranges[0]; - assert!(query_range.start.line < selection_in_cached_range.row, + assert_eq!(ranges.len(), 3, + "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); + let visible_query_range = &ranges[0]; + let above_query_range = &ranges[1]; + let below_query_range = &ranges[2]; + assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, + "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); + assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, + "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); + assert!(above_query_range.start.line < selection_in_cached_range.row, "Hints should be queried with the selected range after the query range start"); - assert!(query_range.end.line > selection_in_cached_range.row, + assert!(below_query_range.end.line > selection_in_cached_range.row, "Hints should be queried with the selected range before the query range end"); - assert!(query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, + assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, "Hints query range should contain one more screen before"); - assert!(query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, + assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, "Hints query range should contain one more screen after"); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 4, "Should query for hints once after the edit"); - let expected_layers = vec!["4".to_string()]; - assert_eq!(expected_layers, cached_hint_labels(editor), + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); + let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor), "Should have hints from the new LSP response after the edit"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 4, "Should update the cache for every LSP response with hints added"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); }); } @@ -2402,19 +2448,19 @@ pub mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), ]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); }); editor.update(cx, |editor, cx| { @@ -2430,7 +2476,7 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2441,10 +2487,10 @@ pub mod tests { "other hint #1".to_string(), "other hint #2".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); }); @@ -2453,9 +2499,12 @@ pub mod tests { s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) }); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); let last_scroll_update_version = editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2469,11 +2518,11 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len()); - expected_layers.len() + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() }); editor.update(cx, |editor, cx| { @@ -2483,7 +2532,7 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2497,9 +2546,9 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); }); @@ -2512,7 +2561,7 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint(edited) #0".to_string(), "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), @@ -2523,15 +2572,15 @@ pub mod tests { "other hint(edited) #1".to_string(), ]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "After multibuffer edit, editor gets scolled back to the last selection; \ all hints should be invalidated and requeried for all of its visible excerpts" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let current_cache_version = editor.inlay_hint_cache().version; - let minimum_expected_version = last_scroll_update_version + expected_layers.len(); + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); assert!( current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" @@ -2872,9 +2921,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; - assert_eq!(expected_layers, cached_hint_labels(editor)); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let expected_hints = vec!["1".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); } From 44c340b5f25dfd6bc2a27192e5f05a9e8c154101 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 17:33:17 +0300 Subject: [PATCH 32/67] Properly invalidate the hint cache --- crates/editor/src/inlay_hint_cache.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 54ed8f8f1182610213b91c51d909adcd0a8f1df8..b33b93a3487d36f9ffa939c0e4317664f299f9d3 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -785,6 +785,7 @@ async fn fetch_and_update_hints( editor, new_update, query, + invalidate, buffer_snapshot, multi_buffer_snapshot, cx, @@ -893,6 +894,7 @@ fn apply_hint_update( editor: &mut Editor, new_update: ExcerptHintsUpdate, query: ExcerptQuery, + invalidate: bool, buffer_snapshot: BufferSnapshot, multi_buffer_snapshot: MultiBufferSnapshot, cx: &mut ViewContext<'_, '_, Editor>, @@ -970,7 +972,7 @@ fn apply_hint_update( cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); drop(cached_excerpt_hints); - if query.invalidate.should_invalidate() { + if invalidate { let mut outdated_excerpt_caches = HashSet::default(); for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { let excerpt_hints = excerpt_hints.read(); From 732af201dc599ef58a0ce650658775c6060ee58e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 09:59:16 -0700 Subject: [PATCH 33/67] Upgrade to rust 1.72 --- Dockerfile | 2 +- rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 77d011490e5821f282240af7d387b19f67a0edbe..208700f7fb5f25d19dc5e5cfd1477f11219c4391 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.71-bullseye as builder +FROM rust:1.72-bullseye as builder WORKDIR app COPY . . diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 50003020e9baf17e3e9e0b50babb19c354356e15..7ed8b98280ca394966621a55b80d3101ac6854c8 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.71" +channel = "1.72" components = [ "rustfmt" ] targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] From 404f76739c3e032b83b0f69f13441f3b3a4025a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 10:11:32 -0700 Subject: [PATCH 34/67] Format let-else statements --- crates/ai/src/assistant.rs | 13 +++- crates/call/src/room.rs | 4 +- crates/collab/src/db.rs | 4 +- crates/collab/src/db/queries/rooms.rs | 6 +- .../src/tests/randomized_integration_tests.rs | 35 +++++++---- crates/collab_ui/src/channel_view.rs | 8 ++- .../src/collab_panel/channel_modal.rs | 9 +-- crates/editor/src/editor.rs | 60 ++++++++++--------- crates/editor/src/inlay_hint_cache.rs | 10 +++- crates/editor/src/items.rs | 20 +++++-- crates/editor/src/multi_buffer.rs | 4 +- .../gpui/src/keymap_matcher/keymap_context.rs | 4 +- crates/language/src/buffer.rs | 4 +- crates/language/src/syntax_map.rs | 12 +++- .../src/syntax_map/syntax_map_tests.rs | 8 ++- crates/language_tools/src/lsp_log.rs | 4 +- crates/project/src/lsp_command.rs | 14 ++++- crates/project/src/worktree.rs | 12 ++-- .../quick_action_bar/src/quick_action_bar.rs | 4 +- crates/search/src/project_search.rs | 4 +- crates/semantic_index/src/semantic_index.rs | 4 +- crates/settings/src/keymap_file.rs | 19 +++--- crates/vcs_menu/src/lib.rs | 32 +++++----- crates/vim/src/normal/paste.rs | 2 +- crates/vim/src/visual.rs | 7 ++- crates/workspace/src/pane_group.rs | 4 +- crates/workspace/src/workspace.rs | 8 ++- crates/zed/src/languages/python.rs | 4 +- 28 files changed, 210 insertions(+), 109 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 81299bbdc26f5001f407901893b7c8d3e0f1b166..2699bf40a088b38033af9244c4365d2c57d43dbf 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1128,7 +1128,9 @@ impl Conversation { stream: true, }; - let Some(api_key) = self.api_key.borrow().clone() else { continue }; + let Some(api_key) = self.api_key.borrow().clone() else { + continue; + }; let stream = stream_completion(api_key, cx.background().clone(), request); let assistant_message = self .insert_message_after( @@ -1484,7 +1486,9 @@ impl Conversation { }) { current_message = messages.next(); } - let Some(message) = current_message.as_ref() else { break }; + let Some(message) = current_message.as_ref() else { + break; + }; // Skip offsets that are in the same message. while offsets.peek().map_or(false, |offset| { @@ -1921,7 +1925,10 @@ impl ConversationEditor { let Some(panel) = workspace.panel::(cx) else { return; }; - let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::(cx)) else { + let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + else { return; }; diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 6f01b1d75789ce61d537be2d780f0dbb5960ad17..cc7445dbcc74ff620968c9ff5a2a99686bd800d9 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -644,7 +644,9 @@ impl Room { if let Some(participants) = remote_participants.log_err() { for (participant, user) in room.participants.into_iter().zip(participants) { - let Some(peer_id) = participant.peer_id else { continue }; + let Some(peer_id) = participant.peer_id else { + continue; + }; this.participant_user_ids.insert(participant.user_id); let old_projects = this diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 9c759f79a8dd47f4cc950809c7816f0204273372..888158188f35d82443e6a88b2237793d2dcdc016 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -249,7 +249,9 @@ impl Database { let mut tx = Arc::new(Some(tx)); let result = f(TransactionHandle(tx.clone())).await; let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else { - return Err(anyhow!("couldn't complete transaction because it's still in use"))?; + return Err(anyhow!( + "couldn't complete transaction because it's still in use" + ))?; }; Ok((tx, result)) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index a85d257187c2207b56b934540ba34c566eb1c77d..435e729fed38c2b5fcd4775e0de92ced74ee0b13 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -465,9 +465,9 @@ impl Database { let mut rejoined_projects = Vec::new(); for rejoined_project in &rejoin_room.rejoined_projects { let project_id = ProjectId::from_proto(rejoined_project.id); - let Some(project) = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? else { continue }; + let Some(project) = project::Entity::find_by_id(project_id).one(&*tx).await? else { + continue; + }; let mut worktrees = Vec::new(); let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 18fe6734cdda0dcb5194e518f06caf589751080e..3557843828cacb81b69b88d24514da6936b83139 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -121,7 +121,9 @@ async fn test_random_collaboration( let mut operation_channels = Vec::new(); loop { - let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { break }; + let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { + break; + }; applied.store(true, SeqCst); let did_apply = apply_server_operation( deterministic.clone(), @@ -224,7 +226,9 @@ async fn apply_server_operation( let client_ix = clients .iter() .position(|(client, cx)| client.current_user_id(cx) == removed_user_id); - let Some(client_ix) = client_ix else { return false }; + let Some(client_ix) = client_ix else { + return false; + }; let user_connection_ids = server .connection_pool .lock() @@ -1591,10 +1595,11 @@ impl TestPlan { 81.. => match self.rng.gen_range(0..100_u32) { // Add a worktree to a local project 0..=50 => { - let Some(project) = client - .local_projects() - .choose(&mut self.rng) - .cloned() else { continue }; + let Some(project) = + client.local_projects().choose(&mut self.rng).cloned() + else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let mut paths = client.fs().paths(false); paths.remove(0); @@ -1611,7 +1616,9 @@ impl TestPlan { // Add an entry to a worktree _ => { - let Some(project) = choose_random_project(client, &mut self.rng) else { continue }; + let Some(project) = choose_random_project(client, &mut self.rng) else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let is_local = project.read_with(cx, |project, _| project.is_local()); let worktree = project.read_with(cx, |project, cx| { @@ -1645,7 +1652,9 @@ impl TestPlan { // Query and mutate buffers 60..=90 => { - let Some(project) = choose_random_project(client, &mut self.rng) else { continue }; + let Some(project) = choose_random_project(client, &mut self.rng) else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let is_local = project.read_with(cx, |project, _| project.is_local()); @@ -1656,7 +1665,10 @@ impl TestPlan { .buffers_for_project(&project) .iter() .choose(&mut self.rng) - .cloned() else { continue }; + .cloned() + else { + continue; + }; let full_path = buffer .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); @@ -2026,7 +2038,10 @@ async fn simulate_client( client.app_state.languages.add(Arc::new(language)); while let Some(batch_id) = operation_rx.next().await { - let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break }; + let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) + else { + break; + }; applied.store(true, SeqCst); match apply_client_operation(&client, operation, &mut cx).await { Ok(()) => {} diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index bb1e840ffca40087519d324db5a2ae9a62a38222..a34f10b2db29f3f132e823fce27209d0a24a12c6 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -272,8 +272,12 @@ impl FollowableItem for ChannelView { state: &mut Option, cx: &mut AppContext, ) -> Option>>> { - let Some(proto::view::Variant::ChannelView(_)) = state else { return None }; - let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() }; + let Some(proto::view::Variant::ChannelView(_)) = state else { + return None; + }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { + unreachable!() + }; let open = ChannelView::open(state.channel_id, pane, workspace, cx); diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 0adf2806d72dc5c440ee08ec80a1331cdccc62cc..4c811a2df547dc78e0a602ae2002a4e9dbeb4e46 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -152,12 +152,9 @@ impl View for ChannelModal { let theme = &theme::current(cx).collab_panel.tabbed_modal; let mode = self.picker.read(cx).delegate().mode; - let Some(channel) = self - .channel_store - .read(cx) - .channel_for_id(self.channel_id) else { - return Empty::new().into_any() - }; + let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else { + return Empty::new().into_any(); + }; enum InviteMembers {} enum ManageMembers {} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 681e1d48b277a60ef0bd4d53cfc9726caed9fb4a..c5ff1f027da7faac89bbcbf596d501fffbeae2cb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6243,7 +6243,9 @@ impl Editor { ) { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_offsets_with(|snapshot, selection| { - let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else { + let Some(enclosing_bracket_ranges) = + snapshot.enclosing_bracket_ranges(selection.start..selection.end) + else { return; }; @@ -6255,7 +6257,8 @@ impl Editor { let close = close.to_inclusive(); let length = close.end() - open.start; let inside = selection.start >= open.end && selection.end <= *close.start(); - let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head()); + let in_bracket_range = open.to_inclusive().contains(&selection.head()) + || close.contains(&selection.head()); // If best is next to a bracket and current isn't, skip if !in_bracket_range && best_in_bracket_range { @@ -6270,19 +6273,21 @@ impl Editor { best_length = length; best_inside = inside; best_in_bracket_range = in_bracket_range; - best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) { - if inside { - open.end - } else { - open.start - } - } else { - if inside { - *close.start() + best_destination = Some( + if close.contains(&selection.start) && close.contains(&selection.end) { + if inside { + open.end + } else { + open.start + } } else { - *close.end() - } - }); + if inside { + *close.start() + } else { + *close.end() + } + }, + ); } if let Some(destination) = best_destination { @@ -6526,7 +6531,9 @@ impl Editor { split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { return }; + let Some(workspace) = self.workspace(cx) else { + return; + }; let buffer = self.buffer.read(cx); let head = self.selections.newest::(cx).head(); let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { @@ -6557,7 +6564,9 @@ impl Editor { split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { return }; + let Some(workspace) = self.workspace(cx) else { + return; + }; let pane = workspace.read(cx).active_pane().clone(); // If there is one definition, just open it directly if definitions.len() == 1 { @@ -7639,10 +7648,9 @@ impl Editor { let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let Some((_, ranges)) = self.background_highlights - .get(&TypeId::of::()) else { - return vec![]; - }; + let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { + return vec![]; + }; let start_ix = match ranges.binary_search_by(|probe| { let cmp = document_to_inlay_range(probe, display_snapshot) @@ -7993,9 +8001,7 @@ impl Editor { suggestion_accepted: bool, cx: &AppContext, ) { - let Some(project) = &self.project else { - return - }; + let Some(project) = &self.project else { return }; // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension let file_extension = self @@ -8024,9 +8030,7 @@ impl Editor { file_extension: Option, cx: &AppContext, ) { - let Some(project) = &self.project else { - return - }; + let Some(project) = &self.project else { return }; // If None, we are in a file without an extension let file = self @@ -8127,7 +8131,9 @@ impl Editor { } } - let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; }; + let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { + return; + }; cx.write_to_clipboard(ClipboardItem::new(lines)); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b33b93a3487d36f9ffa939c0e4317664f299f9d3..a4b91e826d06083a0979b6c4ffd45225d8fb41c2 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -341,7 +341,10 @@ impl InlayHintCache { shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { let Some(buffer) = shown_anchor .buffer_id - .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false }; + .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) + else { + return false; + }; let buffer_snapshot = buffer.read(cx).snapshot(); loop { match excerpt_cache.peek() { @@ -554,7 +557,10 @@ fn spawn_new_update_tasks( cx, ), ) - }) else { return; }; + }) + else { + return; + }; let query = ExcerptQuery { buffer_id, excerpt_id, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 30ed56af476547503d95afb563635ad9bddcbec6..d9998725922f5154e299bb3bd7a32b04ff18c2d2 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -55,8 +55,12 @@ impl FollowableItem for Editor { cx: &mut AppContext, ) -> Option>>> { let project = workspace.read(cx).project().to_owned(); - let Some(proto::view::Variant::Editor(_)) = state else { return None }; - let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() }; + let Some(proto::view::Variant::Editor(_)) = state else { + return None; + }; + let Some(proto::view::Variant::Editor(state)) = state.take() else { + unreachable!() + }; let client = project.read(cx).client(); let replica_id = project.read(cx).replica_id(); @@ -341,10 +345,16 @@ async fn update_editor_from_message( let mut insertions = message.inserted_excerpts.into_iter().peekable(); while let Some(insertion) = insertions.next() { - let Some(excerpt) = insertion.excerpt else { continue }; - let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue }; + let Some(excerpt) = insertion.excerpt else { + continue; + }; + let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { + continue; + }; let buffer_id = excerpt.buffer_id; - let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue }; + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { + continue; + }; let adjacent_excerpts = iter::from_fn(|| { let insertion = insertions.peek()?; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 9dd40af8981dab6d82ba4dda237ca46804575519..b9bf4f52a1c50e3b31aaf91da6586b9cfea7c958 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2756,7 +2756,9 @@ impl MultiBufferSnapshot { // Get the ranges of the innermost pair of brackets. let mut result: Option<(Range, Range)> = None; - let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; }; + let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { + return None; + }; for (open, close) in enclosing_bracket_ranges { let len = close.end - open.start; diff --git a/crates/gpui/src/keymap_matcher/keymap_context.rs b/crates/gpui/src/keymap_matcher/keymap_context.rs index fd60a8f4b5d385eb94b7edf0bfeb407a9dce8c20..d9c54dbc8e6fe71a7567ac4d6908a2d53689943d 100644 --- a/crates/gpui/src/keymap_matcher/keymap_context.rs +++ b/crates/gpui/src/keymap_matcher/keymap_context.rs @@ -67,7 +67,9 @@ impl KeymapContextPredicate { } pub fn eval(&self, contexts: &[KeymapContext]) -> bool { - let Some(context) = contexts.first() else { return false }; + let Some(context) = contexts.first() else { + return false; + }; match self { Self::Identifier(name) => (&context.set).contains(name.as_str()), Self::Equal(left, right) => context diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1b83ca59646cbe9f434aabed1c4f12b48778ca66..7569925256f098c2c4c63128213b696c1ae7dc88 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2484,7 +2484,9 @@ impl BufferSnapshot { matches.advance(); - let Some((open, close)) = open.zip(close) else { continue }; + let Some((open, close)) = open.zip(close) else { + continue; + }; let bracket_range = open.start..=close.end; if !bracket_range.overlaps(&range) { diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index b6e1d16e18beac7c9b1282a639a619b57038867e..18f2e9b264159299b92148019ffcfe5de7006dca 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -310,7 +310,9 @@ impl SyntaxSnapshot { // Ignore edits that end before the start of this layer, and don't consider them // for any subsequent layers at this same depth. loop { - let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { continue 'outer }; + let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { + continue 'outer; + }; if edit_range.end.cmp(&layer.range.start, text).is_le() { first_edit_ix_for_depth += 1; } else { @@ -391,7 +393,9 @@ impl SyntaxSnapshot { .filter::<_, ()>(|summary| summary.contains_unknown_injections); cursor.next(text); while let Some(layer) = cursor.item() { - let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() }; + let SyntaxLayerContent::Pending { language_name } = &layer.content else { + unreachable!() + }; if registry .language_for_name_or_extension(language_name) .now_or_never() @@ -533,7 +537,9 @@ impl SyntaxSnapshot { let content = match step.language { ParseStepLanguage::Loaded { language } => { - let Some(grammar) = language.grammar() else { continue }; + let Some(grammar) = language.grammar() else { + continue; + }; let tree; let changed_ranges; diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index c7babf207efcb2fdb30ec19c65adc7589f193ec4..bd50608122b80e9dd3ceba0a20d6b29dbb9f07c4 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -932,8 +932,12 @@ fn check_interpolation( .zip(new_syntax_map.layers.iter()) { assert_eq!(old_layer.range, new_layer.range); - let Some(old_tree) = old_layer.content.tree() else { continue }; - let Some(new_tree) = new_layer.content.tree() else { continue }; + let Some(old_tree) = old_layer.content.tree() else { + continue; + }; + let Some(new_tree) = new_layer.content.tree() else { + continue; + }; let old_start_byte = old_layer.range.start.to_offset(old_buffer); let new_start_byte = new_layer.range.start.to_offset(new_buffer); let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point(); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 16fb019c62a81bfcbf0c37332ef0723347fb5ad8..d18f57ffe95386d3b986e30a8cddad27c4f250fd 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -549,7 +549,9 @@ impl View for LspLogToolbarItemView { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx).clone(); - let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() }; + let Some(log_view) = self.log_view.as_ref() else { + return Empty::new().into_any(); + }; let log_view = log_view.read(cx); let menu_rows = log_view.menu_items(cx).unwrap_or_default(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 292f9a5226dfb4897c4faa94ab35e6108d4ec136..8ca2571869f4575262cd9453015dec5ef9dfcd7d 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1655,7 +1655,11 @@ impl LspCommand for OnTypeFormatting { type ProtoRequest = proto::OnTypeFormatting; fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { - let Some(on_type_formatting_options) = &server_capabilities.document_on_type_formatting_provider else { return false }; + let Some(on_type_formatting_options) = + &server_capabilities.document_on_type_formatting_provider + else { + return false; + }; on_type_formatting_options .first_trigger_character .contains(&self.trigger) @@ -1769,7 +1773,9 @@ impl LspCommand for OnTypeFormatting { _: ModelHandle, _: AsyncAppContext, ) -> Result> { - let Some(transaction) = message.transaction else { return Ok(None) }; + let Some(transaction) = message.transaction else { + return Ok(None); + }; Ok(Some(language::proto::deserialize_transaction(transaction)?)) } @@ -2238,7 +2244,9 @@ impl LspCommand for InlayHints { type ProtoRequest = proto::InlayHints; fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { - let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false }; + let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { + return false; + }; match inlay_hint_provider { lsp::OneOf::Left(enabled) => *enabled, lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 9e30796bbc58e3176ef73e8acb95358565fc6b34..c128de79107118e421fa306f743716ca38e051f2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2317,9 +2317,10 @@ impl BackgroundScannerState { for changed_path in changed_paths { let Some(dot_git_dir) = changed_path .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) else { - continue; - }; + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) + else { + continue; + }; // Avoid processing the same repository multiple times, if multiple paths // within it have changed. @@ -2348,7 +2349,10 @@ impl BackgroundScannerState { let Some(work_dir) = self .snapshot .entry_for_id(entry_id) - .map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue }; + .map(|entry| RepositoryWorkDirectory(entry.path.clone())) + else { + continue; + }; log::info!("reload git repository {:?}", dot_git_dir); let repository = repository.repo_ptr.lock(); diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 3055399c139ddff1c469d58c4be979943c77b345..1804c2b1fce77054cc21b4d5f119da32531a8bf8 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -40,7 +40,9 @@ impl View for QuickActionBar { } fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - let Some(editor) = self.active_editor() else { return Empty::new().into_any(); }; + let Some(editor) = self.active_editor() else { + return Empty::new().into_any(); + }; let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); let mut bar = Flex::row().with_child(render_quick_action_bar_button( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index eba4058729513335d05710511fd573b3b54d3c83..d7633a45e49af1a90a86d6bdce2dba95d209d74b 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -885,7 +885,9 @@ impl ProjectSearchView { if !dir_entry.is_dir() { return; } - let Some(filter_str) = dir_entry.path.to_str() else { return; }; + let Some(filter_str) = dir_entry.path.to_str() else { + return; + }; let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let search = cx.add_view(|cx| ProjectSearchView::new(model, cx)); diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 70495b59d30cc9bb47eb9aa66e6cf22e73d720da..736f2c98a8b6ac92f8b857b961e5e5a796135f65 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -57,7 +57,9 @@ pub fn init( cx.subscribe_global::({ move |event, cx| { - let Some(semantic_index) = SemanticIndex::global(cx) else { return; }; + let Some(semantic_index) = SemanticIndex::global(cx) else { + return; + }; let workspace = &event.0; if let Some(workspace) = workspace.upgrade(cx) { let project = workspace.read(cx).project().clone(); diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 93cb2ab3d74bd873f55c75d4b4415e7fbf782b51..28cc2db784d5d4a9f4cfc8f2049a18171f1ce551 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -63,20 +63,23 @@ impl KeymapFile { // string. But `RawValue` currently does not work inside of an untagged enum. match action { Value::Array(items) => { - let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else { + let Ok([name, data]): Result<[serde_json::Value; 2], _> = + items.try_into() + else { return Some(Err(anyhow!("Expected array of length 2"))); }; let serde_json::Value::String(name) = name else { - return Some(Err(anyhow!("Expected first item in array to be a string."))) + return Some(Err(anyhow!( + "Expected first item in array to be a string." + ))); }; - cx.deserialize_action( - &name, - Some(data), - ) - }, + cx.deserialize_action(&name, Some(data)) + } Value::String(name) => cx.deserialize_action(&name, None), Value::Null => Ok(no_action()), - _ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))), + _ => { + return Some(Err(anyhow!("Expected two-element array, got {action:?}"))) + } } .with_context(|| { format!( diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 5d2055051792c8967d2ca7d2d4ffa46940fc5c29..73ed4b059ea37ba6770280a2e84318d1c4517aec 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -107,20 +107,15 @@ impl PickerDelegate for BranchListDelegate { let delegate = view.delegate(); let project = delegate.workspace.read(cx).project().read(&cx); - let Some(worktree) = project - .visible_worktrees(cx) - .next() - else { + let Some(worktree) = project.visible_worktrees(cx).next() else { bail!("Cannot update branch list as there are no visible worktrees") }; - let mut cwd = worktree .read(cx) - .abs_path() - .to_path_buf(); + let mut cwd = worktree.read(cx).abs_path().to_path_buf(); cwd.push(".git"); - let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")}; - let mut branches = repo - .lock() - .branches()?; + let Some(repo) = project.fs().open_repo(&cwd) else { + bail!("Project does not have associated git repository.") + }; + let mut branches = repo.lock().branches()?; const RECENT_BRANCHES_COUNT: usize = 10; if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT { // Truncate list of recent branches @@ -142,8 +137,13 @@ impl PickerDelegate for BranchListDelegate { }) .collect::>()) }) - .log_err() else { return; }; - let Some(candidates) = candidates.log_err() else {return;}; + .log_err() + else { + return; + }; + let Some(candidates) = candidates.log_err() else { + return; + }; let matches = if query.is_empty() { candidates .into_iter() @@ -184,7 +184,11 @@ impl PickerDelegate for BranchListDelegate { fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { let current_pick = self.selected_index(); - let Some(current_pick) = self.matches.get(current_pick).map(|pick| pick.string.clone()) else { + let Some(current_pick) = self + .matches + .get(current_pick) + .map(|pick| pick.string.clone()) + else { return; }; cx.spawn(|picker, mut cx| async move { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 3d16bb355246688096f425a3ab5076a3864d9e25..3c437f91779ba27f2f2f36c555e6574b2158094b 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -33,7 +33,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { editor.set_clip_at_line_ends(false, cx); let Some(item) = cx.read_from_clipboard() else { - return + return; }; let clipboard_text = Cow::Borrowed(item.text()); if clipboard_text.is_empty() { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index d198eb628d1a2d198bb1b584db7c9a43d2569ec3..6d40f844e70cd3777ab0b8c554cbb06209996381 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -77,7 +77,10 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex } let Some((new_head, goal)) = - motion.move_point(map, current_head, selection.goal, times) else { return }; + motion.move_point(map, current_head, selection.goal, times) + else { + return; + }; selection.set_head(new_head, goal); @@ -132,7 +135,7 @@ pub fn visual_block_motion( } let Some((new_head, _)) = move_selection(&map, head, goal) else { - return + return; }; head = new_head; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 7528fb746864e2c66f013cf88014e855fa52a20a..93fb484214fc181d4636845cf1e00a19760b2fb3 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -742,8 +742,8 @@ mod element { while proposed_current_pixel_change.abs() > 0. { let Some(current_ix) = successors.next() else { - break; - }; + break; + }; let next_target_size = f32::max( size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 62bb7a82a29619d7f6bec11053db56cbf75a5faf..be8148256d0b0f294bbeeafbcfb59e47ec4862d7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2314,8 +2314,12 @@ impl Workspace { item_id_to_move: usize, cx: &mut ViewContext, ) { - let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; }; - let Some(from) = from.upgrade(cx) else { return; }; + let Some(pane_to_split) = pane_to_split.upgrade(cx) else { + return; + }; + let Some(from) = from.upgrade(cx) else { + return; + }; let new_pane = self.add_pane(cx); self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx); diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 41ad28ba862e38e04b52ec5e4e1b77e87b183200..331d829cbc78f24bcc5a641bc11c556de2cd11bc 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -89,7 +89,9 @@ impl LspAdapter for PythonLspAdapter { // to allow our own fuzzy score to be used to break ties. // // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 - let Some(sort_text) = &mut item.sort_text else { return }; + let Some(sort_text) = &mut item.sort_text else { + return; + }; let mut parts = sort_text.split('.'); let Some(first) = parts.next() else { return }; let Some(second) = parts.next() else { return }; From f798be6e273d6744663a6988afc01a3463a4c5bd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 10:25:21 -0700 Subject: [PATCH 35/67] Fix rust 1.72 warnings about shadowed glob re-exports --- crates/language/src/buffer.rs | 2 +- crates/language/src/language.rs | 2 +- crates/lsp/src/lsp.rs | 2 +- crates/project/src/project_tests.rs | 2 +- crates/project/src/worktree.rs | 4 ++-- crates/project/src/worktree_tests.rs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 7569925256f098c2c4c63128213b696c1ae7dc88..d5586af5ef8fd9088685a6934c16ea62169d953f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -14,7 +14,7 @@ use crate::{ CodeLabel, LanguageScope, Outline, }; use anyhow::{anyhow, Result}; -use clock::ReplicaId; +pub use clock::ReplicaId; use fs::LineEnding; use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 82245d67ca0487adb33aac8ec124f658948c3509..7a9e6b83ceb48584211792239fe2a802ec2e886f 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -18,7 +18,7 @@ use futures::{ FutureExt, TryFutureExt as _, }; use gpui::{executor::Background, AppContext, AsyncAppContext, Task}; -use highlight_map::HighlightMap; +pub use highlight_map::HighlightMap; use lazy_static::lazy_static; use lsp::{CodeActionKind, LanguageServerBinary}; use parking_lot::{Mutex, RwLock}; diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e0ae64d8069c08b12e11b8b12155892dc974ae0d..7cba03955280dbed1cd98fd9a7b61a1b526c440f 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -77,7 +77,7 @@ pub enum Subscription { } #[derive(Serialize, Deserialize)] -struct Request<'a, T> { +pub struct Request<'a, T> { jsonrpc: &'static str, id: usize, method: &'a str, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 259c10ca057c8bb29ad5b2d805107eb982239441..a504900c83e9178f9ee6cbcf6a66e77a8fa44c0d 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,4 +1,4 @@ -use crate::{search::PathMatcher, worktree::WorktreeHandle, Event, *}; +use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; use fs::{FakeFs, LineEnding, RealFs}; use futures::{future, StreamExt}; use gpui::{executor::Deterministic, test::subscribe, AppContext}; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c128de79107118e421fa306f743716ca38e051f2..e6e0f37cc74b317c5d9de9adda90f9820c230777 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4030,7 +4030,7 @@ struct UpdateIgnoreStatusJob { scan_queue: Sender, } -pub trait WorktreeHandle { +pub trait WorktreeModelHandle { #[cfg(any(test, feature = "test-support"))] fn flush_fs_events<'a>( &self, @@ -4038,7 +4038,7 @@ pub trait WorktreeHandle { ) -> futures::future::LocalBoxFuture<'a, ()>; } -impl WorktreeHandle for ModelHandle { +impl WorktreeModelHandle for ModelHandle { // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that // occurred before the worktree was constructed. These events can cause the worktree to perform // extra directory scans, and emit extra scan-state notifications. diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index 6f5b3635096e334b57357f633370782f6a2a965a..4253f45b0ce912412b0f9716474f92d0f875f026 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -1,5 +1,5 @@ use crate::{ - worktree::{Event, Snapshot, WorktreeHandle}, + worktree::{Event, Snapshot, WorktreeModelHandle}, Entry, EntryKind, PathChange, Worktree, }; use anyhow::Result; From 1f3e009b3255d46a7708b2a4f2c1edd1f35a8d05 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 11:34:07 -0600 Subject: [PATCH 36/67] Fix zed-industries/community#1950 --- crates/vim/src/visual.rs | 36 ++++++++++++++----- .../vim/test_data/test_visual_block_mode.json | 6 ++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 6d40f844e70cd3777ab0b8c554cbb06209996381..b68da870f0d579bd555e6adba7b8e0890286c419 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -126,10 +126,15 @@ pub fn visual_block_motion( let map = &s.display_map(); let mut head = s.newest_anchor().head().to_display_point(map); let mut tail = s.oldest_anchor().tail().to_display_point(map); - let mut goal = s.newest_anchor().goal; - let was_reversed = tail.column() > head.column(); + let (start, end) = match s.newest_anchor().goal { + SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), + SelectionGoal::Column(start) if preserve_goal => (start, start + 1), + _ => (tail.column(), head.column()), + }; + let goal = SelectionGoal::ColumnRange { start, end }; + let was_reversed = tail.column() > head.column(); if !was_reversed && !preserve_goal { head = movement::saturating_left(map, head); } @@ -149,13 +154,6 @@ pub fn visual_block_motion( head = movement::saturating_right(map, head) } - let (start, end) = match goal { - SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), - SelectionGoal::Column(start) if preserve_goal => (start, start + 1), - _ => (tail.column(), head.column()), - }; - goal = SelectionGoal::ColumnRange { start, end }; - let columns = if is_reversed { head.column()..tail.column() } else if head.column() == tail.column() { @@ -791,6 +789,26 @@ mod test { " }) .await; + + //https://github.com/zed-industries/community/issues/1950 + cx.set_shared_state(indoc! { + "Theˇ quick brown + + fox jumps over + the lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"]) + .await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick brown + + fox «jˇ»umps over + the lazy dog + " + }) + .await; } #[gpui::test] diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json index ac306de4ab783715fce34305fd3551a6a3d57131..2239ef43a8037d06d91be33afb45488fbda204fd 100644 --- a/crates/vim/test_data/test_visual_block_mode.json +++ b/crates/vim/test_data/test_visual_block_mode.json @@ -30,3 +30,9 @@ {"Key":"o"} {"Key":"escape"} {"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}} +{"Put":{"state":"Theˇ quick brown\n\nfox jumps over\nthe lazy dog\n"}} +{"Key":"l"} +{"Key":"ctrl-v"} +{"Key":"j"} +{"Key":"j"} +{"Get":{"state":"The «qˇ»uick brown\n\nfox «jˇ»umps over\nthe lazy dog\n","mode":"VisualBlock"}} From 790aa5d476f2263f2a08a705bb82c7ce5b8d2b53 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 12:53:40 -0600 Subject: [PATCH 37/67] Add relative_line_mode Co-Authored-By: joseph@zed.dev --- assets/settings/default.json | 1 + crates/editor/src/editor_settings.rs | 2 + crates/editor/src/element.rs | 81 +++++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 24412b883bf0be12cb2639dd54dec7f70adf6882..c6c3ff59918d190809ee37ddf71b1fd19fe58e23 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -98,6 +98,7 @@ // Whether to show selections in the scrollbar. "selections": true }, + "relative_line_numbers": false, // Inlay hint related settings "inlay_hints": { // Global switch to toggle hints on and off, switched off by default. diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index f4499b5651cc158c66df3faad7f0ecf707e01bb6..b06f23429a15b17368c8da23a755ad2fe3c637c5 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -9,6 +9,7 @@ pub struct EditorSettings { pub show_completions_on_input: bool, pub use_on_type_format: bool, pub scrollbar: Scrollbar, + pub relative_line_numbers: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -34,6 +35,7 @@ pub struct EditorSettingsContent { pub show_completions_on_input: Option, pub use_on_type_format: Option, pub scrollbar: Option, + pub relative_line_numbers: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3ba807308c6718e2fcbf7e8edf0f9c9f9d08824e..d088fc2e3a1659e65df42d872c659abf162dda58 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1439,10 +1439,47 @@ impl EditorElement { .collect() } + fn calculate_relative_line_numbers( + &self, + rows: &Range, + buffer_rows: &Vec>, + relative_to: Option, + ) -> HashMap { + let mut relative_rows: HashMap = Default::default(); + if let Some(relative_to) = relative_to { + let head_idx = (relative_to - rows.start) as usize; + let mut delta = 1; + let mut i = head_idx + 1; + while i < buffer_rows.len() { + if buffer_rows[i].is_some() { + relative_rows.insert(i, delta); + delta += 1; + } + i += 1; + } + delta = 1; + i = head_idx; + while i > 0 && buffer_rows[i].is_none() { + i -= 1; + } + + while i > 0 { + i -= 1; + if buffer_rows[i].is_some() { + relative_rows.insert(i, delta); + delta += 1; + } + } + } + + relative_rows + } + fn layout_line_numbers( &self, rows: Range, active_rows: &BTreeMap, + newest_selection_head: Option, is_singleton: bool, snapshot: &EditorSnapshot, cx: &ViewContext, @@ -1455,21 +1492,33 @@ impl EditorElement { let mut line_number_layouts = Vec::with_capacity(rows.len()); let mut fold_statuses = Vec::with_capacity(rows.len()); let mut line_number = String::new(); - for (ix, row) in snapshot + let is_relative = settings::get::(cx).relative_line_numbers; + let relative_to = if is_relative { + newest_selection_head.map(|head| head.row()) + } else { + None + }; + + let buffer_rows = snapshot .buffer_rows(rows.start) .take((rows.end - rows.start) as usize) - .enumerate() - { + .collect::>(); + + let relative_rows = self.calculate_relative_line_numbers(&rows, &buffer_rows, relative_to); + + for (ix, row) in buffer_rows.iter().enumerate() { let display_row = rows.start + ix as u32; let (active, color) = if active_rows.contains_key(&display_row) { (true, style.line_number_active) } else { (false, style.line_number) }; - if let Some(buffer_row) = row { + if let Some(buffer_row) = *row { if include_line_numbers { line_number.clear(); - write!(&mut line_number, "{}", buffer_row + 1).unwrap(); + let default_number = buffer_row + 1; + let number = relative_rows.get(&ix).unwrap_or(&default_number); + write!(&mut line_number, "{}", number).unwrap(); line_number_layouts.push(Some(cx.text_layout_cache().layout_str( &line_number, style.text.font_size, @@ -2296,6 +2345,7 @@ impl Element for EditorElement { let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, + newest_selection_head, is_singleton, &snapshot, cx, @@ -3054,7 +3104,6 @@ mod tests { #[gpui::test] fn test_layout_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx .add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); @@ -3066,10 +3115,28 @@ mod tests { let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); element - .layout_line_numbers(0..6, &Default::default(), false, &snapshot, cx) + .layout_line_numbers(0..6, &Default::default(), None, false, &snapshot, cx) .0 }); assert_eq!(layouts.len(), 6); + + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + let rows = 0..6; + let buffer_rows = snapshot + .buffer_rows(rows.start) + .take((rows.end - rows.start) as usize) + .collect::>(); + + element.calculate_relative_line_numbers(&rows, &buffer_rows, Some(3)) + }); + assert_eq!(relative_rows[&0], 3); + assert_eq!(relative_rows[&1], 2); + assert_eq!(relative_rows[&2], 1); + // current line has no relative number + assert_eq!(relative_rows[&4], 1); + assert_eq!(relative_rows[&5], 2); } #[gpui::test] From 8d5dc266a3b83f1c7b776049b96820f433dbcffa Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 23:39:13 -0600 Subject: [PATCH 38/67] Fix relative line numbers when newest cursor offscreen --- crates/editor/src/element.rs | 108 ++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d088fc2e3a1659e65df42d872c659abf162dda58..04f84fb6164238407b7ffc2cde1f955624f6b00c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1441,34 +1441,48 @@ impl EditorElement { fn calculate_relative_line_numbers( &self, + snapshot: &EditorSnapshot, rows: &Range, - buffer_rows: &Vec>, relative_to: Option, - ) -> HashMap { - let mut relative_rows: HashMap = Default::default(); - if let Some(relative_to) = relative_to { - let head_idx = (relative_to - rows.start) as usize; - let mut delta = 1; - let mut i = head_idx + 1; - while i < buffer_rows.len() { - if buffer_rows[i].is_some() { - relative_rows.insert(i, delta); - delta += 1; + ) -> HashMap { + let mut relative_rows: HashMap = Default::default(); + let Some(relative_to) = relative_to else { + return relative_rows; + }; + + let start = rows.start.min(relative_to); + let end = rows.end.max(relative_to); + + let buffer_rows = snapshot + .buffer_rows(start) + .take(1 + (end - start) as usize) + .collect::>(); + + let head_idx = relative_to - start; + let mut delta = 1; + let mut i = head_idx + 1; + while i < buffer_rows.len() as u32 { + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); } - i += 1; - } - delta = 1; - i = head_idx; - while i > 0 && buffer_rows[i].is_none() { - i -= 1; + delta += 1; } + i += 1; + } + delta = 1; + i = head_idx.min(buffer_rows.len() as u32 - 1); + while i > 0 && buffer_rows[i as usize].is_none() { + i -= 1; + } - while i > 0 { - i -= 1; - if buffer_rows[i].is_some() { - relative_rows.insert(i, delta); - delta += 1; + while i > 0 { + i -= 1; + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); } + delta += 1; } } @@ -1499,25 +1513,26 @@ impl EditorElement { None }; - let buffer_rows = snapshot + let relative_rows = self.calculate_relative_line_numbers(&snapshot, &rows, relative_to); + + for (ix, row) in snapshot .buffer_rows(rows.start) .take((rows.end - rows.start) as usize) - .collect::>(); - - let relative_rows = self.calculate_relative_line_numbers(&rows, &buffer_rows, relative_to); - - for (ix, row) in buffer_rows.iter().enumerate() { + .enumerate() + { let display_row = rows.start + ix as u32; let (active, color) = if active_rows.contains_key(&display_row) { (true, style.line_number_active) } else { (false, style.line_number) }; - if let Some(buffer_row) = *row { + if let Some(buffer_row) = row { if include_line_numbers { line_number.clear(); let default_number = buffer_row + 1; - let number = relative_rows.get(&ix).unwrap_or(&default_number); + let number = relative_rows + .get(&(ix as u32 + rows.start)) + .unwrap_or(&default_number); write!(&mut line_number, "{}", number).unwrap(); line_number_layouts.push(Some(cx.text_layout_cache().layout_str( &line_number, @@ -2345,7 +2360,7 @@ impl Element for EditorElement { let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, - newest_selection_head, + newest_selection_head.or_else(|| Some(editor.selections.newest_display(cx).head())), is_singleton, &snapshot, cx, @@ -3122,14 +3137,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - - let rows = 0..6; - let buffer_rows = snapshot - .buffer_rows(rows.start) - .take((rows.end - rows.start) as usize) - .collect::>(); - - element.calculate_relative_line_numbers(&rows, &buffer_rows, Some(3)) + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) }); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); @@ -3137,6 +3145,28 @@ mod tests { // current line has no relative number assert_eq!(relative_rows[&4], 1); assert_eq!(relative_rows[&5], 2); + + // works if cursor is before screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&3], 2); + assert_eq!(relative_rows[&4], 3); + assert_eq!(relative_rows[&5], 4); + + // works if cursor is after screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&0], 5); + assert_eq!(relative_rows[&1], 4); + assert_eq!(relative_rows[&2], 3); } #[gpui::test] From f18cdcba546f0965aa2708324064e4639c46eac5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 10:56:29 -0600 Subject: [PATCH 39/67] Fix relative line numbers in vim visual mode In visual mode when your selection ends with a newline we show the cursor at the end of the previous line (not the start of the current line). We had only been accounting for this if the cursor was on-screen. --- crates/editor/src/element.rs | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 04f84fb6164238407b7ffc2cde1f955624f6b00c..2a623b9b6bcb0a14d7acb423b6407fa1b410c59e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1493,7 +1493,7 @@ impl EditorElement { &self, rows: Range, active_rows: &BTreeMap, - newest_selection_head: Option, + newest_selection_head: DisplayPoint, is_singleton: bool, snapshot: &EditorSnapshot, cx: &ViewContext, @@ -1508,7 +1508,7 @@ impl EditorElement { let mut line_number = String::new(); let is_relative = settings::get::(cx).relative_line_numbers; let relative_to = if is_relative { - newest_selection_head.map(|head| head.row()) + Some(newest_selection_head.row()) } else { None }; @@ -2357,10 +2357,22 @@ impl Element for EditorElement { }) .collect(); + let head_for_relative = newest_selection_head.unwrap_or_else(|| { + let newest = editor.selections.newest::(cx); + SelectionLayout::new( + newest, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + true, + ) + .head + }); + let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, - newest_selection_head.or_else(|| Some(editor.selections.newest_display(cx).head())), + head_for_relative, is_singleton, &snapshot, cx, @@ -3130,14 +3142,21 @@ mod tests { let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); element - .layout_line_numbers(0..6, &Default::default(), None, false, &snapshot, cx) + .layout_line_numbers( + 0..6, + &Default::default(), + DisplayPoint::new(0, 0), + false, + &snapshot, + cx, + ) .0 }); assert_eq!(layouts.len(), 6); let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) + element.calculate_relative_line_numbers(&snapshot, &(0..6), 3) }); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); @@ -3150,7 +3169,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + element.calculate_relative_line_numbers(&snapshot, &(3..6), 1) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&3], 2); @@ -3161,7 +3180,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + element.calculate_relative_line_numbers(&snapshot, &(0..3), 6) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&0], 5); From bde67b2b9c6fff9d1977e43efd251bff023a53b0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 11:08:39 -0600 Subject: [PATCH 40/67] Fix merge-conflict --- crates/editor/src/element.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2a623b9b6bcb0a14d7acb423b6407fa1b410c59e..6f93b07f654067ceb5a95eb86173396d647cc7a9 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2365,6 +2365,7 @@ impl Element for EditorElement { editor.cursor_shape, &snapshot.display_snapshot, true, + true, ) .head }); @@ -3156,7 +3157,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..6), 3) + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) }); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); @@ -3169,7 +3170,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(3..6), 1) + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&3], 2); @@ -3180,7 +3181,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..3), 6) + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&0], 5); From 507a5db09c53014e8e45c00fe39c71e992f978ab Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 25 Aug 2023 15:06:31 -0400 Subject: [PATCH 41/67] WIP Co-Authored-By: Mikayla Maki --- assets/settings/default.json | 27 ++- crates/project/src/terminals.rs | 59 +++---- crates/terminal/src/terminal.rs | 122 +------------- crates/terminal/src/terminal_settings.rs | 163 +++++++++++++++++++ crates/terminal_view/src/terminal_element.rs | 3 +- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 5 +- 7 files changed, 213 insertions(+), 168 deletions(-) create mode 100644 crates/terminal/src/terminal_settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 27be6ae5d28204eb29049ea2eaa650e55892fa4c..b1d36c938f819eac4468e7dc816a90eb62b43fa9 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -333,6 +333,24 @@ // "custom": 2 // }, "line_height": "comfortable", + // Activate the python virtual environment, if one is found, in the + // terminal's working directory (as resolved by the working_directory + // setting). Set this to "off" to disable this behavior. + "detect_venv": { + "on": { + // Default directories to search for virtual environments, relative + // to the current working directory. We recommend overriding this + // in your project's settings, rather than globally. + "directories": [ + ".env", + "env", + ".venv", + "venv" + ], + // Can also be 'csh' and 'fish' + "activate_script": "default" + } + } // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. // "font_size": "15", @@ -340,15 +358,6 @@ // the terminal will default to matching the buffer's font family. // "font_family": "Zed Mono", // --- - // Whether or not to automatically search for, and activate, Python virtual - // environments. - // Current limitations: - // - Only ".env", "env", ".venv", and "venv" are searched for at the - // root of the project - // - Only works with Posix-complaint shells - // - Only activates the first virtual environment it finds, regardless - // of the nunber of projects in the workspace. - "activate_python_virtual_environment": false }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 2fb66a6c4c95b06acf4a305ec6652d2f2ee3a653..68a043131684619d0a2cb12e2d18f52fd4e3ebaa 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,10 @@ use crate::Project; use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; -use std::path::PathBuf; -use terminal::{Shell, Terminal, TerminalBuilder, TerminalSettings}; +use std::path::{Path, PathBuf}; +use terminal::{ + terminal_settings::{self, TerminalSettings, VenvSettingsContent}, + Terminal, TerminalBuilder, +}; #[cfg(target_os = "macos")] use std::os::unix::ffi::OsStrExt; @@ -23,8 +26,7 @@ impl Project { )); } else { let settings = settings::get::(cx); - let activate_python_virtual_environment = - settings.activate_python_virtual_environment.clone(); + let python_settings = settings.detect_venv.clone(); let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( @@ -53,15 +55,15 @@ impl Project { }) .detach(); - if activate_python_virtual_environment { - let activate_script_path = self.find_activate_script_path(&shell, cx); + if let Some(python_settings) = &python_settings.as_option() { + let activate_script_path = + self.find_activate_script_path(&python_settings, working_directory); self.activate_python_virtual_environment( activate_script_path, &terminal_handle, cx, ); } - terminal_handle }); @@ -71,37 +73,26 @@ impl Project { pub fn find_activate_script_path( &mut self, - shell: &Shell, - cx: &mut ModelContext, + settings: &VenvSettingsContent, + working_directory: Option, ) -> Option { - let program = match shell { - terminal::Shell::System => "Figure this out", - terminal::Shell::Program(program) => program, - terminal::Shell::WithArguments { program, args: _ } => program, + // When we are unable to resolve the working directory, the terminal builder + // defaults to '/'. We should probably encode this directly somewhere, but for + // now, let's just hard code it here. + let working_directory = working_directory.unwrap_or_else(|| Path::new("/").to_path_buf()); + let activate_script_name = match settings.activate_script { + terminal_settings::ActivateScript::Default => "activate", + terminal_settings::ActivateScript::Csh => "activate.csh", + terminal_settings::ActivateScript::Fish => "activate.fish", }; - // This is so hacky - find a better way to do this - let script_name = if program.contains("fish") { - "activate.fish" - } else { - "activate" - }; + for virtual_environment_name in settings.directories { + let mut path = working_directory.join(virtual_environment_name); + path.push("bin/"); + path.push(activate_script_name); - let worktree_paths = self - .worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()); - - const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; - - for worktree_path in worktree_paths { - for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES { - let mut path = worktree_path.join(virtual_environment_name); - path.push("bin/"); - path.push(script_name); - - if path.exists() { - return Some(path); - } + if path.exists() { + return Some(path); } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 73ff09225f62c5c09185d074163f9eea631decf1..e28e0ca5c16bb15454aa2d9bbc248963df56c2a9 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,5 +1,6 @@ pub mod mappings; pub use alacritty_terminal; +pub mod terminal_settings; use alacritty_terminal::{ ansi::{ClearMode, Handler}, @@ -31,8 +32,8 @@ use mappings::mouse::{ }; use procinfo::LocalProcessInfo; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings}; use util::truncate_and_trailoff; use std::{ @@ -48,7 +49,6 @@ use std::{ use thiserror::Error; use gpui::{ - fonts, geometry::vector::{vec2f, Vector2F}, keymap_matcher::Keystroke, platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase}, @@ -134,124 +134,6 @@ pub fn init(cx: &mut AppContext) { settings::register::(cx); } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum TerminalDockPosition { - Left, - Bottom, - Right, -} - -#[derive(Deserialize)] -pub struct TerminalSettings { - pub shell: Shell, - pub working_directory: WorkingDirectory, - font_size: Option, - pub font_family: Option, - pub line_height: TerminalLineHeight, - pub font_features: Option, - pub env: HashMap, - pub blinking: TerminalBlink, - pub alternate_scroll: AlternateScroll, - pub option_as_meta: bool, - pub copy_on_select: bool, - pub dock: TerminalDockPosition, - pub default_width: f32, - pub default_height: f32, - pub activate_python_virtual_environment: bool, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct TerminalSettingsContent { - pub shell: Option, - pub working_directory: Option, - pub font_size: Option, - pub font_family: Option, - pub line_height: Option, - pub font_features: Option, - pub env: Option>, - pub blinking: Option, - pub alternate_scroll: Option, - pub option_as_meta: Option, - pub copy_on_select: Option, - pub dock: Option, - pub default_width: Option, - pub default_height: Option, - pub activate_python_virtual_environment: Option, -} - -impl TerminalSettings { - pub fn font_size(&self, cx: &AppContext) -> Option { - self.font_size - .map(|size| theme::adjusted_font_size(size, cx)) - } -} - -impl settings::Setting for TerminalSettings { - const KEY: Option<&'static str> = Some("terminal"); - - type FileContent = TerminalSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &AppContext, - ) -> Result { - Self::load_via_json_merge(default_value, user_values) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] -#[serde(rename_all = "snake_case")] -pub enum TerminalLineHeight { - #[default] - Comfortable, - Standard, - Custom(f32), -} - -impl TerminalLineHeight { - pub fn value(&self) -> f32 { - match self { - TerminalLineHeight::Comfortable => 1.618, - TerminalLineHeight::Standard => 1.3, - TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum TerminalBlink { - Off, - TerminalControlled, - On, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Shell { - System, - Program(String), - WithArguments { program: String, args: Vec }, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AlternateScroll { - On, - Off, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectory { - CurrentProjectDirectory, - FirstProjectDirectory, - AlwaysHome, - Always { directory: String }, -} - #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub struct TerminalSize { pub cell_width: f32, diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..e0649ebf65cbecb84da761d3a295de08334c6176 --- /dev/null +++ b/crates/terminal/src/terminal_settings.rs @@ -0,0 +1,163 @@ +use std::{collections::HashMap, path::PathBuf}; + +use gpui::{fonts, AppContext}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TerminalDockPosition { + Left, + Bottom, + Right, +} + +#[derive(Deserialize)] +pub struct TerminalSettings { + pub shell: Shell, + pub working_directory: WorkingDirectory, + font_size: Option, + pub font_family: Option, + pub line_height: TerminalLineHeight, + pub font_features: Option, + pub env: HashMap, + pub blinking: TerminalBlink, + pub alternate_scroll: AlternateScroll, + pub option_as_meta: bool, + pub copy_on_select: bool, + pub dock: TerminalDockPosition, + pub default_width: f32, + pub default_height: f32, + pub detect_venv: VenvSettings, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum VenvSettings { + #[default] + Off, + On { + activate_script: Option, + directories: Option>, + }, +} + +pub struct VenvSettingsContent<'a> { + pub activate_script: ActivateScript, + pub directories: &'a [PathBuf], +} + +impl VenvSettings { + pub fn as_option(&self) -> Option { + match self { + VenvSettings::Off => None, + VenvSettings::On { + activate_script, + directories, + } => Some(VenvSettingsContent { + activate_script: activate_script.unwrap_or(ActivateScript::Default), + directories: directories.as_deref().unwrap_or(&[]), + }), + } + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ActivateScript { + #[default] + Default, + Csh, + Fish, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct TerminalSettingsContent { + pub shell: Option, + pub working_directory: Option, + pub font_size: Option, + pub font_family: Option, + pub line_height: Option, + pub font_features: Option, + pub env: Option>, + pub blinking: Option, + pub alternate_scroll: Option, + pub option_as_meta: Option, + pub copy_on_select: Option, + pub dock: Option, + pub default_width: Option, + pub default_height: Option, + pub detect_venv: Option, +} + +impl TerminalSettings { + pub fn font_size(&self, cx: &AppContext) -> Option { + self.font_size + .map(|size| theme::adjusted_font_size(size, cx)) + } +} + +impl settings::Setting for TerminalSettings { + const KEY: Option<&'static str> = Some("terminal"); + + type FileContent = TerminalSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum TerminalLineHeight { + #[default] + Comfortable, + Standard, + Custom(f32), +} + +impl TerminalLineHeight { + pub fn value(&self) -> f32 { + match self { + TerminalLineHeight::Comfortable => 1.618, + TerminalLineHeight::Standard => 1.3, + TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum TerminalBlink { + Off, + TerminalControlled, + On, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Shell { + System, + Program(String), + WithArguments { program: String, args: Vec }, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AlternateScroll { + On, + Off, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectory { + CurrentProjectDirectory, + FirstProjectDirectory, + AlwaysHome, + Always { directory: String }, +} diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 1d12b83c5c31162d39125322eef282a9d45b7f59..b3d87f531ad5794b86f4b56dbe307e3078a5ffd3 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -25,7 +25,8 @@ use terminal::{ term::{cell::Flags, TermMode}, }, mappings::colors::convert_color, - IndexedCell, Terminal, TerminalContent, TerminalSettings, TerminalSize, + terminal_settings::TerminalSettings, + IndexedCell, Terminal, TerminalContent, TerminalSize, }; use theme::{TerminalStyle, ThemeSettings}; use util::ResultExt; diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 472e748359ea7399a5bcf680c57ffa5a17ad1e8d..9fb3939e1f17a9adfe842130c43684ee4b2cddac 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -9,7 +9,7 @@ use gpui::{ use project::Fs; use serde::{Deserialize, Serialize}; use settings::SettingsStore; -use terminal::{TerminalDockPosition, TerminalSettings}; +use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 92465d6b32cff774480ca33d9adc8b5665a49141..104d181a7b9de60460bafa0f12abac51f4ac22e2 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -33,7 +33,8 @@ use terminal::{ index::Point, term::{search::RegexSearch, TermMode}, }, - Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory, + terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory}, + Event, MaybeNavigationTarget, Terminal, }; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ @@ -44,8 +45,6 @@ use workspace::{ NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; -pub use terminal::TerminalSettings; - const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); ///Event to transmit the scroll from the element to the view From 6fdf101745d18c16d54423d8b6131d4c8b31679c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 25 Aug 2023 14:34:19 -0700 Subject: [PATCH 42/67] Update database and RPC to provide configured feature flags --- crates/client/src/test.rs | 1 + .../20221109000000_test_schema.sql | 19 ++++++ ...0230825190322_add_server_feature_flags.sql | 16 +++++ crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries/users.rs | 54 +++++++++++++++++ crates/collab/src/db/tables.rs | 2 + crates/collab/src/db/tables/feature_flag.rs | 40 +++++++++++++ crates/collab/src/db/tables/user.rs | 23 +++++++ crates/collab/src/db/tables/user_feature.rs | 42 +++++++++++++ crates/collab/src/db/tests.rs | 1 + .../collab/src/db/tests/feature_flag_tests.rs | 60 +++++++++++++++++++ crates/collab/src/rpc.rs | 15 +++-- crates/rpc/proto/zed.proto | 1 + crates/rpc/src/rpc.rs | 2 +- 14 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 crates/collab/migrations/20230825190322_add_server_feature_flags.sql create mode 100644 crates/collab/src/db/tables/feature_flag.rs create mode 100644 crates/collab/src/db/tables/user_feature.rs create mode 100644 crates/collab/src/db/tests/feature_flag_tests.rs diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 4c12a205660f7932a6a7b412c6ee686a6199372c..00e7cd1508613c60a05ddbba8cabff86bbaf1d14 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -168,6 +168,7 @@ impl FakeServer { GetPrivateUserInfoResponse { metrics_id: "the-metrics-id".into(), staff: false, + flags: Default::default(), }, ) .await; diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7a4cd9fd23cbc80bb38e3b2e7446ae53a902066a..80477dcb3c3b9f4fc1efd25622243b59901cf4fc 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -249,3 +249,22 @@ CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replic CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); + + +CREATE TABLE "feature_flags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "flag" TEXT NOT NULL UNIQUE +); + +CREATE INDEX "index_feature_flags" ON "feature_flags" ("id"); + + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags (id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/migrations/20230825190322_add_server_feature_flags.sql b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql new file mode 100644 index 0000000000000000000000000000000000000000..fffde54a20e4869ccbef2093de4e7fe5044132e2 --- /dev/null +++ b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql @@ -0,0 +1,16 @@ +CREATE TABLE "feature_flags" ( + "id" SERIAL PRIMARY KEY, + "flag" VARCHAR(255) NOT NULL UNIQUE +); + +CREATE UNIQUE INDEX "index_feature_flags" ON "feature_flags" ("id"); + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 8501083f839940ed9723813b9aac8a029d706a0d..b33ea57183b8771792ea50c6b3ab2b2631971194 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -125,3 +125,4 @@ id_type!(ServerId); id_type!(SignupId); id_type!(UserId); id_type!(ChannelBufferCollaboratorId); +id_type!(FlagId); diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index bac0f14f8324126fe4f403887aa4eb65e4241de2..bd7c3e9ffd62dea8b0d283fb1c6e1c26e8958d2b 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -240,4 +240,58 @@ impl Database { result.push('%'); result } + + #[cfg(debug_assertions)] + pub async fn create_user_flag(&self, flag: &str) -> Result { + self.transaction(|tx| async move { + let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { + flag: ActiveValue::set(flag.to_string()), + ..Default::default() + }) + .exec(&*tx) + .await? + .last_insert_id; + + Ok(flag) + }) + .await + } + + #[cfg(debug_assertions)] + pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { + self.transaction(|tx| async move { + user_feature::Entity::insert(user_feature::ActiveModel { + user_id: ActiveValue::set(user), + feature_id: ActiveValue::set(flag), + }) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn get_user_flags(&self, user: UserId) -> Result> { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + Flag, + } + + let flags = user::Model { + id: user, + ..Default::default() + } + .find_linked(user::UserFlags) + .select_only() + .column(feature_flag::Column::Flag) + .into_values::<_, QueryAs>() + .all(&*tx) + .await?; + + Ok(flags) + }) + .await + } } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index fe747e0d27ec1cc5b67b0bbdb55a1c5992fa27b4..1765cee065fb6c7ae31818568a229e3c3c0bd3f0 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -7,6 +7,7 @@ pub mod channel_buffer_collaborator; pub mod channel_member; pub mod channel_path; pub mod contact; +pub mod feature_flag; pub mod follower; pub mod language_server; pub mod project; @@ -16,6 +17,7 @@ pub mod room_participant; pub mod server; pub mod signup; pub mod user; +pub mod user_feature; pub mod worktree; pub mod worktree_diagnostic_summary; pub mod worktree_entry; diff --git a/crates/collab/src/db/tables/feature_flag.rs b/crates/collab/src/db/tables/feature_flag.rs new file mode 100644 index 0000000000000000000000000000000000000000..41c1451c648e7115165a2cf3bfc4e84d9ae534a1 --- /dev/null +++ b/crates/collab/src/db/tables/feature_flag.rs @@ -0,0 +1,40 @@ +use sea_orm::entity::prelude::*; + +use crate::db::FlagId; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "feature_flags")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: FlagId, + pub flag: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeature, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeature.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +pub struct FlaggedUsers; + +impl Linked for FlaggedUsers { + type FromEntity = Entity; + + type ToEntity = super::user::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::Flag.def().rev(), + super::user_feature::Relation::User.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 402b06c2a71a164c9f9314ec0d9e4aa5519156c7..739693527f00a594f3376a6093dc8c0b1d270a8f 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -28,6 +28,8 @@ pub enum Relation { HostedProjects, #[sea_orm(has_many = "super::channel_member::Entity")] ChannelMemberships, + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeatures, } impl Related for Entity { @@ -54,4 +56,25 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeatures.def() + } +} + impl ActiveModelBehavior for ActiveModel {} + +pub struct UserFlags; + +impl Linked for UserFlags { + type FromEntity = Entity; + + type ToEntity = super::feature_flag::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::User.def().rev(), + super::user_feature::Relation::Flag.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user_feature.rs b/crates/collab/src/db/tables/user_feature.rs new file mode 100644 index 0000000000000000000000000000000000000000..cc24b5e796342f7733f59933362d46a0df2be112 --- /dev/null +++ b/crates/collab/src/db/tables/user_feature.rs @@ -0,0 +1,42 @@ +use sea_orm::entity::prelude::*; + +use crate::db::{FlagId, UserId}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "user_features")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + #[sea_orm(primary_key)] + pub feature_id: FlagId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::feature_flag::Entity", + from = "Column::FeatureId", + to = "super::feature_flag::Column::Id" + )] + Flag, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Flag.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 36a0888a62ed243904598d1386f8567fe5b821fd..ee961006cbbf74b019141c0973aca18d73309012 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,5 +1,6 @@ mod buffer_tests; mod db_tests; +mod feature_flag_tests; use super::*; use gpui::executor::Background; diff --git a/crates/collab/src/db/tests/feature_flag_tests.rs b/crates/collab/src/db/tests/feature_flag_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..9d5f039747c18fb6cfae77191654ba5b4584e21e --- /dev/null +++ b/crates/collab/src/db/tests/feature_flag_tests.rs @@ -0,0 +1,60 @@ +use crate::{ + db::{Database, NewUserParams}, + test_both_dbs, +}; +use std::sync::Arc; + +test_both_dbs!( + test_get_user_flags, + test_get_user_flags_postgres, + test_get_user_flags_sqlite +); + +async fn test_get_user_flags(db: &Arc) { + let user_1 = db + .create_user( + &format!("user1@example.com"), + false, + NewUserParams { + github_login: format!("user1"), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_2 = db + .create_user( + &format!("user2@example.com"), + false, + NewUserParams { + github_login: format!("user2"), + github_user_id: 2, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + const CHANNELS_ALPHA: &'static str = "channels-alpha"; + const NEW_SEARCH: &'static str = "new-search"; + + let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap(); + let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap(); + + db.add_user_flag(user_1, channels_flag).await.unwrap(); + db.add_user_flag(user_1, search_flag).await.unwrap(); + + db.add_user_flag(user_2, channels_flag).await.unwrap(); + + let mut user_1_flags = db.get_user_flags(user_1).await.unwrap(); + user_1_flags.sort(); + assert_eq!(user_1_flags, &[CHANNELS_ALPHA, NEW_SEARCH]); + + let mut user_2_flags = db.get_user_flags(user_2).await.unwrap(); + user_2_flags.sort(); + assert_eq!(user_2_flags, &[CHANNELS_ALPHA]); +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 18587c2ba8f590f3646a3a7de6d4121ffe35586d..6b44711c42f4a37eea15c437879650a7c269aad5 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2609,20 +2609,19 @@ async fn get_private_user_info( response: Response, session: Session, ) -> Result<()> { - let metrics_id = session - .db() - .await - .get_user_metrics_id(session.user_id) - .await?; - let user = session - .db() - .await + let db = session.db().await; + + let metrics_id = db.get_user_metrics_id(session.user_id).await?; + let user = db .get_user_by_id(session.user_id) .await? .ok_or_else(|| anyhow!("user not found"))?; + let flags = db.get_user_flags(session.user_id).await?; + response.send(proto::GetPrivateUserInfoResponse { metrics_id, staff: user.admin, + flags, })?; Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ce47830af22e8203f33aaa86f3f953a86065ad94..e0356ebb9ad4c5cb46ecf9f16ece6dc91d6ecffb 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1111,6 +1111,7 @@ message GetPrivateUserInfo {} message GetPrivateUserInfoResponse { string metrics_id = 1; bool staff = 2; + repeated string flags = 3; } // Entities diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index bc9dd6f80ba039bb705e3d1518c737ba56c969b9..d64cbae92993ec2b092fcebdcf48d20f2c7449d6 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 = 61; +pub const PROTOCOL_VERSION: u32 = 62; From a3b2c03b1720a9dbda35ff8084a850d8b4560cd8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 16:13:12 -0700 Subject: [PATCH 43/67] Fix bugs in autoscroll with 'fit' strategy * Scroll to the newest cursor if all cursors can't fit in the viewport. * Refuse to layout an editor less tall than one line height. Co-authored-by: Nathan --- crates/editor/src/editor_tests.rs | 68 ++++++++++++++++++++++++ crates/editor/src/element.rs | 7 +-- crates/editor/src/scroll/autoscroll.rs | 73 +++++++++++++++----------- 3 files changed, 111 insertions(+), 37 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a2a561402f904be26d7a3a8d316519fb60387a19..25a5d45282d580aab5a93bf8ad3750250486e42a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1434,6 +1434,74 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_autoscroll(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.update_editor(|editor, cx| { + editor.set_vertical_scroll_margin(2, cx); + editor.style(cx).text.line_height(cx.font_cache()) + }); + + let window = cx.window; + window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); + + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); + }); + + // Add a cursor below the visible area. Since both cursors cannot fit + // on screen, the editor autoscrolls to reveal the newest cursor, and + // allows the vertical scroll margin below that cursor. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); + }); + + // Move down. The editor cursor scrolls down to track the newest cursor. + cx.update_editor(|editor, cx| { + editor.move_down(&Default::default(), cx); + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); + }); + + // Add a cursor above the visible area. Since both cursors fit on screen, + // the editor scrolls to show both. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(1, 0)..Point::new(1, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); + }); +} + #[gpui::test] async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3ba807308c6718e2fcbf7e8edf0f9c9f9d08824e..1f77979adb90c164c0305ff96cd95693aff50f32 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2113,14 +2113,11 @@ impl Element for EditorElement { scroll_height .min(constraint.max_along(Axis::Vertical)) .max(constraint.min_along(Axis::Vertical)) + .max(line_height) .min(line_height * max_lines as f32), ) } else if let EditorMode::SingleLine = snapshot.mode { - size.set_y( - line_height - .min(constraint.max_along(Axis::Vertical)) - .max(constraint.min_along(Axis::Vertical)), - ) + size.set_y(line_height.max(constraint.min_along(Axis::Vertical))) } else if size.y().is_infinite() { size.set_y(scroll_height); } diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index e83e2286b1f4809d777c72257eda0e7471508ccf..ffada50179fa233b12e4a02b4fed6e52bcf137ca 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -65,47 +65,52 @@ impl Editor { self.set_scroll_position(scroll_position, cx); } - let (autoscroll, local) = - if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() { - autoscroll - } else { - return false; - }; - - let first_cursor_top; - let last_cursor_bottom; + let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { + return false; + }; + + let mut target_top; + let mut target_bottom; if let Some(highlighted_rows) = &self.highlighted_rows { - first_cursor_top = highlighted_rows.start as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else if autoscroll == Autoscroll::newest() { - let newest_selection = self.selections.newest::(cx); - first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; - last_cursor_bottom = first_cursor_top + 1.; + target_top = highlighted_rows.start as f32; + target_bottom = target_top + 1.; } else { let selections = self.selections.all::(cx); - first_cursor_top = selections + target_top = selections .first() .unwrap() .head() .to_display_point(&display_map) .row() as f32; - last_cursor_bottom = selections + target_bottom = selections .last() .unwrap() .head() .to_display_point(&display_map) .row() as f32 + 1.0; + + // If the selections can't all fit on screen, scroll to the newest. + if autoscroll == Autoscroll::newest() + || autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines + { + let newest_selection_top = selections + .iter() + .max_by_key(|s| s.id) + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32; + target_top = newest_selection_top; + target_bottom = newest_selection_top + 1.; + } } let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { 0. } else { - ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() + ((visible_lines - (target_bottom - target_top)) / 2.0).floor() }; - if margin < 0.0 { - return false; - } let strategy = match autoscroll { Autoscroll::Strategy(strategy) => strategy, @@ -113,8 +118,8 @@ impl Editor { let last_autoscroll = &self.scroll_manager.last_autoscroll; if let Some(last_autoscroll) = last_autoscroll { if self.scroll_manager.anchor.offset == last_autoscroll.0 - && first_cursor_top == last_autoscroll.1 - && last_cursor_bottom == last_autoscroll.2 + && target_top == last_autoscroll.1 + && target_bottom == last_autoscroll.2 { last_autoscroll.3.next() } else { @@ -129,37 +134,41 @@ impl Editor { match strategy { AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); - let target_top = (first_cursor_top - margin).max(0.0); - let target_bottom = last_cursor_bottom + margin; + let target_top = (target_top - margin).max(0.0); + let target_bottom = target_bottom + margin; let start_row = scroll_position.y(); let end_row = start_row + visible_lines; - if target_top < start_row { + let needs_scroll_up = target_top < start_row; + let needs_scroll_down = target_bottom >= end_row; + + if needs_scroll_up && !needs_scroll_down { scroll_position.set_y(target_top); self.set_scroll_position_internal(scroll_position, local, true, cx); - } else if target_bottom >= end_row { + } + if !needs_scroll_up && needs_scroll_down { scroll_position.set_y(target_bottom - visible_lines); self.set_scroll_position_internal(scroll_position, local, true, cx); } } AutoscrollStrategy::Center => { - scroll_position.set_y((first_cursor_top - margin).max(0.0)); + scroll_position.set_y((target_top - margin).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } AutoscrollStrategy::Top => { - scroll_position.set_y((first_cursor_top).max(0.0)); + scroll_position.set_y((target_top).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } AutoscrollStrategy::Bottom => { - scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0)); + scroll_position.set_y((target_bottom - visible_lines).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } } self.scroll_manager.last_autoscroll = Some(( self.scroll_manager.anchor.offset, - first_cursor_top, - last_cursor_bottom, + target_top, + target_bottom, strategy, )); From 2495d6581efcef080cc33eaa3928ee9d487dbc34 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 26 Aug 2023 01:31:52 +0200 Subject: [PATCH 44/67] Un serialize project search (#2857) This is the first batch of improvements to current project search. There are few things we can do better still, but I want to get this out in next Preview. Most of the slowness at this point seems to stem from updating UI too often. Release Notes: - Improved project search by making it report results sooner. --------- Co-authored-by: Julia Risley --- crates/collab/src/tests/integration_tests.rs | 21 +- .../src/tests/randomized_integration_tests.rs | 18 +- crates/editor/src/multi_buffer.rs | 96 ++- crates/project/src/project.rs | 583 ++++++++++++------ crates/project/src/project_tests.rs | 11 +- crates/search/src/project_search.rs | 62 +- 6 files changed, 490 insertions(+), 301 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 9bee8d434cd9ccb6d0fa252e2badc49be99a54d4..b1227b9501a4990b9afb68aa72d22efd355defd7 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4,7 +4,7 @@ use crate::{ }; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, @@ -4821,15 +4821,16 @@ async fn test_project_search( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Perform a search as the guest. - let results = project_b - .update(cx_b, |project, cx| { - project.search( - SearchQuery::text("world", false, false, Vec::new(), Vec::new()), - cx, - ) - }) - .await - .unwrap(); + let mut results = HashMap::default(); + let mut search_rx = project_b.update(cx_b, |project, cx| { + project.search( + SearchQuery::text("world", false, false, Vec::new(), Vec::new()), + cx, + ) + }); + while let Some((buffer, ranges)) = search_rx.next().await { + results.entry(buffer).or_insert(ranges); + } let mut ranges_by_path = results .into_iter() diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 3557843828cacb81b69b88d24514da6936b83139..814f248b6dc722fa67f2af2dd70c66f54af3a57a 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -6,7 +6,7 @@ use crate::{ use anyhow::{anyhow, Result}; use call::ActiveCall; use client::RECEIVE_TIMEOUT; -use collections::BTreeMap; +use collections::{BTreeMap, HashMap}; use editor::Bias; use fs::{repository::GitFileStatus, FakeFs, Fs as _}; use futures::StreamExt as _; @@ -722,7 +722,7 @@ async fn apply_client_operation( if detach { "detaching" } else { "awaiting" } ); - let search = project.update(cx, |project, cx| { + let mut search = project.update(cx, |project, cx| { project.search( SearchQuery::text(query, false, false, Vec::new(), Vec::new()), cx, @@ -730,15 +730,13 @@ async fn apply_client_operation( }); drop(project); let search = cx.background().spawn(async move { - search - .await - .map_err(|err| anyhow!("search request failed: {:?}", err)) + let mut results = HashMap::default(); + while let Some((buffer, ranges)) = search.next().await { + results.entry(buffer).or_insert(ranges); + } + results }); - if detach { - cx.update(|cx| search.detach_and_log_err(cx)); - } else { - search.await?; - } + search.await; } ClientOperation::WriteFsEntry { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index b9bf4f52a1c50e3b31aaf91da6586b9cfea7c958..5c0d8b641cac5731508beae589a499727aac0dd8 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -6,7 +6,7 @@ use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; use git::diff::DiffHunk; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{AppContext, Entity, ModelContext, ModelHandle}; pub use language::Completion; use language::{ char_kind, @@ -788,59 +788,59 @@ impl MultiBuffer { pub fn stream_excerpts_with_context_lines( &mut self, - excerpts: Vec<(ModelHandle, Vec>)>, + buffer: ModelHandle, + ranges: Vec>, context_line_count: u32, cx: &mut ModelContext, - ) -> (Task<()>, mpsc::Receiver>) { + ) -> mpsc::Receiver> { let (mut tx, rx) = mpsc::channel(256); - let task = cx.spawn(|this, mut cx| async move { - for (buffer, ranges) in excerpts { - let (buffer_id, buffer_snapshot) = - buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); - - let mut excerpt_ranges = Vec::new(); - let mut range_counts = Vec::new(); - cx.background() - .scoped(|scope| { - scope.spawn(async { - let (ranges, counts) = - build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); - excerpt_ranges = ranges; - range_counts = counts; - }); - }) - .await; - - let mut ranges = ranges.into_iter(); - let mut range_counts = range_counts.into_iter(); - for excerpt_ranges in excerpt_ranges.chunks(100) { - let excerpt_ids = this.update(&mut cx, |this, cx| { - this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) + cx.spawn(|this, mut cx| async move { + let (buffer_id, buffer_snapshot) = + buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); + + let mut excerpt_ranges = Vec::new(); + let mut range_counts = Vec::new(); + cx.background() + .scoped(|scope| { + scope.spawn(async { + let (ranges, counts) = + build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); + excerpt_ranges = ranges; + range_counts = counts; }); + }) + .await; - for (excerpt_id, range_count) in - excerpt_ids.into_iter().zip(range_counts.by_ref()) - { - for range in ranges.by_ref().take(range_count) { - let start = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.start, - }; - let end = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.end, - }; - if tx.send(start..end).await.is_err() { - break; - } + let mut ranges = ranges.into_iter(); + let mut range_counts = range_counts.into_iter(); + for excerpt_ranges in excerpt_ranges.chunks(100) { + let excerpt_ids = this.update(&mut cx, |this, cx| { + this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) + }); + + for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.by_ref()) + { + for range in ranges.by_ref().take(range_count) { + let start = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.start, + }; + let end = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.end, + }; + if tx.send(start..end).await.is_err() { + break; } } } } - }); - (task, rx) + }) + .detach(); + + rx } pub fn push_excerpts( @@ -4438,7 +4438,7 @@ mod tests { async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - let (task, anchor_ranges) = multibuffer.update(cx, |multibuffer, cx| { + let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx); let ranges = vec![ snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)), @@ -4446,12 +4446,10 @@ mod tests { snapshot.anchor_before(Point::new(15, 0)) ..snapshot.anchor_before(Point::new(15, 0)), ]; - multibuffer.stream_excerpts_with_context_lines(vec![(buffer.clone(), ranges)], 2, cx) + multibuffer.stream_excerpts_with_context_lines(buffer.clone(), ranges, 2, cx) }); let anchor_ranges = anchor_ranges.collect::>().await; - // Ensure task is finished when stream completes. - task.await; let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); assert_eq!( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c7765bf55a70b4829cf43575b7679db94ff44065..bb18a41ad42a58cd8271519cdfd074b93603ce25 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,8 +26,8 @@ use futures::{ }; use globset::{Glob, GlobSet, GlobSetBuilder}; use gpui::{ - AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext, - ModelHandle, Task, WeakModelHandle, + executor::Background, AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, + ModelContext, ModelHandle, Task, WeakModelHandle, }; use itertools::Itertools; use language::{ @@ -37,11 +37,11 @@ use language::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, }, - range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel, - Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _, - Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt, - Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, - ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction, + CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, + File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, + OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, + ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -57,8 +57,8 @@ use serde::Serialize; use settings::SettingsStore; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; +use smol::channel::{Receiver, Sender}; use std::{ - cell::RefCell, cmp::{self, Ordering}, convert::TryInto, hash::Hash, @@ -67,7 +67,6 @@ use std::{ ops::Range, path::{self, Component, Path, PathBuf}, process::Stdio, - rc::Rc, str, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -525,6 +524,28 @@ impl FormatTrigger { } } } +#[derive(Clone, Debug, PartialEq)] +enum SearchMatchCandidate { + OpenBuffer { + buffer: ModelHandle, + // This might be an unnamed file without representation on filesystem + path: Option>, + }, + Path { + worktree_id: WorktreeId, + path: Arc, + }, +} + +type SearchMatchCandidateIndex = usize; +impl SearchMatchCandidate { + fn path(&self) -> Option> { + match self { + SearchMatchCandidate::OpenBuffer { path, .. } => path.clone(), + SearchMatchCandidate::Path { path, .. } => Some(path.clone()), + } + } +} impl Project { pub fn init_settings(cx: &mut AppContext) { @@ -5099,187 +5120,11 @@ impl Project { &self, query: SearchQuery, cx: &mut ModelContext, - ) -> Task, Vec>>>> { + ) -> Receiver<(ModelHandle, Vec>)> { if self.is_local() { - let snapshots = self - .visible_worktrees(cx) - .filter_map(|tree| { - let tree = tree.read(cx).as_local()?; - Some(tree.snapshot()) - }) - .collect::>(); - - let background = cx.background().clone(); - let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); - if path_count == 0 { - return Task::ready(Ok(Default::default())); - } - let workers = background.num_cpus().min(path_count); - let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024); - cx.background() - .spawn({ - let fs = self.fs.clone(); - let background = cx.background().clone(); - let query = query.clone(); - async move { - let fs = &fs; - let query = &query; - let matching_paths_tx = &matching_paths_tx; - let paths_per_worker = (path_count + workers - 1) / workers; - let snapshots = &snapshots; - background - .scoped(|scope| { - for worker_ix in 0..workers { - let worker_start_ix = worker_ix * paths_per_worker; - let worker_end_ix = worker_start_ix + paths_per_worker; - scope.spawn(async move { - let mut snapshot_start_ix = 0; - let mut abs_path = PathBuf::new(); - for snapshot in snapshots { - let snapshot_end_ix = - snapshot_start_ix + snapshot.visible_file_count(); - if worker_end_ix <= snapshot_start_ix { - break; - } else if worker_start_ix > snapshot_end_ix { - snapshot_start_ix = snapshot_end_ix; - continue; - } else { - let start_in_snapshot = worker_start_ix - .saturating_sub(snapshot_start_ix); - let end_in_snapshot = - cmp::min(worker_end_ix, snapshot_end_ix) - - snapshot_start_ix; - - for entry in snapshot - .files(false, start_in_snapshot) - .take(end_in_snapshot - start_in_snapshot) - { - if matching_paths_tx.is_closed() { - break; - } - let matches = if query - .file_matches(Some(&entry.path)) - { - abs_path.clear(); - abs_path.push(&snapshot.abs_path()); - abs_path.push(&entry.path); - if let Some(file) = - fs.open_sync(&abs_path).await.log_err() - { - query.detect(file).unwrap_or(false) - } else { - false - } - } else { - false - }; - - if matches { - let project_path = - (snapshot.id(), entry.path.clone()); - if matching_paths_tx - .send(project_path) - .await - .is_err() - { - break; - } - } - } - - snapshot_start_ix = snapshot_end_ix; - } - } - }); - } - }) - .await; - } - }) - .detach(); - - let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); - let open_buffers = self - .opened_buffers - .values() - .filter_map(|b| b.upgrade(cx)) - .collect::>(); - cx.spawn(|this, cx| async move { - for buffer in &open_buffers { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer.clone(), snapshot)).await?; - } - - let open_buffers = Rc::new(RefCell::new(open_buffers)); - while let Some(project_path) = matching_paths_rx.next().await { - if buffers_tx.is_closed() { - break; - } - - let this = this.clone(); - let open_buffers = open_buffers.clone(); - let buffers_tx = buffers_tx.clone(); - cx.spawn(|mut cx| async move { - if let Some(buffer) = this - .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) - .await - .log_err() - { - if open_buffers.borrow_mut().insert(buffer.clone()) { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer, snapshot)).await?; - } - } - - Ok::<_, anyhow::Error>(()) - }) - .detach(); - } - - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); - - let background = cx.background().clone(); - cx.background().spawn(async move { - let query = &query; - let mut matched_buffers = Vec::new(); - for _ in 0..workers { - matched_buffers.push(HashMap::default()); - } - background - .scoped(|scope| { - for worker_matched_buffers in matched_buffers.iter_mut() { - let mut buffers_rx = buffers_rx.clone(); - scope.spawn(async move { - while let Some((buffer, snapshot)) = buffers_rx.next().await { - let buffer_matches = if query.file_matches( - snapshot.file().map(|file| file.path().as_ref()), - ) { - query - .search(&snapshot, None) - .await - .iter() - .map(|range| { - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end) - }) - .collect() - } else { - Vec::new() - }; - if !buffer_matches.is_empty() { - worker_matched_buffers - .insert(buffer.clone(), buffer_matches); - } - } - }); - } - }) - .await; - Ok(matched_buffers.into_iter().flatten().collect()) - }) + self.search_local(query, cx) } else if let Some(project_id) = self.remote_id() { + let (tx, rx) = smol::channel::unbounded(); let request = self.client.request(query.to_proto(project_id)); cx.spawn(|this, mut cx| async move { let response = request.await?; @@ -5303,13 +5148,303 @@ impl Project { .or_insert(Vec::new()) .push(start..end) } - Ok(result) + for (buffer, ranges) in result { + let _ = tx.send((buffer, ranges)).await; + } + Result::<(), anyhow::Error>::Ok(()) }) + .detach_and_log_err(cx); + rx } else { - Task::ready(Ok(Default::default())) + unimplemented!(); } } + pub fn search_local( + &self, + query: SearchQuery, + cx: &mut ModelContext, + ) -> Receiver<(ModelHandle, Vec>)> { + // Local search is split into several phases. + // TL;DR is that we do 2 passes; initial pass to pick files which contain at least one match + // and the second phase that finds positions of all the matches found in the candidate files. + // The Receiver obtained from this function returns matches sorted by buffer path. Files without a buffer path are reported first. + // + // It gets a bit hairy though, because we must account for files that do not have a persistent representation + // on FS. Namely, if you have an untitled buffer or unsaved changes in a buffer, we want to scan that too. + // + // 1. We initialize a queue of match candidates and feed all opened buffers into it (== unsaved files / untitled buffers). + // Then, we go through a worktree and check for files that do match a predicate. If the file had an opened version, we skip the scan + // of FS version for that file altogether - after all, what we have in memory is more up-to-date than what's in FS. + // 2. At this point, we have a list of all potentially matching buffers/files. + // We sort that list by buffer path - this list is retained for later use. + // We ensure that all buffers are now opened and available in project. + // 3. We run a scan over all the candidate buffers on multiple background threads. + // We cannot assume that there will even be a match - while at least one match + // is guaranteed for files obtained from FS, the buffers we got from memory (unsaved files/unnamed buffers) might not have a match at all. + // There is also an auxilliary background thread responsible for result gathering. + // This is where the sorted list of buffers comes into play to maintain sorted order; Whenever this background thread receives a notification (buffer has/doesn't have matches), + // it keeps it around. It reports matches in sorted order, though it accepts them in unsorted order as well. + // As soon as the match info on next position in sorted order becomes available, it reports it (if it's a match) or skips to the next + // entry - which might already be available thanks to out-of-order processing. + // + // We could also report matches fully out-of-order, without maintaining a sorted list of matching paths. + // This however would mean that project search (that is the main user of this function) would have to do the sorting itself, on the go. + // This isn't as straightforward as running an insertion sort sadly, and would also mean that it would have to care about maintaining match index + // in face of constantly updating list of sorted matches. + // Meanwhile, this implementation offers index stability, since the matches are already reported in a sorted order. + let snapshots = self + .visible_worktrees(cx) + .filter_map(|tree| { + let tree = tree.read(cx).as_local()?; + Some(tree.snapshot()) + }) + .collect::>(); + + let background = cx.background().clone(); + let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); + if path_count == 0 { + let (_, rx) = smol::channel::bounded(1024); + return rx; + } + let workers = background.num_cpus().min(path_count); + let (matching_paths_tx, matching_paths_rx) = smol::channel::bounded(1024); + let mut unnamed_files = vec![]; + let opened_buffers = self + .opened_buffers + .iter() + .filter_map(|(_, b)| { + let buffer = b.upgrade(cx)?; + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + if let Some(path) = snapshot.file().map(|file| file.path()) { + Some((path.clone(), (buffer, snapshot))) + } else { + unnamed_files.push(buffer); + None + } + }) + .collect(); + cx.background() + .spawn(Self::background_search( + unnamed_files, + opened_buffers, + cx.background().clone(), + self.fs.clone(), + workers, + query.clone(), + path_count, + snapshots, + matching_paths_tx, + )) + .detach(); + + let (buffers, buffers_rx) = Self::sort_candidates_and_open_buffers(matching_paths_rx, cx); + let background = cx.background().clone(); + let (result_tx, result_rx) = smol::channel::bounded(1024); + cx.background() + .spawn(async move { + let Ok(buffers) = buffers.await else { + return; + }; + + let buffers_len = buffers.len(); + if buffers_len == 0 { + return; + } + let query = &query; + let (finished_tx, mut finished_rx) = smol::channel::unbounded(); + background + .scoped(|scope| { + #[derive(Clone)] + struct FinishedStatus { + entry: Option<(ModelHandle, Vec>)>, + buffer_index: SearchMatchCandidateIndex, + } + + for _ in 0..workers { + let finished_tx = finished_tx.clone(); + let mut buffers_rx = buffers_rx.clone(); + scope.spawn(async move { + while let Some((entry, buffer_index)) = buffers_rx.next().await { + let buffer_matches = if let Some((_, snapshot)) = entry.as_ref() + { + if query.file_matches( + snapshot.file().map(|file| file.path().as_ref()), + ) { + query + .search(&snapshot, None) + .await + .iter() + .map(|range| { + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .collect() + } else { + Vec::new() + } + } else { + Vec::new() + }; + + let status = if !buffer_matches.is_empty() { + let entry = if let Some((buffer, _)) = entry.as_ref() { + Some((buffer.clone(), buffer_matches)) + } else { + None + }; + FinishedStatus { + entry, + buffer_index, + } + } else { + FinishedStatus { + entry: None, + buffer_index, + } + }; + if finished_tx.send(status).await.is_err() { + break; + } + } + }); + } + // Report sorted matches + scope.spawn(async move { + let mut current_index = 0; + let mut scratch = vec![None; buffers_len]; + while let Some(status) = finished_rx.next().await { + debug_assert!( + scratch[status.buffer_index].is_none(), + "Got match status of position {} twice", + status.buffer_index + ); + let index = status.buffer_index; + scratch[index] = Some(status); + while current_index < buffers_len { + let Some(current_entry) = scratch[current_index].take() else { + // We intentionally **do not** increment `current_index` here. When next element arrives + // from `finished_rx`, we will inspect the same position again, hoping for it to be Some(_) + // this time. + break; + }; + if let Some(entry) = current_entry.entry { + result_tx.send(entry).await.log_err(); + } + current_index += 1; + } + if current_index == buffers_len { + break; + } + } + }); + }) + .await; + }) + .detach(); + result_rx + } + /// Pick paths that might potentially contain a match of a given search query. + async fn background_search( + unnamed_buffers: Vec>, + opened_buffers: HashMap, (ModelHandle, BufferSnapshot)>, + background: Arc, + fs: Arc, + workers: usize, + query: SearchQuery, + path_count: usize, + snapshots: Vec, + matching_paths_tx: Sender, + ) { + let fs = &fs; + let query = &query; + let matching_paths_tx = &matching_paths_tx; + let snapshots = &snapshots; + let paths_per_worker = (path_count + workers - 1) / workers; + for buffer in unnamed_buffers { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: None, + }) + .await + .log_err(); + } + for (path, (buffer, _)) in opened_buffers.iter() { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: Some(path.clone()), + }) + .await + .log_err(); + } + background + .scoped(|scope| { + for worker_ix in 0..workers { + let worker_start_ix = worker_ix * paths_per_worker; + let worker_end_ix = worker_start_ix + paths_per_worker; + let unnamed_buffers = opened_buffers.clone(); + scope.spawn(async move { + let mut snapshot_start_ix = 0; + let mut abs_path = PathBuf::new(); + for snapshot in snapshots { + let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count(); + if worker_end_ix <= snapshot_start_ix { + break; + } else if worker_start_ix > snapshot_end_ix { + snapshot_start_ix = snapshot_end_ix; + continue; + } else { + let start_in_snapshot = + worker_start_ix.saturating_sub(snapshot_start_ix); + let end_in_snapshot = + cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix; + + for entry in snapshot + .files(false, start_in_snapshot) + .take(end_in_snapshot - start_in_snapshot) + { + if matching_paths_tx.is_closed() { + break; + } + if unnamed_buffers.contains_key(&entry.path) { + continue; + } + let matches = if query.file_matches(Some(&entry.path)) { + abs_path.clear(); + abs_path.push(&snapshot.abs_path()); + abs_path.push(&entry.path); + if let Some(file) = fs.open_sync(&abs_path).await.log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + } + } else { + false + }; + + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }; + if matching_paths_tx.send(project_path).await.is_err() { + break; + } + } + } + + snapshot_start_ix = snapshot_end_ix; + } + } + }); + } + }) + .await; + } + // TODO: Wire this up to allow selecting a server? fn request_lsp( &self, @@ -5384,6 +5519,61 @@ impl Project { Task::ready(Ok(Default::default())) } + fn sort_candidates_and_open_buffers( + mut matching_paths_rx: Receiver, + cx: &mut ModelContext, + ) -> ( + futures::channel::oneshot::Receiver>, + Receiver<( + Option<(ModelHandle, BufferSnapshot)>, + SearchMatchCandidateIndex, + )>, + ) { + let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); + let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); + cx.spawn(|this, cx| async move { + let mut buffers = vec![]; + while let Some(entry) = matching_paths_rx.next().await { + buffers.push(entry); + } + buffers.sort_by_key(|candidate| candidate.path()); + let matching_paths = buffers.clone(); + let _ = sorted_buffers_tx.send(buffers); + for (index, candidate) in matching_paths.into_iter().enumerate() { + if buffers_tx.is_closed() { + break; + } + let this = this.clone(); + let buffers_tx = buffers_tx.clone(); + cx.spawn(|mut cx| async move { + let buffer = match candidate { + SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), + SearchMatchCandidate::Path { worktree_id, path } => this + .update(&mut cx, |this, cx| { + this.open_buffer((worktree_id, path), cx) + }) + .await + .log_err(), + }; + if let Some(buffer) = buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx + .send((Some((buffer, snapshot)), index)) + .await + .log_err(); + } else { + buffers_tx.send((None, index)).await.log_err(); + } + + Ok::<_, anyhow::Error>(()) + }) + .detach(); + } + }) + .detach(); + (sorted_buffers_rx, buffers_rx) + } + pub fn find_or_create_local_worktree( &mut self, abs_path: impl AsRef, @@ -7006,17 +7196,17 @@ impl Project { ) -> Result { let peer_id = envelope.original_sender_id()?; let query = SearchQuery::from_proto(envelope.payload)?; - let result = this - .update(&mut cx, |this, cx| this.search(query, cx)) - .await?; + let mut result = this.update(&mut cx, |this, cx| this.search(query, cx)); - this.update(&mut cx, |this, cx| { + cx.spawn(|mut cx| async move { let mut locations = Vec::new(); - for (buffer, ranges) in result { + while let Some((buffer, ranges)) = result.next().await { for range in ranges { let start = serialize_anchor(&range.start); let end = serialize_anchor(&range.end); - let buffer_id = this.create_buffer_for_peer(&buffer, peer_id, cx); + let buffer_id = this.update(&mut cx, |this, cx| { + this.create_buffer_for_peer(&buffer, peer_id, cx) + }); locations.push(proto::Location { buffer_id, start: Some(start), @@ -7026,6 +7216,7 @@ impl Project { } Ok(proto::SearchProjectResponse { locations }) }) + .await } async fn handle_open_buffer_for_symbol( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a504900c83e9178f9ee6cbcf6a66e77a8fa44c0d..7c5983a0a90de924384c37871720246dce5f6983 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3953,11 +3953,12 @@ async fn search( query: SearchQuery, cx: &mut gpui::TestAppContext, ) -> Result>>> { - let results = project - .update(cx, |project, cx| project.search(query, cx)) - .await?; - - Ok(results + let mut search_rx = project.update(cx, |project, cx| project.search(query, cx)); + let mut result = HashMap::default(); + while let Some((buffer, range)) = search_rx.next().await { + result.entry(buffer).or_insert(range); + } + Ok(result .into_iter() .map(|(buffer, ranges)| { buffer.read_with(cx, |buffer, _| { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d7633a45e49af1a90a86d6bdce2dba95d209d74b..6364183877b7e1f2bff8ea26b6244a0ed57c6be9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -185,28 +185,26 @@ impl ProjectSearch { self.active_query = Some(query); self.match_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { - let matches = search.await.log_err()?; + let mut matches = search; let this = this.upgrade(&cx)?; - let mut matches = matches.into_iter().collect::>(); - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { this.match_ranges.clear(); + this.excerpts.update(cx, |this, cx| this.clear(cx)); this.no_results = Some(true); - matches.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - excerpts.stream_excerpts_with_context_lines(matches, 1, cx) - }) }); - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } + while let Some((buffer, anchors)) = matches.next().await { + let mut ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); - cx.notify(); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx) + }) }); + + while let Some(range) = ranges.next().await { + this.update(&mut cx, |this, _| this.match_ranges.push(range)); + } + this.update(&mut cx, |_, cx| cx.notify()); } this.update(&mut cx, |this, cx| { @@ -238,29 +236,31 @@ impl ProjectSearch { self.no_results = Some(true); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let results = search?.await.log_err()?; + let matches = results + .into_iter() + .map(|result| (result.buffer, vec![result.range.start..result.range.start])); - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { this.excerpts.update(cx, |excerpts, cx| { excerpts.clear(cx); - - let matches = results - .into_iter() - .map(|result| (result.buffer, vec![result.range.start..result.range.start])) - .collect(); - - excerpts.stream_excerpts_with_context_lines(matches, 3, cx) }) }); - - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } + for (buffer, ranges) in matches { + let mut match_ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); - cx.notify(); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx) + }) }); + while let Some(match_range) = match_ranges.next().await { + this.update(&mut cx, |this, cx| { + this.match_ranges.push(match_range); + while let Ok(Some(match_range)) = match_ranges.try_next() { + this.match_ranges.push(match_range); + } + cx.notify(); + }); + } } this.update(&mut cx, |this, cx| { From ddd7ab116f981d2871f266d63721aaca5942fa15 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 00:05:39 +0300 Subject: [PATCH 45/67] Do not convert lsp::Location of hint labels before resolve --- crates/editor/src/link_go_to_definition.rs | 130 ++++++++----- crates/project/src/lsp_command.rs | 208 ++++++--------------- crates/project/src/project.rs | 29 ++- crates/rpc/proto/zed.proto | 5 +- 4 files changed, 163 insertions(+), 209 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 926c0d6ddeb588bf133d032e9492c2249e9711eb..8d46194f4226e527f5ff9da681e44957a05d7474 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -4,13 +4,15 @@ use crate::{ hover_popover::{self, InlayHover}, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; +use anyhow::Context; use gpui::{Task, ViewContext}; -use language::{Bias, ToOffset}; +use language::{point_from_lsp, Bias, LanguageServerName, ToOffset}; +use lsp::LanguageServerId; use project::{ HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, ResolveState, }; -use std::ops::Range; +use std::{ops::Range, sync::Arc}; use util::TryFutureExt; #[derive(Debug, Default)] @@ -24,7 +26,7 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(InlayRange, LocationLink), + InlayHint(InlayRange, lsp::Location, LanguageServerId), None, } @@ -38,7 +40,7 @@ pub struct InlayRange { #[derive(Debug, Clone)] pub enum TriggerPoint { Text(Anchor), - InlayHint(InlayRange, LocationLink), + InlayHint(InlayRange, lsp::Location, LanguageServerId), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -61,12 +63,12 @@ impl DocumentRange { let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } - (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _, _)) => { range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } (DocumentRange::Inlay(_), TriggerPoint::Text(_)) - | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _, _)) => false, } } } @@ -75,7 +77,7 @@ impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(coordinates, _) => &coordinates.inlay_position, + TriggerPoint::InlayHint(coordinates, _, _) => &coordinates.inlay_position, } } @@ -88,7 +90,7 @@ impl TriggerPoint { LinkDefinitionKind::Symbol } } - TriggerPoint::InlayHint(_, _) => LinkDefinitionKind::Type, + TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type, } } } @@ -110,7 +112,9 @@ pub fn update_go_to_definition_link( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id) => { + Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id)) + } GoToDefinitionTrigger::None => None, }; @@ -277,35 +281,25 @@ pub fn update_inlay_link_and_hover_points( ); hover_updated = true; } - if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = - cached_hint.position.buffer_id.and_then(|buffer_id| { - editor.buffer().read(cx).buffer(buffer_id) - }) - { - go_to_definition_updated = true; - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::InlayHint( - InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - LocationLink { - origin: Some(Location { - buffer, - range: cached_hint.position - ..cached_hint.position, - }), - target: location, - }, - ), - cmd_held, - shift_held, - cx, - ); - } + if let Some((language_server_id, location)) = + hovered_hint_part.location + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + location, + language_server_id, + ), + cmd_held, + shift_held, + cx, + ); } } } @@ -415,7 +409,6 @@ pub fn show_link_definition( let end = snapshot .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - DocumentRange::Text(start..end) }) }), @@ -423,10 +416,59 @@ pub fn show_link_definition( ) }) } - TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( - Some(DocumentRange::Inlay(trigger_source.clone())), - vec![trigger_target.clone()], - )), + TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => { + let target = match project.update(&mut cx, |project, cx| { + let language_server_name = project + .language_server_for_buffer(buffer.read(cx), *server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + *server_id, + language_server_name, + cx, + ) + }) + }) { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }); + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + + target.map(|target| { + ( + Some(DocumentRange::Inlay(trigger_source.clone())), + vec![LocationLink { + origin: Some(Location { + buffer: buffer.clone(), + range: trigger_source.inlay_position.text_anchor + ..trigger_source.inlay_position.text_anchor, + }), + target, + }], + ) + }) + } }; this.update(&mut cx, |this, cx| { @@ -479,7 +521,7 @@ pub fn show_link_definition( ..snapshot.anchor_after(offset_range.end), ) } - TriggerPoint::InlayHint(inlay_coordinates, _) => { + TriggerPoint::InlayHint(inlay_coordinates, _, _) => { DocumentRange::Inlay(inlay_coordinates) } }); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8ca2571869f4575262cd9453015dec5ef9dfcd7d..8239cf869067043d25549c5cfa74337e5211271d 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,6 +1,6 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, - InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Item, Location, LocationLink, + InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; @@ -14,8 +14,8 @@ use language::{ point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, - CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, - Transaction, Unclipped, + CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1787,7 +1787,6 @@ impl LspCommand for OnTypeFormatting { impl InlayHints { pub async fn lsp_to_project_hint( lsp_hint: lsp::InlayHint, - project: &ModelHandle, buffer_handle: &ModelHandle, server_id: LanguageServerId, resolve_state: ResolveState, @@ -1809,15 +1808,9 @@ impl InlayHints { buffer.anchor_after(position) } }); - let label = Self::lsp_inlay_label_to_project( - &buffer_handle, - project, - server_id, - lsp_hint.label, - cx, - ) - .await - .context("lsp to project inlay hint conversion")?; + let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id) + .await + .context("lsp to project inlay hint conversion")?; let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { false } else { @@ -1847,72 +1840,14 @@ impl InlayHints { } async fn lsp_inlay_label_to_project( - buffer: &ModelHandle, - project: &ModelHandle, - server_id: LanguageServerId, lsp_label: lsp::InlayHintLabel, - cx: &mut AsyncAppContext, + server_id: LanguageServerId, ) -> anyhow::Result { let label = match lsp_label { lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), lsp::InlayHintLabel::LabelParts(lsp_parts) => { - let mut parts_data = Vec::with_capacity(lsp_parts.len()); - buffer.update(cx, |buffer, cx| { - for lsp_part in lsp_parts { - let location_buffer_task = match &lsp_part.location { - Some(lsp_location) => { - let location_buffer_task = project.update(cx, |project, cx| { - let language_server_name = project - .language_server_for_buffer(buffer, server_id, cx) - .map(|(_, lsp_adapter)| { - LanguageServerName(Arc::from(lsp_adapter.name())) - }); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - server_id, - language_server_name, - cx, - ) - }) - }); - Some(lsp_location.clone()).zip(location_buffer_task) - } - None => None, - }; - - parts_data.push((lsp_part, location_buffer_task)); - } - }); - - let mut parts = Vec::with_capacity(parts_data.len()); - for (lsp_part, location_buffer_task) in parts_data { - let location = match location_buffer_task { - Some((lsp_location, target_buffer_handle_task)) => { - let target_buffer_handle = target_buffer_handle_task - .await - .context("resolving location for label part buffer")?; - let range = cx.read(|cx| { - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - }); - Some(Location { - buffer: target_buffer_handle, - range, - }) - } - None => None, - }; - + let mut parts = Vec::with_capacity(lsp_parts.len()); + for lsp_part in lsp_parts { parts.push(InlayHintLabelPart { value: lsp_part.value, tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { @@ -1929,7 +1864,7 @@ impl InlayHints { }) } }), - location, + location: Some(server_id).zip(lsp_part.location), }); } InlayHintLabel::LabelParts(parts) @@ -1939,7 +1874,7 @@ impl InlayHints { Ok(label) } - pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { + pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint { let (state, lsp_resolve_state) = match response_hint.resolve_state { ResolveState::Resolved => (0, None), ResolveState::CanResolve(server_id, resolve_data) => ( @@ -1969,7 +1904,11 @@ impl InlayHints { InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), InlayHintLabel::LabelParts(label_parts) => { proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + parts: label_parts.into_iter().map(|label_part| { + let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string()); + let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + proto::InlayHintLabelPart { value: label_part.value, tooltip: label_part.tooltip.map(|tooltip| { let proto_tooltip = match tooltip { @@ -1981,12 +1920,11 @@ impl InlayHints { }; proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() + location_url, + location_range_start, + location_range_end, + language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64), + }}).collect() }) } }), @@ -1994,16 +1932,12 @@ impl InlayHints { kind: response_hint.kind.map(|kind| kind.name().to_string()), tooltip: response_hint.tooltip.map(|response_tooltip| { let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } + InlayHintTooltip::String(s) => proto::inlay_hint_tooltip::Content::Value(s), InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - is_markdown: markup_content.kind == HoverBlockKind::Markdown, - value: markup_content.value, - }, - ) + proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }) } }; proto::InlayHintTooltip { @@ -2014,16 +1948,7 @@ impl InlayHints { } } - pub async fn proto_to_project_hint( - message_hint: proto::InlayHint, - project: &ModelHandle, - cx: &mut AsyncAppContext, - ) -> anyhow::Result { - let buffer_id = message_hint - .position - .as_ref() - .and_then(|location| location.buffer_id) - .context("missing buffer id")?; + pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result { let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) }); @@ -2064,9 +1989,6 @@ impl InlayHints { proto::inlay_hint_label::Label::LabelParts(parts) => { let mut label_parts = Vec::new(); for part in parts.parts { - let buffer = project - .update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx)) - .await?; label_parts.push(InlayHintLabelPart { value: part.value, tooltip: part.tooltip.map(|tooltip| match tooltip.content { @@ -2087,19 +2009,35 @@ impl InlayHints { }), None => InlayHintLabelPartTooltip::String(String::new()), }), - location: match part.location { - Some(location) => Some(Location { - range: location - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")? - ..location - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?, - buffer, - }), - None => None, + location: { + match part + .location_url + .zip( + part.location_range_start.and_then(|start| { + Some(start..part.location_range_end?) + }), + ) + .zip(part.language_server_id) + { + Some(((uri, range), server_id)) => Some(( + LanguageServerId(server_id as usize), + lsp::Location { + uri: lsp::Url::parse(&uri) + .context("invalid uri in hint part {part:?}")?, + range: lsp::Range::new( + point_to_lsp(PointUtf16::new( + range.start.row, + range.start.column, + )), + point_to_lsp(PointUtf16::new( + range.end.row, + range.end.column, + )), + ), + }, + )), + None => None, + } }, }); } @@ -2132,12 +2070,7 @@ impl InlayHints { }) } - pub fn project_to_lsp_hint( - hint: InlayHint, - project: &ModelHandle, - snapshot: &BufferSnapshot, - cx: &AsyncAppContext, - ) -> lsp::InlayHint { + pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp::InlayHint { lsp::InlayHint { position: point_to_lsp(hint.position.to_point_utf16(snapshot)), kind: hint.kind.map(|kind| match kind { @@ -2190,22 +2123,7 @@ impl InlayHints { } }) }), - location: part.location.and_then(|location| { - let (path, location_snapshot) = cx.read(|cx| { - let buffer = location.buffer.read(cx); - let project_path = buffer.project_path(cx)?; - let location_snapshot = buffer.snapshot(); - let path = project.read(cx).absolute_path(&project_path, cx); - path.zip(Some(location_snapshot)) - })?; - Some(lsp::Location::new( - lsp::Url::from_file_path(path).unwrap(), - range_to_lsp( - location.range.start.to_point_utf16(&location_snapshot) - ..location.range.end.to_point_utf16(&location_snapshot), - ), - )) - }), + location: part.location.map(|(_, location)| location), command: None, }) .collect(), @@ -2299,12 +2217,10 @@ impl LspCommand for InlayHints { ResolveState::Resolved }; - let project = project.clone(); let buffer = buffer.clone(); cx.spawn(|mut cx| async move { InlayHints::lsp_to_project_hint( lsp_hint, - &project, &buffer, server_id, resolve_state, @@ -2359,12 +2275,12 @@ impl LspCommand for InlayHints { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - cx: &mut AppContext, + _: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| InlayHints::project_to_proto_hint(response_hint, cx)) + .map(|response_hint| InlayHints::project_to_proto_hint(response_hint)) .collect(), version: serialize_version(buffer_version), } @@ -2373,7 +2289,7 @@ impl LspCommand for InlayHints { async fn response_from_proto( self, message: proto::InlayHintsResponse, - project: ModelHandle, + _: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, ) -> anyhow::Result> { @@ -2385,7 +2301,7 @@ impl LspCommand for InlayHints { let mut hints = Vec::new(); for message_hint in message.hints { - hints.push(InlayHints::proto_to_project_hint(message_hint, &project, &mut cx).await?); + hints.push(InlayHints::proto_to_project_hint(message_hint)?); } Ok(hints) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index bb18a41ad42a58cd8271519cdfd074b93603ce25..547c7dcc959b32423699b250d03ec13991da03fb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -369,7 +369,7 @@ pub enum InlayHintLabel { pub struct InlayHintLabelPart { pub value: String, pub tooltip: Option, - pub location: Option, + pub location: Option<(LanguageServerId, lsp::Location)>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1708,7 +1708,7 @@ impl Project { } /// LanguageServerName is owned, because it is inserted into a map - fn open_local_buffer_via_lsp( + pub fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, language_server_id: LanguageServerId, @@ -5069,16 +5069,15 @@ impl Project { } let buffer_snapshot = buffer.snapshot(); - cx.spawn(|project, mut cx| async move { + cx.spawn(|_, mut cx| async move { let resolve_task = lang_server.request::( - InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), + InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), ); let resolved_hint = resolve_task .await .context("inlay hint resolve LSP request")?; let resolved_hint = InlayHints::lsp_to_project_hint( resolved_hint, - &project, &buffer_handle, server_id, ResolveState::Resolved, @@ -5094,19 +5093,16 @@ impl Project { project_id, buffer_id: buffer_handle.read(cx).remote_id(), language_server_id: server_id.0 as u64, - hint: Some(InlayHints::project_to_proto_hint(hint.clone(), cx)), + hint: Some(InlayHints::project_to_proto_hint(hint.clone())), }; - cx.spawn(|project, mut cx| async move { + cx.spawn(|_, _| async move { let response = client .request(request) .await .context("inlay hints proto request")?; match response.hint { - Some(resolved_hint) => { - InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) - .await - .context("inlay hints proto resolve response conversion") - } + Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint) + .context("inlay hints proto resolve response conversion"), None => Ok(hint), } }) @@ -7091,8 +7087,7 @@ impl Project { .payload .hint .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); - let hint = InlayHints::proto_to_project_hint(proto_hint, &this, &mut cx) - .await + let hint = InlayHints::proto_to_project_hint(proto_hint) .context("resolved proto inlay hint conversion")?; let buffer = this.update(&mut cx, |this, cx| { this.opened_buffers @@ -7111,10 +7106,8 @@ impl Project { }) .await .context("inlay hints fetch")?; - let resolved_hint = cx.read(|cx| InlayHints::project_to_proto_hint(response_hint, cx)); - Ok(proto::ResolveInlayHintResponse { - hint: Some(resolved_hint), + hint: Some(InlayHints::project_to_proto_hint(response_hint)), }) } @@ -7882,7 +7875,7 @@ impl Project { self.language_servers_for_buffer(buffer, cx).next() } - fn language_server_for_buffer( + pub fn language_server_for_buffer( &self, buffer: &Buffer, server_id: LanguageServerId, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ce47830af22e8203f33aaa86f3f953a86065ad94..3d855c9c694295aa9942779a7ff65b7e1844d3a0 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -773,7 +773,10 @@ message InlayHintLabelParts { message InlayHintLabelPart { string value = 1; InlayHintLabelPartTooltip tooltip = 2; - Location location = 3; + optional string location_url = 3; + PointUtf16 location_range_start = 4; + PointUtf16 location_range_end = 5; + optional uint64 language_server_id = 6; } message InlayHintTooltip { From 665d86ea737c07abf16b9c9f00e24cb6214e0332 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 01:55:17 +0300 Subject: [PATCH 46/67] Defer navigation target buffer opening --- crates/editor/src/editor.rs | 222 +++++++++++++++------ crates/editor/src/element.rs | 8 +- crates/editor/src/link_go_to_definition.rs | 190 +++++++----------- 3 files changed, 246 insertions(+), 174 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c5ff1f027da7faac89bbcbf596d501fffbeae2cb..79186d6e8ca7ec9ff47a74552d45582951f55752 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -23,7 +23,7 @@ pub mod test; use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; use client::{ClickhouseEvent, TelemetrySettings}; use clock::{Global, ReplicaId}; @@ -60,21 +60,24 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, - Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, - OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, + LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, + TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, DocumentRange, InlayRange, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, DocumentRange, GoToDefinitionLink, InlayRange, + LinkGoToDefinitionState, }; use log::error; +use lsp::LanguageServerId; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; use ordered_float::OrderedFloat; -use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction}; +use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, @@ -6551,7 +6554,14 @@ impl Editor { cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move { let definitions = definitions.await?; editor.update(&mut cx, |editor, cx| { - editor.navigate_to_definitions(definitions, split, cx); + editor.navigate_to_definitions( + definitions + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), + split, + cx, + ); })?; Ok::<(), anyhow::Error>(()) }) @@ -6560,7 +6570,7 @@ impl Editor { pub fn navigate_to_definitions( &mut self, - mut definitions: Vec, + mut definitions: Vec, split: bool, cx: &mut ViewContext, ) { @@ -6571,67 +6581,167 @@ impl Editor { // If there is one definition, just open it directly if definitions.len() == 1 { let definition = definitions.pop().unwrap(); - let range = definition - .target - .range - .to_offset(definition.target.buffer.read(cx)); - - let range = self.range_for_match(&range); - if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); - } else { - cx.window_context().defer(move |cx| { - let target_editor: ViewHandle = workspace.update(cx, |workspace, cx| { - if split { - workspace.split_project_item(definition.target.buffer.clone(), cx) + let target_task = match definition { + GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))), + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + self.compute_target_location(lsp_location, server_id, cx) + } + }; + cx.spawn(|editor, mut cx| async move { + let target = target_task.await.context("target resolution task")?; + if let Some(target) = target { + editor.update(&mut cx, |editor, cx| { + let range = target.range.to_offset(target.buffer.read(cx)); + let range = editor.range_for_match(&range); + if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }); } else { - workspace.open_project_item(definition.target.buffer.clone(), cx) + cx.window_context().defer(move |cx| { + let target_editor: ViewHandle = + workspace.update(cx, |workspace, cx| { + if split { + workspace.split_project_item(target.buffer.clone(), cx) + } else { + workspace.open_project_item(target.buffer.clone(), cx) + } + }); + target_editor.update(cx, |target_editor, cx| { + // When selecting a definition in a different buffer, disable the nav history + // to avoid creating a history entry at the previous cursor location. + pane.update(cx, |pane, _| pane.disable_history()); + target_editor.change_selections( + Some(Autoscroll::fit()), + cx, + |s| { + s.select_ranges([range]); + }, + ); + pane.update(cx, |pane, _| pane.enable_history()); + }); + }); } - }); - target_editor.update(cx, |target_editor, cx| { - // When selecting a definition in a different buffer, disable the nav history - // to avoid creating a history entry at the previous cursor location. - pane.update(cx, |pane, _| pane.disable_history()); - target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); - pane.update(cx, |pane, _| pane.enable_history()); - }); - }); - } + }) + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); } else if !definitions.is_empty() { let replica_id = self.replica_id(cx); - cx.window_context().defer(move |cx| { - let title = definitions - .iter() - .find(|definition| definition.origin.is_some()) - .and_then(|definition| { - definition.origin.as_ref().map(|origin| { - let buffer = origin.buffer.read(cx); - format!( - "Definitions for {}", - buffer - .text_for_range(origin.range.clone()) - .collect::() - ) - }) + cx.spawn(|editor, mut cx| async move { + let (title, location_tasks) = editor + .update(&mut cx, |editor, cx| { + let title = definitions + .iter() + .find_map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + link.origin.as_ref().map(|origin| { + let buffer = origin.buffer.read(cx); + format!( + "Definitions for {}", + buffer + .text_for_range(origin.range.clone()) + .collect::() + ) + }) + } + GoToDefinitionLink::InlayHint(_, _) => None, + }) + .unwrap_or("Definitions".to_string()); + let location_tasks = definitions + .into_iter() + .map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + Task::Ready(Some(Ok(Some(link.target)))) + } + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + editor.compute_target_location(lsp_location, server_id, cx) + } + }) + .collect::>(); + (title, location_tasks) }) - .unwrap_or("Definitions".to_owned()); - let locations = definitions + .context("location tasks preparation")?; + + let locations = futures::future::join_all(location_tasks) + .await .into_iter() - .map(|definition| definition.target) - .collect(); - workspace.update(cx, |workspace, cx| { + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + workspace.update(&mut cx, |workspace, cx| { Self::open_locations_in_multibuffer( workspace, locations, replica_id, title, split, cx, ) }); - }); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } + fn compute_target_location( + &self, + lsp_location: lsp::Location, + server_id: LanguageServerId, + cx: &mut ViewContext, + ) -> Task>> { + let Some(project) = self.project.clone() else { + return Task::Ready(Some(Ok(None))); + }; + + cx.spawn(move |editor, mut cx| async move { + let location_task = editor.update(&mut cx, |editor, cx| { + project.update(cx, |project, cx| { + let language_server_name = + editor.buffer.read(cx).as_singleton().and_then(|buffer| { + project + .language_server_for_buffer(buffer.read(cx), server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }) + })?; + let location = match location_task { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = { + target_buffer_handle.update(&mut cx, |target_buffer, _| { + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }) + }; + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + Ok(location) + }) + } + pub fn find_all_references( workspace: &mut Workspace, _: &FindAllReferences, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5b52cc3c7259b7b19cc2d1f96122617fed5e1bb8..684f92a96a8ee8b11e218960d6ac556d75187bfe 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -395,9 +395,7 @@ impl EditorElement { update_go_to_definition_link( editor, - point - .map(GoToDefinitionTrigger::Text) - .unwrap_or(GoToDefinitionTrigger::None), + point.map(GoToDefinitionTrigger::Text), cmd, shift, cx, @@ -468,7 +466,7 @@ impl EditorElement { Some(point) => { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(point), + Some(GoToDefinitionTrigger::Text(point)), cmd, shift, cx, @@ -487,7 +485,7 @@ impl EditorElement { } } } else { - update_go_to_definition_link(editor, GoToDefinitionTrigger::None, cmd, shift, cx); + update_go_to_definition_link(editor, None, cmd, shift, cx); hover_at(editor, None, cx); } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 8d46194f4226e527f5ff9da681e44957a05d7474..ceeda0829accacad8e6d7f84afedc474eb05fff7 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -4,15 +4,14 @@ use crate::{ hover_popover::{self, InlayHover}, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; -use anyhow::Context; use gpui::{Task, ViewContext}; -use language::{point_from_lsp, Bias, LanguageServerName, ToOffset}; +use language::{Bias, ToOffset}; use lsp::LanguageServerId; use project::{ - HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, - LocationLink, ResolveState, + HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, + ResolveState, }; -use std::{ops::Range, sync::Arc}; +use std::ops::Range; use util::TryFutureExt; #[derive(Debug, Default)] @@ -20,14 +19,19 @@ pub struct LinkGoToDefinitionState { pub last_trigger_point: Option, pub symbol_range: Option, pub kind: Option, - pub definitions: Vec, + pub definitions: Vec, pub task: Option>>, } pub enum GoToDefinitionTrigger { Text(DisplayPoint), InlayHint(InlayRange, lsp::Location, LanguageServerId), - None, +} + +#[derive(Debug, Clone)] +pub enum GoToDefinitionLink { + Text(LocationLink), + InlayHint(lsp::Location, LanguageServerId), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -97,7 +101,7 @@ impl TriggerPoint { pub fn update_go_to_definition_link( editor: &mut Editor, - origin: GoToDefinitionTrigger, + origin: Option, cmd_held: bool, shift_held: bool, cx: &mut ViewContext, @@ -107,15 +111,15 @@ pub fn update_go_to_definition_link( // Store new mouse point as an anchor let snapshot = editor.snapshot(cx); let trigger_point = match origin { - GoToDefinitionTrigger::Text(p) => { + Some(GoToDefinitionTrigger::Text(p)) => { Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id) => { + Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => { Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id)) } - GoToDefinitionTrigger::None => None, + None => None, }; // If the new point is the same as the previously stored one, return early @@ -287,7 +291,7 @@ pub fn update_inlay_link_and_hover_points( go_to_definition_updated = true; update_go_to_definition_link( editor, - GoToDefinitionTrigger::InlayHint( + Some(GoToDefinitionTrigger::InlayHint( InlayRange { inlay_position: hovered_hint.position, highlight_start: part_range.start, @@ -295,7 +299,7 @@ pub fn update_inlay_link_and_hover_points( }, location, language_server_id, - ), + )), cmd_held, shift_held, cx, @@ -311,13 +315,7 @@ pub fn update_inlay_link_and_hover_points( } if !go_to_definition_updated { - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::None, - cmd_held, - shift_held, - cx, - ); + update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); } if !hover_updated { hover_popover::hover_at(editor, None, cx); @@ -412,63 +410,20 @@ pub fn show_link_definition( DocumentRange::Text(start..end) }) }), - definition_result, - ) - }) - } - TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => { - let target = match project.update(&mut cx, |project, cx| { - let language_server_name = project - .language_server_for_buffer(buffer.read(cx), *server_id, cx) - .map(|(_, lsp_adapter)| { - LanguageServerName(Arc::from(lsp_adapter.name())) - }); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - *server_id, - language_server_name, - cx, - ) - }) - }) { - Some(task) => Some({ - let target_buffer_handle = task.await.context("open local buffer")?; - let range = cx.read(|cx| { - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - }); - Location { - buffer: target_buffer_handle, - range, - } - }), - None => None, - }; - - target.map(|target| { - ( - Some(DocumentRange::Inlay(trigger_source.clone())), - vec![LocationLink { - origin: Some(Location { - buffer: buffer.clone(), - range: trigger_source.inlay_position.text_anchor - ..trigger_source.inlay_position.text_anchor, - }), - target, - }], + definition_result + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), ) }) } + TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => Some(( + Some(DocumentRange::Inlay(*trigger_source)), + vec![GoToDefinitionLink::InlayHint( + lsp_location.clone(), + *server_id, + )], + )), }; this.update(&mut cx, |this, cx| { @@ -488,43 +443,52 @@ pub fn show_link_definition( // the current location. let any_definition_does_not_contain_current_location = definitions.iter().any(|definition| { - let target = &definition.target; - if target.buffer == buffer { - let range = &target.range; - // Expand range by one character as lsp definition ranges include positions adjacent - // but not contained by the symbol range - let start = buffer_snapshot.clip_offset( - range.start.to_offset(&buffer_snapshot).saturating_sub(1), - Bias::Left, - ); - let end = buffer_snapshot.clip_offset( - range.end.to_offset(&buffer_snapshot) + 1, - Bias::Right, - ); - let offset = buffer_position.to_offset(&buffer_snapshot); - !(start <= offset && end >= offset) - } else { - true + match &definition { + GoToDefinitionLink::Text(link) => { + if link.target.buffer == buffer { + let range = &link.target.range; + // Expand range by one character as lsp definition ranges include positions adjacent + // but not contained by the symbol range + let start = buffer_snapshot.clip_offset( + range + .start + .to_offset(&buffer_snapshot) + .saturating_sub(1), + Bias::Left, + ); + let end = buffer_snapshot.clip_offset( + range.end.to_offset(&buffer_snapshot) + 1, + Bias::Right, + ); + let offset = buffer_position.to_offset(&buffer_snapshot); + !(start <= offset && end >= offset) + } else { + true + } + } + GoToDefinitionLink::InlayHint(_, _) => true, } }); if any_definition_does_not_contain_current_location { // Highlight symbol using theme link definition highlight style let style = theme::current(cx).editor.link_definition; - let highlight_range = symbol_range.unwrap_or_else(|| match trigger_point { - TriggerPoint::Text(trigger_anchor) => { - let snapshot = &snapshot.buffer_snapshot; - // If no symbol range returned from language server, use the surrounding word. - let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - DocumentRange::Text( - snapshot.anchor_before(offset_range.start) - ..snapshot.anchor_after(offset_range.end), - ) - } - TriggerPoint::InlayHint(inlay_coordinates, _, _) => { - DocumentRange::Inlay(inlay_coordinates) - } - }); + let highlight_range = + symbol_range.unwrap_or_else(|| match &trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = + snapshot.surrounding_word(*trigger_anchor); + DocumentRange::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(inlay_coordinates, _, _) => { + DocumentRange::Inlay(*inlay_coordinates) + } + }); match highlight_range { DocumentRange::Text(text_range) => this @@ -689,7 +653,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, true, cx, @@ -801,7 +765,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -841,7 +805,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -869,7 +833,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -892,7 +856,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), false, false, cx, @@ -957,7 +921,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -977,7 +941,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -1079,7 +1043,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, From b2b091879057bf9be983bb8d305ef36ed64387f3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 02:13:36 +0300 Subject: [PATCH 47/67] Consider padding during hint highlight range mapping --- crates/editor/src/hover_popover.rs | 15 +++++++++++++-- crates/editor/src/link_go_to_definition.rs | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 3ce936ae8275b03e4f75abe2cd39ae11f7938214..89e8d8e246cfe7dee57fb51ec16ab817bf88d69f 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -57,19 +57,30 @@ pub struct InlayHover { pub fn find_hovered_hint_part( label_parts: Vec, + padding_left: bool, + padding_right: bool, hint_range: Range, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; let mut part_start = hint_range.start; - for part in label_parts { + let last_label_part_index = label_parts.len() - 1; + for (i, part) in label_parts.into_iter().enumerate() { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; part_start.0 += part_len; } else { - return Some((part, part_start..InlayOffset(part_start.0 + part_len))); + let mut part_end = InlayOffset(part_start.0 + part_len); + if padding_left { + part_start.0 += 1; + part_end.0 += 1; + } + if padding_right && i == last_label_part_index { + part_end.0 -= 1; + } + return Some((part, part_start..part_end)); } } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ceeda0829accacad8e6d7f84afedc474eb05fff7..2ada17de06ec5d02eb6df49f3b65f734ff8f7167 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -218,6 +218,15 @@ pub fn update_inlay_link_and_hover_points( ResolveState::Resolved => { match cached_hint.label { project::InlayHintLabel::String(_) => { + let mut highlight_start = hint_start_offset; + let mut highlight_end = hint_end_offset; + if cached_hint.padding_left { + highlight_start.0 += 1; + highlight_end.0 += 1; + } + if cached_hint.padding_right { + highlight_end.0 -= 1; + } if let Some(tooltip) = cached_hint.tooltip { hover_popover::hover_at_inlay( editor, @@ -238,8 +247,8 @@ pub fn update_inlay_link_and_hover_points( triggered_from: hovered_offset, range: InlayRange { inlay_position: hovered_hint.position, - highlight_start: hint_start_offset, - highlight_end: hint_end_offset, + highlight_start, + highlight_end, }, }, cx, @@ -251,6 +260,8 @@ pub fn update_inlay_link_and_hover_points( if let Some((hovered_hint_part, part_range)) = hover_popover::find_hovered_hint_part( label_parts, + cached_hint.padding_left, + cached_hint.padding_right, hint_start_offset..hint_end_offset, hovered_offset, ) From e6c4802488f39ebc3f5ce6c3d400fba54c038342 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 02:44:19 +0300 Subject: [PATCH 48/67] Properly clip request offsets --- crates/editor/src/inlay_hint_cache.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index a4b91e826d06083a0979b6c4ffd45225d8fb41c2..0067c832d36bf87758563774206186411d94075a 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -19,6 +19,7 @@ use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use sum_tree::Bias; use text::ToOffset; use util::post_inc; @@ -632,8 +633,8 @@ fn determine_query_ranges( return None; } else { vec![ - buffer.anchor_before(excerpt_visible_range.start) - ..buffer.anchor_after(excerpt_visible_range.end), + buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)), ] }; @@ -651,8 +652,8 @@ fn determine_query_ranges( .min(full_excerpt_range_end_offset) .min(buffer.len()); vec![ - buffer.anchor_before(after_visible_range_start) - ..buffer.anchor_after(after_range_end_offset), + buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)), ] }; @@ -668,8 +669,8 @@ fn determine_query_ranges( .saturating_sub(excerpt_visible_len) .max(full_excerpt_range_start_offset); vec![ - buffer.anchor_before(before_range_start_offset) - ..buffer.anchor_after(before_visible_range_end), + buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)), ] }; From 81c64647e82777a6286bbf7534b7aa41d47beaf5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 03:00:53 +0300 Subject: [PATCH 49/67] Fix the test --- crates/editor/src/link_go_to_definition.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 2ada17de06ec5d02eb6df49f3b65f734ff8f7167..280993dd26499cf529ef62e905c140bcc35982bc 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -975,6 +975,7 @@ mod tests { // the cached location instead Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); + cx.foreground().run_until_parked(); cx.assert_editor_state(indoc! {" fn «testˇ»() { do_work(); } fn do_work() { test(); } From 74565ed0b86afa85fb8e26cb35219182ecf667b7 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 25 Aug 2023 17:00:53 -0700 Subject: [PATCH 50/67] Add feature flags handling to the client, rewrite staff mode to a trait extension style --- Cargo.lock | 28 +++---- Cargo.toml | 2 +- crates/channel/Cargo.toml | 2 +- crates/client/Cargo.toml | 2 +- crates/client/src/telemetry.rs | 2 - crates/client/src/user.rs | 35 ++++---- crates/collab_ui/Cargo.toml | 2 +- crates/collab_ui/src/collab_panel.rs | 35 ++++---- .../{staff_mode => feature_flags}/Cargo.toml | 4 +- crates/feature_flags/src/feature_flags.rs | 79 +++++++++++++++++++ crates/rpc/src/rpc.rs | 2 +- crates/settings/Cargo.toml | 2 +- crates/staff_mode/src/staff_mode.rs | 36 --------- crates/theme_selector/Cargo.toml | 2 +- crates/theme_selector/src/theme_selector.rs | 4 +- crates/zed/Cargo.toml | 2 +- crates/zed/src/languages/json.rs | 4 +- crates/zed/src/main.rs | 7 +- 18 files changed, 143 insertions(+), 107 deletions(-) rename crates/{staff_mode => feature_flags}/Cargo.toml (71%) create mode 100644 crates/feature_flags/src/feature_flags.rs delete mode 100644 crates/staff_mode/src/staff_mode.rs diff --git a/Cargo.lock b/Cargo.lock index 8197f883c0615e0e1293d16ec832c31e3c842031..bfdb1b6092d1b221697a356a845e2db3ee4d6b02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1200,6 +1200,7 @@ dependencies = [ "client", "collections", "db", + "feature_flags", "futures 0.3.28", "gpui", "image", @@ -1215,7 +1216,6 @@ dependencies = [ "serde_derive", "settings", "smol", - "staff_mode", "sum_tree", "tempfile", "text", @@ -1374,6 +1374,7 @@ dependencies = [ "async-tungstenite", "collections", "db", + "feature_flags", "futures 0.3.28", "gpui", "image", @@ -1388,7 +1389,6 @@ dependencies = [ "serde_derive", "settings", "smol", - "staff_mode", "sum_tree", "tempfile", "text", @@ -1528,6 +1528,7 @@ dependencies = [ "context_menu", "db", "editor", + "feature_flags", "feedback", "futures 0.3.28", "fuzzy", @@ -1543,7 +1544,6 @@ dependencies = [ "serde", "serde_derive", "settings", - "staff_mode", "theme", "theme_selector", "util", @@ -2529,6 +2529,14 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "feature_flags" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", +] + [[package]] name = "feedback" version = "0.1.0" @@ -6834,6 +6842,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "feature_flags", "fs", "futures 0.3.28", "gpui", @@ -6849,7 +6858,6 @@ dependencies = [ "serde_json_lenient", "smallvec", "sqlez", - "staff_mode", "toml 0.5.11", "tree-sitter", "tree-sitter-json 0.19.0", @@ -7284,14 +7292,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "staff_mode" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -7672,6 +7672,7 @@ name = "theme_selector" version = "0.1.0" dependencies = [ "editor", + "feature_flags", "fs", "fuzzy", "gpui", @@ -7681,7 +7682,6 @@ dependencies = [ "postage", "settings", "smol", - "staff_mode", "theme", "util", "workspace", @@ -9726,6 +9726,7 @@ dependencies = [ "diagnostics", "editor", "env_logger 0.9.3", + "feature_flags", "feedback", "file_finder", "fs", @@ -9772,7 +9773,6 @@ dependencies = [ "simplelog", "smallvec", "smol", - "staff_mode", "sum_tree", "tempdir", "terminal_view", diff --git a/Cargo.toml b/Cargo.toml index 0fb8f0b6b718013b65a999cd8620282fd6979a6b..5938ecb40240765844c1849662b082afeae07a3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ members = [ "crates/snippet", "crates/sqlez", "crates/sqlez_macros", - "crates/staff_mode", + "crates/feature_flags", "crates/sum_tree", "crates/terminal", "crates/text", diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index 0978462a1a8a8a66760992edc4967b5b451603bc..c2191fdfa3edaaf0824e5e59ed974a7c53030ccd 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -21,7 +21,7 @@ rpc = { path = "../rpc" } text = { path = "../text" } language = { path = "../language" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } anyhow.workspace = true diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 64d8f02c8ae1eba2525abca8a4847edb30a458e8..e3038e5bcc49bd41b756062b676e00f4f355867a 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -19,7 +19,7 @@ util = { path = "../util" } rpc = { path = "../rpc" } text = { path = "../text" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } anyhow.workspace = true diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 48886377ba56ad046027da5cf7a754bdc22cea72..9cc5d13af0c72d84bdad54c88d97e2e9ce2586df 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -135,8 +135,6 @@ impl Telemetry { } } - /// This method takes the entire TelemetrySettings struct in order to force client code - /// to pull the struct out of the settings global. Do not remove! pub fn set_authenticated_user_info( self: &Arc, metrics_id: Option, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1dc384da1725c6d58b92aff7477ed516ef69590f..5f13aa40acee9063bfd90c10b43044ff40952db2 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,11 +1,11 @@ use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; +use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; -use staff_mode::StaffMode; use std::sync::{Arc, Weak}; use util::http::HttpClient; use util::TryFutureExt as _; @@ -145,26 +145,23 @@ impl UserStore { let fetch_metrics_id = client.request(proto::GetPrivateUserInfo {}).log_err(); let (user, info) = futures::join!(fetch_user, fetch_metrics_id); - cx.read(|cx| { - client.telemetry.set_authenticated_user_info( - info.as_ref().map(|info| info.metrics_id.clone()), - info.as_ref().map(|info| info.staff).unwrap_or(false), - cx, - ) - }); - cx.update(|cx| { - cx.update_default_global(|staff_mode: &mut StaffMode, _| { - if !staff_mode.0 { - *staff_mode = StaffMode( - info.as_ref() - .map(|info| info.staff) - .unwrap_or_default(), - ) - } - () + if let Some(info) = info { + cx.update(|cx| { + cx.update_flags(info.staff, info.flags); + client.telemetry.set_authenticated_user_info( + Some(info.metrics_id.clone()), + info.staff, + cx, + ) }); - }); + } else { + cx.read(|cx| { + client + .telemetry + .set_authenticated_user_info(None, false, cx) + }); + } current_user_tx.send(user).await.ok(); diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 1ecb4b84227b066ab959997c128bfd96cec6055d..da32308558f7c7e8279c420961f8d42d9356d37b 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -40,7 +40,7 @@ picker = { path = "../picker" } project = { path = "../project" } recent_projects = {path = "../recent_projects"} settings = { path = "../settings" } -staff_mode = {path = "../staff_mode"} +feature_flags = {path = "../feature_flags"} theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } vcs_menu = { path = "../vcs_menu" } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 411a3a2598c052dfb92f4df438effa1c1e57270a..0593bfcb1f279be0ce9fd7fed4dd2672d1813cc4 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -9,6 +9,8 @@ use client::{proto::PeerId, Client, Contact, User, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; + +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use futures::StreamExt; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -33,7 +35,6 @@ use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings} use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; -use staff_mode::StaffMode; use std::{borrow::Cow, mem, sync::Arc}; use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; @@ -182,9 +183,9 @@ pub struct CollabPanel { } #[derive(Serialize, Deserialize)] -struct SerializedChannelsPanel { +struct SerializedCollabPanel { width: Option, - collapsed_channels: Vec, + collapsed_channels: Option>, } #[derive(Debug)] @@ -472,9 +473,10 @@ impl CollabPanel { })); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); - this.subscriptions.push( - cx.observe_global::(move |this, cx| this.update_entries(true, cx)), - ); + this.subscriptions + .push(cx.observe_flag::(move |_, this, cx| { + this.update_entries(true, cx) + })); this.subscriptions.push(cx.subscribe( &this.channel_store, |this, _channel_store, e, cx| match e { @@ -510,7 +512,7 @@ impl CollabPanel { .log_err() .flatten() { - Some(serde_json::from_str::(&panel)?) + Some(serde_json::from_str::(&panel)?) } else { None }; @@ -520,7 +522,9 @@ impl CollabPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; - panel.collapsed_channels = serialized_panel.collapsed_channels; + panel.collapsed_channels = serialized_panel + .collapsed_channels + .unwrap_or_else(|| Vec::new()); cx.notify(); }); } @@ -537,9 +541,9 @@ impl CollabPanel { KEY_VALUE_STORE .write_kvp( COLLABORATION_PANEL_KEY.into(), - serde_json::to_string(&SerializedChannelsPanel { + serde_json::to_string(&SerializedCollabPanel { width, - collapsed_channels, + collapsed_channels: Some(collapsed_channels), })?, ) .await?; @@ -672,7 +676,8 @@ impl CollabPanel { } let mut request_entries = Vec::new(); - if self.include_channels_section(cx) { + + if cx.has_flag::() { self.entries.push(ListEntry::Header(Section::Channels, 0)); if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { @@ -1909,14 +1914,6 @@ impl CollabPanel { .into_any() } - fn include_channels_section(&self, cx: &AppContext) -> bool { - if cx.has_global::() { - cx.global::().0 - } else { - false - } - } - fn deploy_channel_context_menu( &mut self, position: Option, diff --git a/crates/staff_mode/Cargo.toml b/crates/feature_flags/Cargo.toml similarity index 71% rename from crates/staff_mode/Cargo.toml rename to crates/feature_flags/Cargo.toml index 2193bd11b127d94840ed22c1bd7d4e0fb2b8310b..af273fe4033c7fbca36df2ccc8a2daae86eec19b 100644 --- a/crates/staff_mode/Cargo.toml +++ b/crates/feature_flags/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "staff_mode" +name = "feature_flags" version = "0.1.0" edition = "2021" publish = false [lib] -path = "src/staff_mode.rs" +path = "src/feature_flags.rs" [dependencies] gpui = { path = "../gpui" } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs new file mode 100644 index 0000000000000000000000000000000000000000..d14152b04c6155b37091adabd32ab68bcdbf6cdd --- /dev/null +++ b/crates/feature_flags/src/feature_flags.rs @@ -0,0 +1,79 @@ +use gpui::{AppContext, Subscription, ViewContext}; + +#[derive(Default)] +struct FeatureFlags { + flags: Vec, + staff: bool, +} + +impl FeatureFlags { + fn has_flag(&self, flag: &str) -> bool { + self.staff || self.flags.iter().find(|f| f.as_str() == flag).is_some() + } +} + +pub trait FeatureFlag { + const NAME: &'static str; +} + +pub enum ChannelsAlpha {} + +impl FeatureFlag for ChannelsAlpha { + const NAME: &'static str = "channels_alpha"; +} + +pub trait FeatureFlagViewExt { + fn observe_flag(&mut self, callback: F) -> Subscription + where + F: Fn(bool, &mut V, &mut ViewContext) + 'static; +} + +impl FeatureFlagViewExt for ViewContext<'_, '_, V> { + fn observe_flag(&mut self, callback: F) -> Subscription + where + F: Fn(bool, &mut V, &mut ViewContext) + 'static, + { + self.observe_global::(move |v, cx| { + let feature_flags = cx.global::(); + callback(feature_flags.has_flag(::NAME), v, cx); + }) + } +} + +pub trait FeatureFlagAppExt { + fn update_flags(&mut self, staff: bool, flags: Vec); + fn set_staff(&mut self, staff: bool); + fn has_flag(&self) -> bool; + fn is_staff(&self) -> bool; +} + +impl FeatureFlagAppExt for AppContext { + fn update_flags(&mut self, staff: bool, flags: Vec) { + self.update_default_global::(|feature_flags, _| { + feature_flags.staff = staff; + feature_flags.flags = flags; + }) + } + + fn set_staff(&mut self, staff: bool) { + self.update_default_global::(|feature_flags, _| { + feature_flags.staff = staff; + }) + } + + fn has_flag(&self) -> bool { + if self.has_global::() { + self.global::().has_flag(T::NAME) + } else { + false + } + } + + fn is_staff(&self) -> bool { + if self.has_global::() { + return self.global::().staff; + } else { + false + } + } +} diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index d64cbae92993ec2b092fcebdcf48d20f2c7449d6..bc9dd6f80ba039bb705e3d1518c737ba56c969b9 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 = 62; +pub const PROTOCOL_VERSION: u32 = 61; diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 06b81a0c61139ce0bd0a0c58a6101b8a043393bb..f89b80902d0f8e12aade715e7903e8191a8445dc 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -16,7 +16,7 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } sqlez = { path = "../sqlez" } fs = { path = "../fs" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } util = { path = "../util" } anyhow.workspace = true diff --git a/crates/staff_mode/src/staff_mode.rs b/crates/staff_mode/src/staff_mode.rs deleted file mode 100644 index 49fadc0b2cccdd64fdf22e8fed1a887de009749e..0000000000000000000000000000000000000000 --- a/crates/staff_mode/src/staff_mode.rs +++ /dev/null @@ -1,36 +0,0 @@ -use gpui::AppContext; - -#[derive(Debug, Default)] -pub struct StaffMode(pub bool); - -impl std::ops::Deref for StaffMode { - type Target = bool; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Despite what the type system requires me to tell you, the init function will only be called a once -/// as soon as we know that the staff mode is enabled. -pub fn staff_mode(cx: &mut AppContext, mut init: F) { - if **cx.default_global::() { - init(cx) - } else { - let mut once = Some(()); - cx.observe_global::(move |cx| { - if **cx.global::() && once.take().is_some() { - init(cx); - } - }) - .detach(); - } -} - -/// Immediately checks and runs the init function if the staff mode is not enabled. -/// This is only included for symettry with staff_mode() above -pub fn not_staff_mode(cx: &mut AppContext, init: F) { - if !**cx.default_global::() { - init(cx) - } -} diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 377f64aad6f1579dfe9ebb50fb0e8b9c683e0f01..7e97d3918606e42cbadd62e354bea5ded0f44e76 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -16,7 +16,7 @@ gpui = { path = "../gpui" } picker = { path = "../picker" } theme = { path = "../theme" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } workspace = { path = "../workspace" } util = { path = "../util" } log.workspace = true diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 551000573300a16334a6a44035c91e8777af14d2..1969b0256a3aa5ee9c203ee02b765695bb748bf9 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,9 +1,9 @@ +use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext}; use picker::{Picker, PickerDelegate, PickerEvent}; use settings::{update_settings_file, SettingsStore}; -use staff_mode::StaffMode; use std::sync::Arc; use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use util::ResultExt; @@ -54,7 +54,7 @@ impl ThemeSelectorDelegate { fn new(fs: Arc, cx: &mut ViewContext) -> Self { let original_theme = theme::current(cx).clone(); - let staff_mode = **cx.default_global::(); + let staff_mode = cx.is_staff(); let registry = cx.global::>(); let mut theme_names = registry.list(staff_mode).collect::>(); theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 92900f84cb54ea9563de2618989bc4aac470f417..2a977646470507565dbea2b5ac847d2546f16845 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -60,7 +60,7 @@ quick_action_bar = { path = "../quick_action_bar" } recent_projects = { path = "../recent_projects" } rpc = { path = "../rpc" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } terminal_view = { path = "../terminal_view" } diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index b7e4ab4ba7b32491bbbb8aa025cab543dde113af..61d19ce5b6546ce1de8f040fc97c70b322730f3b 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; +use feature_flags::FeatureFlagAppExt; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; @@ -9,7 +10,6 @@ use node_runtime::NodeRuntime; use serde_json::json; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::fs; -use staff_mode::StaffMode; use std::{ any::Any, ffi::OsString, @@ -104,7 +104,7 @@ impl LspAdapter for JsonLspAdapter { cx: &mut AppContext, ) -> Option> { let action_names = cx.all_action_names().collect::>(); - let staff_mode = cx.default_global::().0; + let staff_mode = cx.is_staff(); let language_names = &self.languages.language_names(); let settings_schema = cx.global::().json_schema( &SettingsJsonSchemaParams { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3b1fccb927b9397b723006a5f868cd6aa37bff50..da726eef65d16e9ffeaa84141506c7e1b184a76a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -53,8 +53,6 @@ use uuid::Uuid; use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; -#[cfg(debug_assertions)] -use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::AppState; use zed::{ @@ -122,7 +120,10 @@ fn main() { cx.set_global(*RELEASE_CHANNEL); #[cfg(debug_assertions)] - cx.set_global(StaffMode(true)); + { + use feature_flags::FeatureFlagAppExt; + cx.set_staff(true); + } let mut store = SettingsStore::default(); store From 2b007930a9ed71c5534e771096ecaa10294d9edd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 03:52:52 +0300 Subject: [PATCH 51/67] Remove query ranges for failed inlay hint requests --- crates/editor/src/inlay_hint_cache.rs | 122 +++++++++++++++++++++----- 1 file changed, 100 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 0067c832d36bf87758563774206186411d94075a..1526e16129466a42631883aa5154f5e3117a7039 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -108,17 +108,23 @@ impl TasksForRanges { updated_ranges.before_visible = updated_ranges .before_visible .into_iter() - .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) .collect(); updated_ranges.visible = updated_ranges .visible .into_iter() - .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) .collect(); updated_ranges.after_visible = updated_ranges .after_visible .into_iter() - .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) .collect(); updated_ranges } @@ -134,7 +140,7 @@ impl TasksForRanges { } } - fn remove_cached_ranges( + fn remove_cached_ranges_from_query( &mut self, buffer_snapshot: &BufferSnapshot, query_range: Range, @@ -196,6 +202,52 @@ impl TasksForRanges { ranges_to_query } + + fn remove_from_cached_ranges( + &mut self, + buffer: &BufferSnapshot, + range_to_remove: Range, + ) { + self.sorted_ranges = self + .sorted_ranges + .drain(..) + .filter_map(|mut cached_range| { + if cached_range.start.cmp(&range_to_remove.end, buffer).is_gt() + || cached_range.end.cmp(&range_to_remove.start, buffer).is_lt() + { + Some(vec![cached_range]) + } else if cached_range + .start + .cmp(&range_to_remove.start, buffer) + .is_ge() + && cached_range.end.cmp(&range_to_remove.end, buffer).is_le() + { + None + } else if range_to_remove + .start + .cmp(&cached_range.start, buffer) + .is_ge() + && range_to_remove.end.cmp(&cached_range.end, buffer).is_le() + { + Some(vec![ + cached_range.start..range_to_remove.start, + range_to_remove.end..cached_range.end, + ]) + } else if cached_range + .start + .cmp(&range_to_remove.start, buffer) + .is_ge() + { + cached_range.start = range_to_remove.end; + Some(vec![cached_range]) + } else { + cached_range.end = range_to_remove.start; + Some(vec![cached_range]) + } + }) + .flatten() + .collect(); + } } impl InlayHintCache { @@ -692,7 +744,8 @@ fn new_update_task( cached_excerpt_hints: Option>>, cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { - cx.spawn(|editor, cx| async move { + cx.spawn(|editor, mut cx| async move { + let closure_cx = cx.clone(); let fetch_and_update_hints = |invalidate, range| { fetch_and_update_hints( editor.clone(), @@ -703,37 +756,62 @@ fn new_update_task( query, invalidate, range, - cx.clone(), + closure_cx.clone(), ) }; - let visible_range_update_results = - future::join_all(query_ranges.visible.into_iter().map(|visible_range| { - fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) - })) - .await; - for result in visible_range_update_results { + let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( + |visible_range| async move { + ( + visible_range.clone(), + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + .await, + ) + }, + )) + .await; + + let hint_delay = cx.background().timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )); + + let mut query_range_failed = |range: Range, e: anyhow::Error| { + error!("inlay hint update task for range {range:?} failed: {e:#}"); + editor + .update(&mut cx, |editor, _| { + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.remove_from_cached_ranges(&buffer_snapshot, range); + } + }) + .ok() + }; + + for (range, result) in visible_range_update_results { if let Err(e) = result { - error!("visible range inlay hint update task failed: {e:#}"); + query_range_failed(range, e); } } - cx.background() - .timer(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, - )) - .await; - + hint_delay.await; let invisible_range_update_results = future::join_all( query_ranges .before_visible .into_iter() .chain(query_ranges.after_visible.into_iter()) - .map(|invisible_range| fetch_and_update_hints(false, invisible_range)), + .map(|invisible_range| async move { + ( + invisible_range.clone(), + fetch_and_update_hints(false, invisible_range).await, + ) + }), ) .await; - for result in invisible_range_update_results { + for (range, result) in invisible_range_update_results { if let Err(e) = result { - error!("invisible range inlay hint update task failed: {e:#}"); + query_range_failed(range, e); } } }) From e6fb909d8990ee43f4f3b4381e0d3d8b4615bc7e Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 13:06:50 +0300 Subject: [PATCH 52/67] Limit LSP non-invalidating queries --- crates/editor/src/inlay_hint_cache.rs | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1526e16129466a42631883aa5154f5e3117a7039..4e040a8a7e0aff8dd8d4280f1f0b90ec4786736c 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -19,6 +19,7 @@ use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use smol::lock::Semaphore; use sum_tree::Bias; use text::ToOffset; use util::post_inc; @@ -29,6 +30,7 @@ pub struct InlayHintCache { version: usize, pub(super) enabled: bool, update_tasks: HashMap, + lsp_request_limiter: Arc, } #[derive(Debug)] @@ -258,6 +260,7 @@ impl InlayHintCache { hints: HashMap::default(), update_tasks: HashMap::default(), version: 0, + lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), } } @@ -629,6 +632,7 @@ fn spawn_new_update_tasks( buffer_snapshot.clone(), Arc::clone(&visible_hints), cached_excerpt_hints, + Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), cx, ) }; @@ -733,6 +737,7 @@ fn determine_query_ranges( }) } +const MAX_CONCURRENT_LSP_REQUESTS: usize = 5; const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; fn new_update_task( @@ -742,6 +747,7 @@ fn new_update_task( buffer_snapshot: BufferSnapshot, visible_hints: Arc>, cached_excerpt_hints: Option>>, + lsp_request_limiter: Arc, cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { cx.spawn(|editor, mut cx| async move { @@ -756,6 +762,7 @@ fn new_update_task( query, invalidate, range, + Arc::clone(&lsp_request_limiter), closure_cx.clone(), ) }; @@ -826,10 +833,41 @@ async fn fetch_and_update_hints( query: ExcerptQuery, invalidate: bool, fetch_range: Range, + lsp_request_limiter: Arc, mut cx: gpui::AsyncAppContext, ) -> anyhow::Result<()> { + let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { + (None, false) + } else { + match lsp_request_limiter.try_acquire() { + Some(guard) => (Some(guard), false), + None => (Some(lsp_request_limiter.acquire().await), true), + } + }; let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { + if got_throttled { + if let Some((_, _, current_visible_range)) = editor + .excerpt_visible_offsets(None, cx) + .remove(&query.excerpt_id) + { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + if !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + { + return None; + } + } + } editor .buffer() .read(cx) @@ -847,6 +885,8 @@ async fn fetch_and_update_hints( Some(task) => task.await.context("inlay hint fetch task")?, None => return Ok(()), }; + drop(lsp_request_guard); + let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); let new_update = cx From 3fc48fc2774785b8bbc96a04646d1b079cf9a9da Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 13:46:55 +0300 Subject: [PATCH 53/67] Log LSP inlay hint path --- crates/editor/src/editor.rs | 13 ++++++++ crates/editor/src/inlay_hint_cache.rs | 44 ++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 79186d6e8ca7ec9ff47a74552d45582951f55752..8f0c8c9f220d9fa63bb491174c28508d49a8b23f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1252,6 +1252,17 @@ enum InlayHintRefreshReason { BufferEdited(HashSet>), RefreshRequested, } +impl InlayHintRefreshReason { + fn description(&self) -> &'static str { + match self { + InlayHintRefreshReason::Toggle(_) => "toggle", + InlayHintRefreshReason::SettingsChange(_) => "settings change", + InlayHintRefreshReason::NewLinesShown => "new lines shown", + InlayHintRefreshReason::BufferEdited(_) => "buffer edited", + InlayHintRefreshReason::RefreshRequested => "refresh requested", + } + } +} impl Editor { pub fn single_line( @@ -2741,6 +2752,7 @@ impl Editor { return; } + let reason_description = reason.description(); let (invalidate_cache, required_languages) = match reason { InlayHintRefreshReason::Toggle(enabled) => { self.inlay_hint_cache.enabled = enabled; @@ -2790,6 +2802,7 @@ impl Editor { to_remove, to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( + reason_description, self.excerpt_visible_offsets(required_languages.as_ref(), cx), invalidate_cache, cx, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 4e040a8a7e0aff8dd8d4280f1f0b90ec4786736c..e16971fbbe4feda8669f4be2e14f3908d45932a8 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -13,7 +13,6 @@ use clock::Global; use futures::future; use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; -use log::error; use parking_lot::RwLock; use project::{InlayHint, ResolveState}; @@ -21,7 +20,7 @@ use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; use smol::lock::Semaphore; use sum_tree::Bias; -use text::ToOffset; +use text::{ToOffset, ToPoint}; use util::post_inc; pub struct InlayHintCache { @@ -74,6 +73,7 @@ struct ExcerptQuery { excerpt_id: ExcerptId, cache_version: usize, invalidate: InvalidationStrategy, + reason: &'static str, } impl InvalidationStrategy { @@ -317,6 +317,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, + reason: &'static str, excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, @@ -345,7 +346,14 @@ impl InlayHintCache { cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { - spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx) + spawn_new_update_tasks( + editor, + reason, + excerpts_to_query, + invalidate, + cache_version, + cx, + ) }) .ok(); }) @@ -568,6 +576,7 @@ impl InlayHintCache { fn spawn_new_update_tasks( editor: &mut Editor, + reason: &'static str, excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, update_cache_version: usize, @@ -622,6 +631,7 @@ fn spawn_new_update_tasks( excerpt_id, cache_version: update_cache_version, invalidate, + reason, }; let new_update_task = |query_ranges| { @@ -782,7 +792,7 @@ fn new_update_task( )); let mut query_range_failed = |range: Range, e: anyhow::Error| { - error!("inlay hint update task for range {range:?} failed: {e:#}"); + log::error!("inlay hint update task for range {range:?} failed: {e:#}"); editor .update(&mut cx, |editor, _| { if let Some(task_ranges) = editor @@ -844,6 +854,8 @@ async fn fetch_and_update_hints( None => (Some(lsp_request_limiter.acquire().await), true), } }; + let fetch_range_to_log = + fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { @@ -882,10 +894,25 @@ async fn fetch_and_update_hints( .ok() .flatten(); let new_hints = match inlay_hints_fetch_task { - Some(task) => task.await.context("inlay hint fetch task")?, + Some(fetch_task) => { + log::debug!( + "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", + query_reason = query.reason, + ); + log::trace!( + "Currently visible hints: {visible_hints:?}, cached hints present: {}", + cached_excerpt_hints.is_some(), + ); + fetch_task.await.context("inlay hint fetch task")? + } None => return Ok(()), }; drop(lsp_request_guard); + log::debug!( + "Fetched {} hints for range {fetch_range_to_log:?}", + new_hints.len() + ); + log::trace!("Fetched hints: {new_hints:?}"); let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); @@ -904,6 +931,13 @@ async fn fetch_and_update_hints( }) .await; if let Some(new_update) = new_update { + log::info!( + "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", + new_update.remove_from_visible.len(), + new_update.remove_from_cache.len(), + new_update.add_to_cache.len() + ); + log::trace!("New update: {new_update:?}"); editor .update(&mut cx, |editor, cx| { apply_hint_update( From 48659d3b3cdd997d3e94ebcb2c4e77e2b6b86211 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 14:13:21 +0300 Subject: [PATCH 54/67] Treat multibuffer edit events properly Miltibuffer emits edit events even if it only got an excerpt added/removed/etc. Separate buffer edits and trigger hint invalidation refresh for them only, also trigger hint new lines refresh on excerpt addition events. --- crates/editor/src/editor.rs | 67 +++++++++++++++++-------------- crates/editor/src/multi_buffer.rs | 36 +++++++++++++---- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8f0c8c9f220d9fa63bb491174c28508d49a8b23f..5e9d604de5c64737bb37052c88868c4a8eb4442c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7896,7 +7896,9 @@ impl Editor { cx: &mut ViewContext, ) { match event { - multi_buffer::Event::Edited => { + multi_buffer::Event::Edited { + sigleton_buffer_edited, + } => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); if self.has_active_copilot_suggestion(cx) { @@ -7904,30 +7906,32 @@ impl Editor { } cx.emit(Event::BufferEdited); - if let Some(project) = &self.project { - let project = project.read(cx); - let languages_affected = multibuffer - .read(cx) - .all_buffers() - .into_iter() - .filter_map(|buffer| { - let buffer = buffer.read(cx); - let language = buffer.language()?; - if project.is_local() - && project.language_servers_for_buffer(buffer, cx).count() == 0 - { - None - } else { - Some(language) - } - }) - .cloned() - .collect::>(); - if !languages_affected.is_empty() { - self.refresh_inlay_hints( - InlayHintRefreshReason::BufferEdited(languages_affected), - cx, - ); + if *sigleton_buffer_edited { + if let Some(project) = &self.project { + let project = project.read(cx); + let languages_affected = multibuffer + .read(cx) + .all_buffers() + .into_iter() + .filter_map(|buffer| { + let buffer = buffer.read(cx); + let language = buffer.language()?; + if project.is_local() + && project.language_servers_for_buffer(buffer, cx).count() == 0 + { + None + } else { + Some(language) + } + }) + .cloned() + .collect::>(); + if !languages_affected.is_empty() { + self.refresh_inlay_hints( + InlayHintRefreshReason::BufferEdited(languages_affected), + cx, + ); + } } } } @@ -7935,11 +7939,14 @@ impl Editor { buffer, predecessor, excerpts, - } => cx.emit(Event::ExcerptsAdded { - buffer: buffer.clone(), - predecessor: *predecessor, - excerpts: excerpts.clone(), - }), + } => { + cx.emit(Event::ExcerptsAdded { + buffer: buffer.clone(), + predecessor: *predecessor, + excerpts: excerpts.clone(), + }); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + } multi_buffer::Event::ExcerptsRemoved { ids } => { cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 5c0d8b641cac5731508beae589a499727aac0dd8..e84cfb85aa0ce0a1d0525b1b40268923852f6d4f 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -67,7 +67,9 @@ pub enum Event { ExcerptsEdited { ids: Vec, }, - Edited, + Edited { + sigleton_buffer_edited: bool, + }, Reloaded, DiffBaseChanged, LanguageChanged, @@ -1022,7 +1024,9 @@ impl MultiBuffer { old: edit_start..edit_start, new: edit_start..edit_end, }]); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsAdded { buffer, predecessor: prev_excerpt_id, @@ -1046,7 +1050,9 @@ impl MultiBuffer { old: 0..prev_len, new: 0..0, }]); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1254,7 +1260,9 @@ impl MultiBuffer { } self.subscriptions.publish_mut(edits); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1315,7 +1323,9 @@ impl MultiBuffer { cx: &mut ModelContext, ) { cx.emit(match event { - language::Event::Edited => Event::Edited, + language::Event::Edited => Event::Edited { + sigleton_buffer_edited: true, + }, language::Event::DirtyChanged => Event::DirtyChanged, language::Event::Saved => Event::Saved, language::Event::FileHandleChanged => Event::FileHandleChanged, @@ -4078,7 +4088,7 @@ mod tests { multibuffer.update(cx, |_, cx| { let events = events.clone(); cx.subscribe(&multibuffer, move |_, _, event, _| { - if let Event::Edited = event { + if let Event::Edited { .. } = event { events.borrow_mut().push(event.clone()) } }) @@ -4133,7 +4143,17 @@ mod tests { // Adding excerpts emits an edited event. assert_eq!( events.borrow().as_slice(), - &[Event::Edited, Event::Edited, Event::Edited] + &[ + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + } + ] ); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -4312,7 +4332,7 @@ mod tests { excerpts, } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), - Event::Edited => { + Event::Edited { .. } => { *follower_edit_event_count.borrow_mut() += 1; } _ => {} From 9bdf76f4453739d9593e2b50e6f0614f7affadc1 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 14:42:20 +0300 Subject: [PATCH 55/67] Properly handle hover-less areas hover --- crates/editor/src/link_go_to_definition.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 280993dd26499cf529ef62e905c140bcc35982bc..84fd9b5cc1ee44d2cd5cf22153e2ce0b9d27af61 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -173,6 +173,8 @@ pub fn update_inlay_link_and_hover_points( } else { None }; + let mut go_to_definition_updated = false; + let mut hover_updated = false; if let Some(hovered_offset) = hovered_offset { let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); let previous_valid_anchor = buffer_snapshot.anchor_at( @@ -183,9 +185,6 @@ pub fn update_inlay_link_and_hover_points( point_for_position.next_valid.to_point(snapshot), Bias::Right, ); - - let mut go_to_definition_updated = false; - let mut hover_updated = false; if let Some(hovered_hint) = editor .visible_inlay_hints(cx) .into_iter() @@ -324,13 +323,13 @@ pub fn update_inlay_link_and_hover_points( } } } + } - if !go_to_definition_updated { - update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); - } - if !hover_updated { - hover_popover::hover_at(editor, None, cx); - } + if !go_to_definition_updated { + update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); + } + if !hover_updated { + hover_popover::hover_at(editor, None, cx); } } From 2a42a08f46116d402286388571403339153c1b63 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 14:47:38 +0300 Subject: [PATCH 56/67] Invalidate skipped throttled hint fetch tasks' ranges --- crates/editor/src/inlay_hint_cache.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index e16971fbbe4feda8669f4be2e14f3908d45932a8..ff0d0082ae01df2c49901a35dbd52793bb48ddda 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -208,7 +208,7 @@ impl TasksForRanges { fn remove_from_cached_ranges( &mut self, buffer: &BufferSnapshot, - range_to_remove: Range, + range_to_remove: &Range, ) { self.sorted_ranges = self .sorted_ranges @@ -791,7 +791,7 @@ fn new_update_task( INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, )); - let mut query_range_failed = |range: Range, e: anyhow::Error| { + let mut query_range_failed = |range: &Range, e: anyhow::Error| { log::error!("inlay hint update task for range {range:?} failed: {e:#}"); editor .update(&mut cx, |editor, _| { @@ -800,7 +800,7 @@ fn new_update_task( .update_tasks .get_mut(&query.excerpt_id) { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, range); + task_ranges.remove_from_cached_ranges(&buffer_snapshot, &range); } }) .ok() @@ -808,7 +808,7 @@ fn new_update_task( for (range, result) in visible_range_update_results { if let Err(e) = result { - query_range_failed(range, e); + query_range_failed(&range, e); } } @@ -828,7 +828,7 @@ fn new_update_task( .await; for (range, result) in invisible_range_update_results { if let Err(e) = result { - query_range_failed(range, e); + query_range_failed(&range, e); } } }) @@ -876,6 +876,14 @@ async fn fetch_and_update_hints( && !double_visible_range .contains(&fetch_range.end.to_offset(&buffer_snapshot)) { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); + } return None; } } From 84284099e201532c2e6f51be2efc5a1dce3ca255 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 15:04:48 +0300 Subject: [PATCH 57/67] Properly handle padding when highlighting inlay hints --- crates/editor/src/hover_popover.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 89e8d8e246cfe7dee57fb51ec16ab817bf88d69f..f5f663660daa04ef20bb042310ccd30cd75b6a7b 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -65,8 +65,7 @@ pub fn find_hovered_hint_part( if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; let mut part_start = hint_range.start; - let last_label_part_index = label_parts.len() - 1; - for (i, part) in label_parts.into_iter().enumerate() { + for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; @@ -77,8 +76,9 @@ pub fn find_hovered_hint_part( part_start.0 += 1; part_end.0 += 1; } - if padding_right && i == last_label_part_index { - part_end.0 -= 1; + if padding_right { + part_start.0 += 1; + part_end.0 += 1; } return Some((part, part_start..part_end)); } From f8a8b998cebbd486e94fc5b7c95d83fe36d05c96 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 15:21:45 +0300 Subject: [PATCH 58/67] Properly react on excerpts drop --- crates/editor/src/editor.rs | 21 ++++++++++++++++----- crates/editor/src/inlay_hint_cache.rs | 22 ++++++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5e9d604de5c64737bb37052c88868c4a8eb4442c..01aa59574dc9455c2f5caee8f53144b1ba021446 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1251,15 +1251,17 @@ enum InlayHintRefreshReason { NewLinesShown, BufferEdited(HashSet>), RefreshRequested, + ExcerptsRemoved(Vec), } impl InlayHintRefreshReason { fn description(&self) -> &'static str { match self { - InlayHintRefreshReason::Toggle(_) => "toggle", - InlayHintRefreshReason::SettingsChange(_) => "settings change", - InlayHintRefreshReason::NewLinesShown => "new lines shown", - InlayHintRefreshReason::BufferEdited(_) => "buffer edited", - InlayHintRefreshReason::RefreshRequested => "refresh requested", + Self::Toggle(_) => "toggle", + Self::SettingsChange(_) => "settings change", + Self::NewLinesShown => "new lines shown", + Self::BufferEdited(_) => "buffer edited", + Self::RefreshRequested => "refresh requested", + Self::ExcerptsRemoved(_) => "excerpts removed", } } } @@ -2789,6 +2791,14 @@ impl Editor { ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), } } + InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { + let InlaySplice { + to_remove, + to_insert, + } = self.inlay_hint_cache.remove_excerpts(excerpts_removed); + self.splice_inlay_hints(to_remove, to_insert, cx); + return; + } InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), InlayHintRefreshReason::BufferEdited(buffer_languages) => { (InvalidationStrategy::BufferEdited, Some(buffer_languages)) @@ -7948,6 +7958,7 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } multi_buffer::Event::ExcerptsRemoved { ids } => { + self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) } multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed), diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index ff0d0082ae01df2c49901a35dbd52793bb48ddda..1ed35ef629688575dbf25d0c64fecdd9f3489fa2 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -474,6 +474,24 @@ impl InlayHintCache { } } + pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> InlaySplice { + let mut to_remove = Vec::new(); + for excerpt_to_remove in excerpts_removed { + self.update_tasks.remove(&excerpt_to_remove); + if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) { + let cached_hints = cached_hints.read(); + to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id)); + } + } + if !to_remove.is_empty() { + self.version += 1; + } + InlaySplice { + to_remove, + to_insert: Vec::new(), + } + } + pub fn clear(&mut self) { self.version += 1; self.update_tasks.clear(); @@ -2956,7 +2974,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 2, + 3, "Excerpt removal should trigger a cache update" ); }); @@ -2984,7 +3002,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 3, + 4, "Settings change should trigger a cache update" ); }); From 73937876b6fb3fca890805a941b203ef1adfe7dd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 21:12:04 +0300 Subject: [PATCH 59/67] Properly omit throttled hint queries --- crates/editor/src/inlay_hint_cache.rs | 50 +++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1ed35ef629688575dbf25d0c64fecdd9f3489fa2..2adf3caaf1c1b2782dc5187223847a4b6301c878 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -877,33 +877,33 @@ async fn fetch_and_update_hints( let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { - if let Some((_, _, current_visible_range)) = editor - .excerpt_visible_offsets(None, cx) - .remove(&query.excerpt_id) - { - let visible_offset_length = current_visible_range.len(); - let double_visible_range = current_visible_range - .start - .saturating_sub(visible_offset_length) - ..current_visible_range - .end - .saturating_add(visible_offset_length) - .min(buffer_snapshot.len()); - if !double_visible_range - .contains(&fetch_range.start.to_offset(&buffer_snapshot)) - && !double_visible_range - .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + Some((_, _, current_visible_range)) => { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + }, + None => true, + }; + if query_not_around_visible_range { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) { - log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); - if let Some(task_ranges) = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) - { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); - } - return None; + task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); } + return None; } } editor From 5cf51211b64b0c70e442402939d08e5bee4e17c7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 21:37:34 +0300 Subject: [PATCH 60/67] Use better names, simplify --- crates/editor/src/editor.rs | 8 +- crates/editor/src/inlay_hint_cache.rs | 119 ++++++++++++-------------- 2 files changed, 58 insertions(+), 69 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 01aa59574dc9455c2f5caee8f53144b1ba021446..2d934a1dc8f213d35648e3fd5799ccf8c08321c3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2792,11 +2792,13 @@ impl Editor { } } InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { - let InlaySplice { + if let Some(InlaySplice { to_remove, to_insert, - } = self.inlay_hint_cache.remove_excerpts(excerpts_removed); - self.splice_inlay_hints(to_remove, to_insert, cx); + }) = self.inlay_hint_cache.remove_excerpts(excerpts_removed) + { + self.splice_inlay_hints(to_remove, to_insert, cx); + } return; } InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 2adf3caaf1c1b2782dc5187223847a4b6301c878..34898aea2efe7ec45229cdb4e63de13cd48217f9 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -104,37 +104,34 @@ impl TasksForRanges { invalidate: InvalidationStrategy, spawn_task: impl FnOnce(QueryRanges) -> Task<()>, ) { - let query_ranges = match invalidate { - InvalidationStrategy::None => { - let mut updated_ranges = query_ranges; - updated_ranges.before_visible = updated_ranges - .before_visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - updated_ranges.visible = updated_ranges - .visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - updated_ranges.after_visible = updated_ranges - .after_visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - updated_ranges - } - InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { - self.tasks.clear(); - self.sorted_ranges.clear(); - query_ranges - } + let query_ranges = if invalidate.should_invalidate() { + self.tasks.clear(); + self.sorted_ranges.clear(); + query_ranges + } else { + let mut non_cached_query_ranges = query_ranges; + non_cached_query_ranges.before_visible = non_cached_query_ranges + .before_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.visible = non_cached_query_ranges + .visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.after_visible = non_cached_query_ranges + .after_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges }; if !query_ranges.is_empty() { @@ -205,45 +202,31 @@ impl TasksForRanges { ranges_to_query } - fn remove_from_cached_ranges( - &mut self, - buffer: &BufferSnapshot, - range_to_remove: &Range, - ) { + fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range) { self.sorted_ranges = self .sorted_ranges .drain(..) .filter_map(|mut cached_range| { - if cached_range.start.cmp(&range_to_remove.end, buffer).is_gt() - || cached_range.end.cmp(&range_to_remove.start, buffer).is_lt() + if cached_range.start.cmp(&range.end, buffer).is_gt() + || cached_range.end.cmp(&range.start, buffer).is_lt() { Some(vec![cached_range]) - } else if cached_range - .start - .cmp(&range_to_remove.start, buffer) - .is_ge() - && cached_range.end.cmp(&range_to_remove.end, buffer).is_le() + } else if cached_range.start.cmp(&range.start, buffer).is_ge() + && cached_range.end.cmp(&range.end, buffer).is_le() { None - } else if range_to_remove - .start - .cmp(&cached_range.start, buffer) - .is_ge() - && range_to_remove.end.cmp(&cached_range.end, buffer).is_le() + } else if range.start.cmp(&cached_range.start, buffer).is_ge() + && range.end.cmp(&cached_range.end, buffer).is_le() { Some(vec![ - cached_range.start..range_to_remove.start, - range_to_remove.end..cached_range.end, + cached_range.start..range.start, + range.end..cached_range.end, ]) - } else if cached_range - .start - .cmp(&range_to_remove.start, buffer) - .is_ge() - { - cached_range.start = range_to_remove.end; + } else if cached_range.start.cmp(&range.start, buffer).is_ge() { + cached_range.start = range.end; Some(vec![cached_range]) } else { - cached_range.end = range_to_remove.start; + cached_range.end = range.start; Some(vec![cached_range]) } }) @@ -474,7 +457,7 @@ impl InlayHintCache { } } - pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> InlaySplice { + pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> Option { let mut to_remove = Vec::new(); for excerpt_to_remove in excerpts_removed { self.update_tasks.remove(&excerpt_to_remove); @@ -483,17 +466,21 @@ impl InlayHintCache { to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id)); } } - if !to_remove.is_empty() { + if to_remove.is_empty() { + None + } else { self.version += 1; - } - InlaySplice { - to_remove, - to_insert: Vec::new(), + Some(InlaySplice { + to_remove, + to_insert: Vec::new(), + }) } } pub fn clear(&mut self) { - self.version += 1; + if !self.update_tasks.is_empty() || !self.hints.is_empty() { + self.version += 1; + } self.update_tasks.clear(); self.hints.clear(); } @@ -818,7 +805,7 @@ fn new_update_task( .update_tasks .get_mut(&query.excerpt_id) { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, &range); + task_ranges.invalidate_range(&buffer_snapshot, &range); } }) .ok() @@ -901,7 +888,7 @@ async fn fetch_and_update_hints( .update_tasks .get_mut(&query.excerpt_id) { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); + task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); } return None; } From dad64edde107d7d6bd7d8f835c299ddf1d43d5df Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 15:14:45 +0300 Subject: [PATCH 61/67] Better highlight hint ranges --- crates/editor/src/hover_popover.rs | 14 ++-------- crates/editor/src/link_go_to_definition.rs | 32 ++++++++++------------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index f5f663660daa04ef20bb042310ccd30cd75b6a7b..4eda65fc122b947fe44e7325a022ca6885668ccd 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -57,8 +57,6 @@ pub struct InlayHover { pub fn find_hovered_hint_part( label_parts: Vec, - padding_left: bool, - padding_right: bool, hint_range: Range, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { @@ -67,19 +65,11 @@ pub fn find_hovered_hint_part( let mut part_start = hint_range.start; for part in label_parts { let part_len = part.value.chars().count(); - if hovered_character >= part_len { + if hovered_character > part_len { hovered_character -= part_len; part_start.0 += part_len; } else { - let mut part_end = InlayOffset(part_start.0 + part_len); - if padding_left { - part_start.0 += 1; - part_end.0 += 1; - } - if padding_right { - part_start.0 += 1; - part_end.0 += 1; - } + let part_end = InlayOffset(part_start.0 + part_len); return Some((part, part_start..part_end)); } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 84fd9b5cc1ee44d2cd5cf22153e2ce0b9d27af61..a36c673eae74ab817ff17ffe49b0288ffc48a681 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -165,11 +165,8 @@ pub fn update_inlay_link_and_hover_points( snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); let hint_end_offset = snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { + let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 { Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) } else { None }; @@ -215,17 +212,18 @@ pub fn update_inlay_link_and_hover_points( } } ResolveState::Resolved => { + let mut actual_hint_start = hint_start_offset; + let mut actual_hint_end = hint_end_offset; + if cached_hint.padding_left { + actual_hint_start.0 += 1; + actual_hint_end.0 += 1; + } + if cached_hint.padding_right { + actual_hint_start.0 += 1; + actual_hint_end.0 += 1; + } match cached_hint.label { project::InlayHintLabel::String(_) => { - let mut highlight_start = hint_start_offset; - let mut highlight_end = hint_end_offset; - if cached_hint.padding_left { - highlight_start.0 += 1; - highlight_end.0 += 1; - } - if cached_hint.padding_right { - highlight_end.0 -= 1; - } if let Some(tooltip) = cached_hint.tooltip { hover_popover::hover_at_inlay( editor, @@ -246,8 +244,8 @@ pub fn update_inlay_link_and_hover_points( triggered_from: hovered_offset, range: InlayRange { inlay_position: hovered_hint.position, - highlight_start, - highlight_end, + highlight_start: actual_hint_start, + highlight_end: actual_hint_end, }, }, cx, @@ -259,9 +257,7 @@ pub fn update_inlay_link_and_hover_points( if let Some((hovered_hint_part, part_range)) = hover_popover::find_hovered_hint_part( label_parts, - cached_hint.padding_left, - cached_hint.padding_right, - hint_start_offset..hint_end_offset, + actual_hint_start..actual_hint_end, hovered_offset, ) { From 693e91f3351d64e85f9f4fe7de466a109445d139 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 18:23:40 +0300 Subject: [PATCH 62/67] Properly compare previous hover trigger point when hover changes --- crates/editor/src/link_go_to_definition.rs | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index a36c673eae74ab817ff17ffe49b0288ffc48a681..909c07880b749ca93d37abea83d6b5bc68f1fa38 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -23,6 +23,7 @@ pub struct LinkGoToDefinitionState { pub task: Option>>, } +#[derive(Debug)] pub enum GoToDefinitionTrigger { Text(DisplayPoint), InlayHint(InlayRange, lsp::Location, LanguageServerId), @@ -81,7 +82,7 @@ impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(coordinates, _, _) => &coordinates.inlay_position, + TriggerPoint::InlayHint(range, _, _) => &range.inlay_position, } } @@ -127,11 +128,22 @@ pub fn update_go_to_definition_link( &trigger_point, &editor.link_go_to_definition_state.last_trigger_point, ) { - if a.anchor() - .cmp(b.anchor(), &snapshot.buffer_snapshot) - .is_eq() - { - return; + match (a, b) { + (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => { + if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() { + return; + } + } + (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => { + if range_a + .inlay_position + .cmp(&range_b.inlay_position, &snapshot.buffer_snapshot) + .is_eq() + { + return; + } + } + _ => {} } } From 81e70905bb20ded519ed987c0948289a541a3a19 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 19:12:32 +0300 Subject: [PATCH 63/67] Do not allow cmd+click in invalid inlay context --- crates/editor/src/element.rs | 2 +- crates/editor/src/link_go_to_definition.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 684f92a96a8ee8b11e218960d6ac556d75187bfe..62f4c8c8065e8eb24ef24e7b1c98e75168034f43 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2755,7 +2755,7 @@ impl PointForPosition { } } - fn as_valid(&self) -> Option { + pub fn as_valid(&self) -> Option { if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { Some(self.previous_valid) } else { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 909c07880b749ca93d37abea83d6b5bc68f1fa38..9ca39f9b307769ffa818f29d55c063e974c4213e 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -596,9 +596,11 @@ fn go_to_fetched_definition_of_kind( cx, ); - match kind { - LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), - LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), + if point.as_valid().is_some() { + match kind { + LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), + LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), + } } } } From 38da2a587a74eeb37e6a715a37ea03df82a430e2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 19:41:15 +0300 Subject: [PATCH 64/67] Fix the tests --- crates/editor/src/hover_popover.rs | 40 +++++++++++++++------- crates/editor/src/link_go_to_definition.rs | 16 ++++++--- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 4eda65fc122b947fe44e7325a022ca6885668ccd..2f278ce262f6dd3e0910b022e6956f26c9bdfa6b 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1376,13 +1376,21 @@ mod tests { .unwrap(); let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) + as u32, + ); PointForPosition { - previous_valid: inlay_range.start.to_display_point(&snapshot), - next_valid: inlay_range.end.to_display_point(&snapshot), - exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (entire_hint_label.find(new_type_label).unwrap() - + new_type_label.len() / 2) - as u32, + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, } }); cx.update_editor(|editor, cx| { @@ -1504,13 +1512,21 @@ mod tests { let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) + as u32, + ); PointForPosition { - previous_valid: inlay_range.start.to_display_point(&snapshot), - next_valid: inlay_range.end.to_display_point(&snapshot), - exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (entire_hint_label.find(struct_label).unwrap() - + struct_label.len() / 2) - as u32, + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, } }); cx.update_editor(|editor, cx| { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 9ca39f9b307769ffa818f29d55c063e974c4213e..1f9a3aab730d4d6077df836e54ba09bc12f397d1 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1170,11 +1170,19 @@ mod tests { .unwrap(); let hint_hover_position = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + (hint_label.len() / 2) as u32, + ); PointForPosition { - previous_valid: inlay_range.start.to_display_point(&snapshot), - next_valid: inlay_range.end.to_display_point(&snapshot), - exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (hint_label.len() / 2) as u32, + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, } }); // Press cmd to trigger highlight From 3bfe78b1dfb0b991d16de1a0be4ac9ec46b741f8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 28 Aug 2023 00:27:59 +0300 Subject: [PATCH 65/67] Use proper property names for inlay hint resolve capabilities --- crates/lsp/src/lsp.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 7cba03955280dbed1cd98fd9a7b61a1b526c440f..d49dafff2f99fd1c132c01349a363096cd63183a 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -435,7 +435,13 @@ impl LanguageServer { }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { - properties: vec!["textEdits".to_string(), "tooltip".to_string()], + properties: vec![ + "textEdits".to_string(), + "tooltip".to_string(), + "label.tooltip".to_string(), + "label.location".to_string(), + "label.command".to_string(), + ], }), dynamic_registration: Some(false), }), From 506ec01df3112b9d392ff56cb716d8e831306af9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 28 Aug 2023 11:19:57 +0300 Subject: [PATCH 66/67] Allow `[` and `]` symbols in terminal links ` ./src/pages/[[...slug]].tsx` is a valid file path in macOs and Linux, and should be available for cmd-hover-click in terminal. --- crates/terminal/src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e28e0ca5c16bb15454aa2d9bbc248963df56c2a9..83ba056485664057a13c731033ccf1557a332fc8 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -78,7 +78,7 @@ lazy_static! { // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings. static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap(); - static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.:/@\-~]+"#).unwrap(); + static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap(); } ///Upward flowing events, for changing the title and such From 07b9c6c302d89b3e23f61e9943b2c6b3ab113d99 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:51:50 +0200 Subject: [PATCH 67/67] language: Make Buffer::new take an explicit ID (#2900) See Linear description for the full explanation of the issue. This PR is mostly a mechanical change, except for the one case where we do pass in an explicit `next_id` instead of `model_id` in project.rs. Release Notes: - Fixed a bug where some results were not reported in project search in presence of unnamed buffers. --- crates/ai/src/assistant.rs | 8 +-- crates/copilot/src/copilot.rs | 4 +- crates/editor/src/display_map.rs | 9 ++- crates/editor/src/editor.rs | 6 +- crates/editor/src/editor_tests.rs | 41 +++++++----- crates/editor/src/movement.rs | 3 +- crates/editor/src/multi_buffer.rs | 47 ++++++++------ crates/language/src/buffer.rs | 8 +-- crates/language/src/buffer_tests.rs | 90 +++++++++++++++----------- crates/language_tools/src/lsp_log.rs | 8 ++- crates/project/src/project.rs | 4 +- crates/search/src/buffer_search.rs | 6 +- crates/zed/src/languages/c.rs | 2 +- crates/zed/src/languages/python.rs | 2 +- crates/zed/src/languages/rust.rs | 2 +- crates/zed/src/languages/typescript.rs | 5 +- 16 files changed, 141 insertions(+), 104 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 2699bf40a088b38033af9244c4365d2c57d43dbf..3c561b0e03944c5ac1c673704110fbf749617b5c 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -855,14 +855,14 @@ impl Conversation { ) -> Self { let markdown = language_registry.language_for_name("Markdown"); let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); buffer.set_language_registry(language_registry); cx.spawn_weak(|buffer, mut cx| async move { let markdown = markdown.await?; let buffer = buffer .upgrade(&cx) .ok_or_else(|| anyhow!("buffer was dropped"))?; - buffer.update(&mut cx, |buffer, cx| { + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) }); anyhow::Ok(()) @@ -944,7 +944,7 @@ impl Conversation { let mut message_anchors = Vec::new(); let mut next_message_id = MessageId(0); let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, saved_conversation.text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); for message in saved_conversation.messages { message_anchors.push(MessageAnchor { id: message.id, @@ -958,7 +958,7 @@ impl Conversation { let buffer = buffer .upgrade(&cx) .ok_or_else(|| anyhow!("buffer was dropped"))?; - buffer.update(&mut cx, |buffer, cx| { + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) }); anyhow::Ok(()) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index ab2d861190ff98fb7b4da954a7b92bfb43d75a9d..427134894f3a7383febb571357bf39083e9b06cc 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -980,7 +980,7 @@ mod tests { deterministic.forbid_parking(); let (copilot, mut lsp) = Copilot::fake(cx); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello")); let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); assert_eq!( @@ -996,7 +996,7 @@ mod tests { } ); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx)); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye")); let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); assert_eq!( diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 611866bcadeaef851ba081434fadc04a2d3031ae..5698ccede14bdfd80f42135b54b36efa01f9b8e1 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1362,7 +1362,8 @@ pub mod tests { cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -1451,7 +1452,8 @@ pub mod tests { cx.update(|cx| init_test(cx, |_| {})); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -1523,7 +1525,8 @@ pub mod tests { let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2d934a1dc8f213d35648e3fd5799ccf8c08321c3..38b91007294040d4824cc79231de0a092ba9207a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1271,7 +1271,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx) } @@ -1280,7 +1280,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) } @@ -1290,7 +1290,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new( EditorMode::AutoHeight { max_lines }, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 25a5d45282d580aab5a93bf8ad3750250486e42a..fbc8a0b23543e80716aa9986c3a15a1d9e4acad7 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -42,7 +42,7 @@ fn test_edit_events(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, "123456", cx); + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456"); buffer.set_group_interval(Duration::from_secs(1)); buffer }); @@ -174,7 +174,7 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut now = Instant::now(); - let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); + let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx @@ -247,7 +247,7 @@ fn test_ime_composition(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, "abcde", cx); + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); // Ensure automatic grouping doesn't occur. buffer.set_group_interval(Duration::ZERO); buffer @@ -2281,10 +2281,12 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { None, )); - let toml_buffer = - cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx)); + let toml_buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) + }); let rust_buffer = cx.add_model(|cx| { - Buffer::new(0, "const c: usize = 3;\n", cx).with_language(rust_language, cx) + Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") + .with_language(rust_language, cx) }); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -3754,7 +3756,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -3917,7 +3920,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor @@ -4480,7 +4484,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -4628,7 +4633,8 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor @@ -5834,7 +5840,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( @@ -5918,7 +5924,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { primary: None, } }); - let buffer = cx.add_model(|cx| Buffer::new(0, initial_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts(buffer, excerpt_ranges, cx); @@ -5976,7 +5982,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { fn test_refresh_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -6063,7 +6069,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -6160,7 +6166,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { "{{} }\n", // ); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -7160,8 +7167,8 @@ async fn test_copilot_multibuffer( let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 6b3032b2a35ba9fd46ec145953e626a0f4914f98..def6340e389367c0e483c9648e377d3d92b68c57 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -756,7 +756,8 @@ mod tests { .select_font(family_id, &Default::default()) .unwrap(); - let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e84cfb85aa0ce0a1d0525b1b40268923852f6d4f..3ace5adbc7f46a0d616fe2ebad554a62f6e95901 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1570,7 +1570,7 @@ impl MultiBuffer { #[cfg(any(test, feature = "test-support"))] impl MultiBuffer { pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> ModelHandle { - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); cx.add_model(|cx| Self::singleton(buffer, cx)) } @@ -1580,7 +1580,7 @@ impl MultiBuffer { ) -> ModelHandle { let multi = cx.add_model(|_| Self::new(0)); for (text, ranges) in excerpts { - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { context: range, primary: None, @@ -1672,7 +1672,7 @@ impl MultiBuffer { if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) { let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() { let text = RandomCharIter::new(&mut *rng).take(10).collect::(); - buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx))); + buffers.push(cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text))); let buffer = buffers.last().unwrap().read(cx); log::info!( "Creating new buffer {} with text: {:?}", @@ -4022,7 +4022,8 @@ mod tests { #[gpui::test] fn test_singleton(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -4049,7 +4050,7 @@ mod tests { #[gpui::test] fn test_remote(cx: &mut AppContext) { - let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx)); + let host_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a")); let guest_buffer = cx.add_model(|cx| { let state = host_buffer.read(cx).to_proto(); let ops = cx @@ -4080,8 +4081,10 @@ mod tests { #[gpui::test] fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx)); + let buffer_1 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); + let buffer_2 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'g'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let events = Rc::new(RefCell::new(Vec::::new())); @@ -4314,8 +4317,10 @@ mod tests { #[gpui::test] fn test_excerpt_events(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx)); + let buffer_1 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'a'))); + let buffer_2 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'm'))); let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); @@ -4420,7 +4425,8 @@ mod tests { #[gpui::test] fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts_with_context_lines( @@ -4456,7 +4462,8 @@ mod tests { #[gpui::test] async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx); @@ -4502,7 +4509,7 @@ mod tests { #[gpui::test] fn test_singleton_multibuffer_anchors(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let old_snapshot = multibuffer.read(cx).snapshot(cx); buffer.update(cx, |buffer, cx| { @@ -4522,8 +4529,8 @@ mod tests { #[gpui::test] fn test_multibuffer_anchors(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "efghi")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( @@ -4580,8 +4587,8 @@ mod tests { #[gpui::test] fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "ABCDEFGHIJKLMNOP")); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); // Create an insertion id in buffer 1 that doesn't exist in buffer 2. @@ -4976,7 +4983,9 @@ mod tests { let base_text = util::RandomCharIter::new(&mut rng) .take(10) .collect::(); - buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx))); + buffers.push( + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text)), + ); buffers.last().unwrap() } else { buffers.choose(&mut rng).unwrap() @@ -5317,8 +5326,8 @@ mod tests { fn test_history(cx: &mut AppContext) { cx.set_global(SettingsStore::test(cx)); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "1234")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "5678")); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let group_interval = multibuffer.read(cx).history.group_interval; multibuffer.update(cx, |multibuffer, cx| { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d5586af5ef8fd9088685a6934c16ea62169d953f..902ed26b57471dce6a9dadcdca0a00dd0a8b9051 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -347,13 +347,9 @@ impl CharKind { } impl Buffer { - pub fn new>( - replica_id: ReplicaId, - base_text: T, - cx: &mut ModelContext, - ) -> Self { + pub fn new>(replica_id: ReplicaId, id: u64, base_text: T) -> Self { Self::build( - TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), + TextBuffer::new(replica_id, id, base_text.into()), None, None, ) diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 9d4b9c38fe287596144fecc731bd59398ec10c0b..db3749aa251517c690c49d25167a640534941a21 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -43,8 +43,8 @@ fn test_line_endings(cx: &mut gpui::AppContext) { init_settings(cx, |_| {}); cx.add_model(|cx| { - let mut buffer = - Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "one\r\ntwo\rthree") + .with_language(Arc::new(rust_lang()), cx); assert_eq!(buffer.text(), "one\ntwo\nthree"); assert_eq!(buffer.line_ending(), LineEnding::Windows); @@ -138,8 +138,8 @@ fn test_edit_events(cx: &mut gpui::AppContext) { let buffer_1_events = Rc::new(RefCell::new(Vec::new())); let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); - let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); + let buffer1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcdef")); + let buffer2 = cx.add_model(|cx| Buffer::new(1, cx.model_id() as u64, "abcdef")); let buffer1_ops = Rc::new(RefCell::new(Vec::new())); buffer1.update(cx, { let buffer1_ops = buffer1_ops.clone(); @@ -222,7 +222,7 @@ fn test_edit_events(cx: &mut gpui::AppContext) { #[gpui::test] async fn test_apply_diff(cx: &mut gpui::TestAppContext) { let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); let anchor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))); let text = "a\nccc\ndddd\nffffff\n"; @@ -254,7 +254,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { ] .join("\n"); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); // Spawn a task to format the buffer's whitespace. // Pause so that the foratting task starts running. @@ -318,8 +318,9 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_reparse(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); // Wait for the initial text to parse buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; @@ -443,7 +444,8 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_resetting_language(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "{}", cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, "{}").with_language(Arc::new(rust_lang()), cx); buffer.set_sync_parse_timeout(Duration::ZERO); buffer }); @@ -491,8 +493,9 @@ async fn test_outline(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let outline = buffer .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -576,8 +579,9 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let outline = buffer .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -613,7 +617,9 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx) + }); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); // extra context nodes are included in the outline. @@ -655,8 +661,9 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); // point is at the start of an item @@ -877,7 +884,8 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: & fn test_range_for_syntax_ancestor(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() { b(|c| {}) }"; - let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); let snapshot = buffer.snapshot(); assert_eq!( @@ -917,7 +925,8 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); @@ -959,7 +968,8 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); @@ -1000,6 +1010,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { c; @@ -1007,7 +1018,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC } " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1073,6 +1083,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { b(); @@ -1080,7 +1091,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC " .replace("|", "") // marker to preserve trailing whitespace .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1136,13 +1146,13 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { i } " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1198,11 +1208,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() {} " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1254,7 +1264,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { cx.add_model(|cx| { let text = "a\nb"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(0..1, "\n"), (2..3, "\n")], Some(AutoindentMode::EachLine), @@ -1280,7 +1291,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { " .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], Some(AutoindentMode::EachLine), @@ -1317,7 +1329,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) { } "# .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); // When this text was copied, both of the quotation marks were at the same // indent level, but the indentation of the first line was not included in @@ -1402,7 +1415,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex } "# .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); // The original indent columns are not known, so this text is // auto-indented in a block as if the first line was copied in @@ -1481,7 +1495,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { " .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language( + let mut buffer = Buffer::new(0, cx.model_id() as u64, text).with_language( Arc::new(Language::new( LanguageConfig { name: "Markdown".into(), @@ -1557,7 +1571,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) { false, ); - let mut buffer = Buffer::new(0, text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, text); buffer.set_language_registry(language_registry); buffer.set_language(Some(html_language), cx); buffer.edit( @@ -1593,7 +1607,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { }); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, "").with_language(Arc::new(ruby_lang()), cx); let text = r#" class C @@ -1683,7 +1698,8 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) { let text = r#"a["b"] = ;"#; - let buffer = Buffer::new(0, text, cx).with_language(Arc::new(language), cx); + let buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx); let snapshot = buffer.snapshot(); let config = snapshot.language_scope_at(0).unwrap(); @@ -1762,7 +1778,8 @@ fn test_language_scope_at_with_rust(cx: &mut AppContext) { "# .unindent(); - let buffer = Buffer::new(0, text.clone(), cx).with_language(Arc::new(language), cx); + let buffer = Buffer::new(0, cx.model_id() as u64, text.clone()) + .with_language(Arc::new(language), cx); let snapshot = buffer.snapshot(); // By default, all brackets are enabled @@ -1806,7 +1823,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { language_registry.add(Arc::new(html_lang())); language_registry.add(Arc::new(erb_lang())); - let mut buffer = Buffer::new(0, text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, text); buffer.set_language_registry(language_registry.clone()); buffer.set_language( language_registry @@ -1838,7 +1855,7 @@ fn test_serialization(cx: &mut gpui::AppContext) { let mut now = Instant::now(); let buffer1 = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "abc", cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "abc"); buffer.edit([(3..3, "D")], None, cx); now += Duration::from_secs(1); @@ -1893,7 +1910,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let mut replica_ids = Vec::new(); let mut buffers = Vec::new(); let network = Rc::new(RefCell::new(Network::new(rng.clone()))); - let base_buffer = cx.add_model(|cx| Buffer::new(0, base_text.as_str(), cx)); + let base_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text.as_str())); for i in 0..rng.gen_range(min_peers..=max_peers) { let buffer = cx.add_model(|cx| { @@ -2394,7 +2411,8 @@ fn assert_bracket_pairs( ) { let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false); let buffer = cx.add_model(|cx| { - Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx) + Buffer::new(0, cx.model_id() as u64, expected_text.clone()) + .with_language(Arc::new(language), cx) }); let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot()); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d18f57ffe95386d3b986e30a8cddad27c4f250fd..51bdb4c5cece790604a31d962fc7e2cef0297f98 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -176,7 +176,9 @@ impl LogStore { cx.notify(); LanguageServerState { rpc_state: None, - log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(), + log_buffer: cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) + .clone(), } }) .log_buffer @@ -241,7 +243,7 @@ impl LogStore { let rpc_state = server_state.rpc_state.get_or_insert_with(|| { let io_tx = self.io_tx.clone(); let language = project.read(cx).languages().language_for_name("JSON"); - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); cx.spawn_weak({ let buffer = buffer.clone(); |_, mut cx| async move { @@ -327,7 +329,7 @@ impl LspLogView { .projects .get(&project.downgrade()) .and_then(|project| project.servers.keys().copied().next()); - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); let mut this = Self { editor: Self::editor_for_buffer(project.clone(), buffer, cx), project, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 547c7dcc959b32423699b250d03ec13991da03fb..f839c8d5c504b93f6b11e31d6e39a6f0284f1255 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1568,9 +1568,9 @@ impl Project { if self.is_remote() { return Err(anyhow!("creating buffers as a guest is not supported yet")); } - + let id = post_inc(&mut self.next_buffer_id); let buffer = cx.add_model(|cx| { - Buffer::new(self.replica_id(), text, cx) + Buffer::new(self.replica_id(), id, text) .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) }); self.register_buffer(&buffer, cx)?; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 4078cb572d6dcc57320f04d1bf7e13504dcbd521..e708781eca232702eea6c9ca7bf4b0e4ecf1ee05 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -837,6 +837,7 @@ mod tests { let buffer = cx.add_model(|cx| { Buffer::new( 0, + cx.model_id() as u64, r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search @@ -844,7 +845,6 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(), - cx, ) }); let window = cx.add_window(|_| EmptyView); @@ -1225,7 +1225,7 @@ mod tests { expected_query_matches_count > 1, "Should pick a query with multiple results" ); - let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); let window = cx.add_window(|_| EmptyView); let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); @@ -1412,7 +1412,7 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); let window = cx.add_window(|_| EmptyView); let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 47aa2b739c3773fe701552a0ac17477c15ee963b..c5041136c9eda608593373a08a57d46d27c0cafd 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -289,7 +289,7 @@ mod tests { let language = crate::languages::language("c", tree_sitter_c::language(), None).await; cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); // empty function buffer.edit([(0..0, "int main() {}")], None, cx); diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 331d829cbc78f24bcc5a641bc11c556de2cd11bc..d89a4171e93e3a006180225ab8786b41d921f2e9 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -210,7 +210,7 @@ mod tests { }); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 3c7f84fec7dced7f8241ff7009160b0d748191f4..d550d126bb1ee03a61b33740a0dc36a286eb84b9 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -474,7 +474,7 @@ mod tests { let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await; cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); // indent between braces buffer.set_text("fn a() {}", cx); diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 0a47d365b598aa41df1c1fa50aedd7d718aceb87..34a512f300584f38eaac4905af4b6a772a012ab7 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -356,8 +356,9 @@ mod tests { "# .unindent(); - let buffer = - cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx.add_model(|cx| { + language::Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx) + }); let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); assert_eq!( outline