diff --git a/Cargo.lock b/Cargo.lock index e426bc4ce64d540ea77fcd03decb875ebb76a572..0cc10bde430f4e527053e21d69c89c66f4d25241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14955,6 +14955,7 @@ dependencies = [ "futures 0.3.31", "gpui", "language", + "lsp", "menu", "project", "schemars 1.0.4", diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 57157e59c6b48541ff82bdc417bc119ed01bb997..53a26d9fabdd59e93efbc615ce5be5d1c2d492fb 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId, + EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay, MultiBuffer, ToOffset, actions::Paste, - display_map::{Crease, CreaseId, FoldId, Inlay}, + display_map::{Crease, CreaseId, FoldId}, }; use futures::{ FutureExt as _, @@ -29,7 +29,8 @@ use language::{Buffer, Language, language_settings::InlayHintKind}; use language_model::LanguageModelImage; use postage::stream::Stream as _; use project::{ - CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree, + CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectItem, ProjectPath, + Worktree, }; use prompt_store::{PromptId, PromptStore}; use rope::Point; @@ -75,7 +76,7 @@ pub enum MessageEditorEvent { impl EventEmitter for MessageEditor {} -const COMMAND_HINT_INLAY_ID: u32 = 0; +const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); impl MessageEditor { pub fn new( @@ -151,7 +152,7 @@ impl MessageEditor { let has_new_hint = !new_hints.is_empty(); editor.splice_inlays( if has_hint { - &[InlayId::Hint(COMMAND_HINT_INLAY_ID)] + &[COMMAND_HINT_INLAY_ID] } else { &[] }, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fa2ca6a890af93979eed759265286d99a5a98bb2..67cde1794865ad4f305be84cdb1572ea5d620978 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -343,7 +343,6 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) - .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 6a41f84697e17d85a0a9777a9285dad691d73176..f675cd3522b0f0e273db7528d62f31e37ceda794 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1849,10 +1849,40 @@ async fn test_mutual_editor_inlay_hint_cache_update( ..lsp::ServerCapabilities::default() }; client_a.language_registry().add(rust_lang()); + + // Set up the language server to return an additional inlay hint on each request. + let edits_made = Arc::new(AtomicUsize::new(0)); + let closure_edits_made = Arc::clone(&edits_made); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { capabilities: capabilities.clone(), + initializer: Some(Box::new(move |fake_language_server| { + let closure_edits_made = closure_edits_made.clone(); + fake_language_server.set_request_handler::( + move |params, _| { + let edits_made_2 = Arc::clone(&closure_edits_made); + async move { + assert_eq!( + params.text_document.uri, + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), + ); + let edits_made = + AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, edits_made as u32), + label: lsp::InlayHintLabel::String(edits_made.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }, + ); + })), ..FakeLspAdapter::default() }, ); @@ -1894,61 +1924,20 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap(); let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - executor.start_waiting(); // The host opens a rust file. - let _buffer_a = project_a - .update(cx_a, |project, cx| { - project.open_local_buffer(path!("/a/main.rs"), cx) - }) - .await - .unwrap(); - let editor_a = workspace_a - .update_in(cx_a, |workspace, window, cx| { - workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - + let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx) + }); let fake_language_server = fake_language_servers.next().await.unwrap(); - - // Set up the language server to return an additional inlay hint on each request. - let edits_made = Arc::new(AtomicUsize::new(0)); - let closure_edits_made = Arc::clone(&edits_made); - fake_language_server - .set_request_handler::(move |params, _| { - let edits_made_2 = Arc::clone(&closure_edits_made); - async move { - assert_eq!( - params.text_document.uri, - lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), - ); - let edits_made = AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, edits_made as u32), - label: lsp::InlayHintLabel::String(edits_made.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await - .unwrap(); - + let editor_a = file_a.await.unwrap().downcast::().unwrap(); executor.run_until_parked(); let initial_edit = edits_made.load(atomic::Ordering::Acquire); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert_eq!( vec![initial_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Host should get its first hints when opens an editor" ); }); @@ -1963,10 +1952,10 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap(); executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec![initial_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Client should get its first hints when opens an editor" ); }); @@ -1981,16 +1970,16 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx_b.focus(&editor_b); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert_eq!( vec![after_client_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), ); }); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec![after_client_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), ); }); @@ -2004,16 +1993,16 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx_a.focus(&editor_a); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert_eq!( vec![after_host_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), ); }); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec![after_host_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), ); }); @@ -2025,26 +2014,22 @@ async fn test_mutual_editor_inlay_hint_cache_update( .expect("inlay refresh request failed"); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert_eq!( vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Host should react to /refresh LSP request" ); }); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Guest should get a /refresh LSP request propagated by host" ); }); } -// This test started hanging on seed 2 after the theme settings -// PR. The hypothesis is that it's been buggy for a while, but got lucky -// on seeds. -#[ignore] #[gpui::test(iterations = 10)] async fn test_inlay_hint_refresh_is_forwarded( cx_a: &mut TestAppContext, @@ -2206,18 +2191,18 @@ async fn test_inlay_hint_refresh_is_forwarded( executor.finish_waiting(); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert!( - extract_hint_labels(editor).is_empty(), + extract_hint_labels(editor, cx).is_empty(), "Host should get no hints due to them turned off" ); }); executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec!["initial hint".to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Client should get its first hints when opens an editor" ); }); @@ -2229,18 +2214,18 @@ async fn test_inlay_hint_refresh_is_forwarded( .into_response() .expect("inlay refresh request failed"); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert!( - extract_hint_labels(editor).is_empty(), + extract_hint_labels(editor, cx).is_empty(), "Host should get no hints due to them turned off, even after the /refresh" ); }); executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec!["other hint".to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Guest should get a /refresh LSP request propagated by host despite host hints are off" ); }); @@ -4217,15 +4202,35 @@ fn tab_undo_assert( cx_b.assert_editor_state(expected_initial); } -fn extract_hint_labels(editor: &Editor) -> Vec { - let mut labels = Vec::new(); - for hint in editor.inlay_hint_cache().hints() { - match hint.label { - project::InlayHintLabel::String(s) => labels.push(s), - _ => unreachable!(), - } +fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + + let mut all_cached_labels = Vec::new(); + let mut all_fetched_hints = Vec::new(); + for buffer in editor.buffer().read(cx).all_buffers() { + lsp_store.update(cx, |lsp_store, cx| { + let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints(); + all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| { + let mut label = hint.text().to_string(); + if hint.padding_left { + label.insert(0, ' '); + } + if hint.padding_right { + label.push_str(" "); + } + label + })); + all_fetched_hints.extend(hints.all_fetched_hints()); + }); } - labels + + assert!( + all_fetched_hints.is_empty(), + "Did not expect background hints fetch tasks, but got {} of them", + all_fetched_hints.len() + ); + + all_cached_labels } #[track_caller] diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 74a235697834b120dd1b0dbb55aae03fe950be64..d97a5ab65aab4bb238182040821ecf9fdf828bc3 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -1,9 +1,9 @@ use super::*; use collections::{HashMap, HashSet}; use editor::{ - DisplayPoint, EditorSettings, + DisplayPoint, EditorSettings, Inlay, actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning}, - display_map::{DisplayRow, Inlay}, + display_map::DisplayRow, test::{ editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aad62e0debd366a968a34e5d7b937b75f6272c0d..a6b3d904be94fdcab1b347f68c6c0b03ae091a04 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -27,7 +27,7 @@ mod tab_map; mod wrap_map; use crate::{ - EditorStyle, InlayId, RowExt, hover_links::InlayHighlight, movement::TextLayoutDetails, + EditorStyle, RowExt, hover_links::InlayHighlight, inlays::Inlay, movement::TextLayoutDetails, }; pub use block_map::{ Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement, @@ -42,7 +42,6 @@ pub use fold_map::{ ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint, }; use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle}; -pub use inlay_map::Inlay; use inlay_map::InlaySnapshot; pub use inlay_map::{InlayOffset, InlayPoint}; pub use invisibles::{is_invisible, replacement}; @@ -50,9 +49,10 @@ use language::{ OffsetUtf16, Point, Subscription as BufferSubscription, language_settings::language_settings, }; use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow, - MultiBufferSnapshot, RowInfo, ToOffset, ToPoint, + Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, + RowInfo, ToOffset, ToPoint, }; +use project::InlayId; use project::project_settings::DiagnosticSeverity; use serde::Deserialize; @@ -594,25 +594,6 @@ impl DisplayMap { self.block_map.read(snapshot, edits); } - pub fn remove_inlays_for_excerpts( - &mut self, - excerpts_removed: &[ExcerptId], - cx: &mut Context, - ) { - let to_remove = self - .inlay_map - .current_inlays() - .filter_map(|inlay| { - if excerpts_removed.contains(&inlay.position.excerpt_id) { - Some(inlay.id) - } else { - None - } - }) - .collect::>(); - self.splice_inlays(&to_remove, Vec::new(), cx); - } - fn tab_size(buffer: &Entity, cx: &App) -> NonZeroU32 { let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx)); let language = buffer diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index d93f5acbc65a9a39a95df51469a3bcc02989426c..a31599ef9b276246226c12640fa8ffbec57eb9e3 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,4 +1,4 @@ -use crate::{InlayId, display_map::inlay_map::InlayChunk}; +use crate::display_map::inlay_map::InlayChunk; use super::{ Highlights, @@ -9,6 +9,7 @@ use language::{Edit, HighlightId, Point, TextSummary}; use multi_buffer::{ Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset, }; +use project::InlayId; use std::{ any::TypeId, cmp::{self, Ordering}, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 050cd06a9781db5812cf129968471561e2abd095..486676f1120bc2e9d85effd4c328a2b7a547e06b 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,17 +1,18 @@ -use crate::{ChunkRenderer, HighlightStyles, InlayId}; +use crate::{ + ChunkRenderer, HighlightStyles, + inlays::{Inlay, InlayContent}, +}; use collections::BTreeSet; -use gpui::{Hsla, Rgba}; use language::{Chunk, Edit, Point, TextSummary}; -use multi_buffer::{ - Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset, -}; +use multi_buffer::{MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset}; +use project::InlayId; use std::{ cmp, ops::{Add, AddAssign, Range, Sub, SubAssign}, - sync::{Arc, OnceLock}, + sync::Arc, }; use sum_tree::{Bias, Cursor, Dimensions, SumTree}; -use text::{ChunkBitmaps, Patch, Rope}; +use text::{ChunkBitmaps, Patch}; use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div}; use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId}; @@ -37,85 +38,6 @@ enum Transform { Inlay(Inlay), } -#[derive(Debug, Clone)] -pub struct Inlay { - pub id: InlayId, - pub position: Anchor, - pub content: InlayContent, -} - -#[derive(Debug, Clone)] -pub enum InlayContent { - Text(text::Rope), - Color(Hsla), -} - -impl Inlay { - pub fn hint(id: u32, position: Anchor, hint: &project::InlayHint) -> Self { - let mut text = hint.text(); - if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') { - text.push(" "); - } - if hint.padding_left && text.chars_at(0).next() != Some(' ') { - text.push_front(" "); - } - Self { - id: InlayId::Hint(id), - position, - content: InlayContent::Text(text), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn mock_hint(id: u32, position: Anchor, text: impl Into) -> Self { - Self { - id: InlayId::Hint(id), - position, - content: InlayContent::Text(text.into()), - } - } - - pub fn color(id: u32, position: Anchor, color: Rgba) -> Self { - Self { - id: InlayId::Color(id), - position, - content: InlayContent::Color(color.into()), - } - } - - pub fn edit_prediction>(id: u32, position: Anchor, text: T) -> Self { - Self { - id: InlayId::EditPrediction(id), - position, - content: InlayContent::Text(text.into()), - } - } - - pub fn debugger>(id: u32, position: Anchor, text: T) -> Self { - Self { - id: InlayId::DebuggerValue(id), - position, - content: InlayContent::Text(text.into()), - } - } - - pub fn text(&self) -> &Rope { - static COLOR_TEXT: OnceLock = OnceLock::new(); - match &self.content { - InlayContent::Text(text) => text, - InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("◼")), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn get_color(&self) -> Option { - match self.content { - InlayContent::Color(color) => Some(color), - _ => None, - } - } -} - impl sum_tree::Item for Transform { type Summary = TransformSummary; @@ -750,7 +672,7 @@ impl InlayMap { #[cfg(test)] pub(crate) fn randomly_mutate( &mut self, - next_inlay_id: &mut u32, + next_inlay_id: &mut usize, rng: &mut rand::rngs::StdRng, ) -> (InlaySnapshot, Vec) { use rand::prelude::*; @@ -1245,17 +1167,18 @@ const fn is_utf8_char_boundary(byte: u8) -> bool { mod tests { use super::*; use crate::{ - InlayId, MultiBuffer, + MultiBuffer, display_map::{HighlightKey, InlayHighlights, TextHighlights}, hover_links::InlayHighlight, }; use gpui::{App, HighlightStyle}; + use multi_buffer::Anchor; use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{any::TypeId, cmp::Reverse, env, sync::Arc}; use sum_tree::TreeMap; - use text::Patch; + use text::{Patch, Rope}; use util::RandomCharIter; use util::post_inc; @@ -1263,7 +1186,7 @@ mod tests { fn test_inlay_properties_label_padding() { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), @@ -1283,7 +1206,7 @@ mod tests { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), @@ -1303,7 +1226,7 @@ mod tests { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), @@ -1323,7 +1246,7 @@ mod tests { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), @@ -1346,7 +1269,7 @@ mod tests { fn test_inlay_hint_padding_with_multibyte_chars() { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String("🎨".to_string()), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1a8c8600bf5bcbde37d0d4fcfb8a2133bba3766d..fb62438cebb9e7baf9f8a45a439465f34b921bce 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7,7 +7,6 @@ //! * [`element`] — the place where all rendering happens //! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them. //! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.). -//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly. //! //! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s). //! @@ -24,7 +23,7 @@ mod highlight_matching_bracket; mod hover_links; pub mod hover_popover; mod indent_guides; -mod inlay_hint_cache; +mod inlays; pub mod items; mod jsx_tag_auto_close; mod linked_editing_ranges; @@ -61,6 +60,7 @@ pub use element::{ }; pub use git::blame::BlameRenderer; pub use hover_popover::hover_markdown_style; +pub use inlays::Inlay; pub use items::MAX_TAB_TITLE_LEN; pub use lsp::CompletionContext; pub use lsp_ext::lsp_tasks; @@ -112,10 +112,10 @@ use gpui::{ div, point, prelude::*, pulsating_between, px, relative, size, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; -use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; +use hover_links::{HoverLink, HoveredLinkState, find_file}; use hover_popover::{HoverState, hide_hover}; use indent_guides::ActiveIndentGuidesState; -use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; +use inlays::{InlaySplice, inlay_hints::InlayHintRefreshReason}; use itertools::{Either, Itertools}; use language::{ AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow, @@ -124,8 +124,8 @@ use language::{ IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ - self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, - all_language_settings, language_settings, + self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, + language_settings, }, point_from_lsp, point_to_lsp, text_diff_with_options, }; @@ -146,9 +146,9 @@ use parking_lot::Mutex; use persistence::DB; use project::{ BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent, - CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, - Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectPath, - ProjectTransaction, TaskSourceKind, + CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId, + InvalidationStrategy, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, + ProjectPath, ProjectTransaction, TaskSourceKind, debugger::{ breakpoint_store::{ Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState, @@ -157,7 +157,10 @@ use project::{ session::{Session, SessionEvent}, }, git_store::{GitStoreEvent, RepositoryEvent}, - lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, + lsp_store::{ + CacheInlayHints, CompletionDocumentation, FormatTrigger, LspFormatTarget, + OpenLspBufferHandle, + }, project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings}, }; use rand::seq::SliceRandom; @@ -178,7 +181,7 @@ use std::{ iter::{self, Peekable}, mem, num::NonZeroU32, - ops::{ControlFlow, Deref, DerefMut, Not, Range, RangeInclusive}, + ops::{Deref, DerefMut, Not, Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -208,6 +211,10 @@ use crate::{ code_context_menus::CompletionsMenuSource, editor_settings::MultiCursorModifier, hover_links::{find_url, find_url_from_range}, + inlays::{ + InlineValueCache, + inlay_hints::{LspInlayHintData, inlay_hint_settings}, + }, scroll::{ScrollOffset, ScrollPixelOffset}, signature_help::{SignatureHelpHiddenBy, SignatureHelpState}, }; @@ -261,42 +268,6 @@ impl ReportEditorEvent { } } -struct InlineValueCache { - enabled: bool, - inlays: Vec, - refresh_task: Task>, -} - -impl InlineValueCache { - fn new(enabled: bool) -> Self { - Self { - enabled, - inlays: Vec::new(), - refresh_task: Task::ready(None), - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum InlayId { - EditPrediction(u32), - DebuggerValue(u32), - // LSP - Hint(u32), - Color(u32), -} - -impl InlayId { - fn id(&self) -> u32 { - match self { - Self::EditPrediction(id) => *id, - Self::DebuggerValue(id) => *id, - Self::Hint(id) => *id, - Self::Color(id) => *id, - } - } -} - pub enum ActiveDebugLine {} pub enum DebugStackFrameLine {} enum DocumentHighlightRead {} @@ -1124,9 +1095,8 @@ pub struct Editor { edit_prediction_preview: EditPredictionPreview, edit_prediction_indent_conflict: bool, edit_prediction_requires_modifier_in_indent_conflict: bool, - inlay_hint_cache: InlayHintCache, - next_inlay_id: u32, - next_color_inlay_id: u32, + next_inlay_id: usize, + next_color_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, gutter_dimensions: GutterDimensions, @@ -1193,10 +1163,19 @@ pub struct Editor { colors: Option, post_scroll_update: Task<()>, refresh_colors_task: Task<()>, + inlay_hints: Option, folding_newlines: Task<()>, pub lookup_key: Option>, } +fn debounce_value(debounce_ms: u64) -> Option { + if debounce_ms > 0 { + Some(Duration::from_millis(debounce_ms)) + } else { + None + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] enum NextScrollCursorCenterTopBottom { #[default] @@ -1621,31 +1600,6 @@ pub enum GotoDefinitionKind { Implementation, } -#[derive(Debug, Clone)] -enum InlayHintRefreshReason { - ModifiersChanged(bool), - Toggle(bool), - SettingsChange(InlayHintSettings), - NewLinesShown, - BufferEdited(HashSet>), - RefreshRequested, - ExcerptsRemoved(Vec), -} - -impl InlayHintRefreshReason { - fn description(&self) -> &'static str { - match self { - Self::ModifiersChanged(_) => "modifiers changed", - Self::Toggle(_) => "toggle", - Self::SettingsChange(_) => "settings change", - Self::NewLinesShown => "new lines shown", - Self::BufferEdited(_) => "buffer edited", - Self::RefreshRequested => "refresh requested", - Self::ExcerptsRemoved(_) => "excerpts removed", - } - } -} - pub enum FormatTarget { Buffers(HashSet>), Ranges(Vec>), @@ -1881,8 +1835,11 @@ impl Editor { project::Event::RefreshCodeLens => { // we always query lens with actions, without storing them, always refreshing them } - project::Event::RefreshInlayHints => { - editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); + project::Event::RefreshInlayHints(server_id) => { + editor.refresh_inlay_hints( + InlayHintRefreshReason::RefreshRequested(*server_id), + cx, + ); } project::Event::LanguageServerRemoved(..) => { if editor.tasks_update_task.is_none() { @@ -1919,17 +1876,12 @@ impl Editor { project::Event::LanguageServerBufferRegistered { buffer_id, .. } => { let buffer_id = *buffer_id; if editor.buffer().read(cx).buffer(buffer_id).is_some() { - let registered = editor.register_buffer(buffer_id, cx); - if registered { - editor.update_lsp_data(Some(buffer_id), window, cx); - editor.refresh_inlay_hints( - InlayHintRefreshReason::RefreshRequested, - cx, - ); - refresh_linked_ranges(editor, window, cx); - editor.refresh_code_actions(window, cx); - editor.refresh_document_highlights(cx); - } + editor.register_buffer(buffer_id, cx); + editor.update_lsp_data(Some(buffer_id), window, cx); + editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + refresh_linked_ranges(editor, window, cx); + editor.refresh_code_actions(window, cx); + editor.refresh_document_highlights(cx); } } @@ -2200,7 +2152,6 @@ impl Editor { diagnostics_enabled: full_mode, word_completions_enabled: full_mode, inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), - inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, pixel_position_of_newest_cursor: None, last_bounds: None, @@ -2266,6 +2217,7 @@ impl Editor { pull_diagnostics_task: Task::ready(()), colors: None, refresh_colors_task: Task::ready(()), + inlay_hints: None, next_color_inlay_id: 0, post_scroll_update: Task::ready(()), linked_edit_ranges: Default::default(), @@ -2403,13 +2355,15 @@ impl Editor { editor.go_to_active_debug_line(window, cx); - if let Some(buffer) = multi_buffer.read(cx).as_singleton() { - editor.register_buffer(buffer.read(cx).remote_id(), cx); - } - editor.minimap = editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx); editor.colors = Some(LspColorData::new(cx)); + editor.inlay_hints = Some(LspInlayHintData::new(inlay_hint_settings)); + + if let Some(buffer) = multi_buffer.read(cx).as_singleton() { + editor.register_buffer(buffer.read(cx).remote_id(), cx); + } + editor.update_lsp_data(None, window, cx); editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx); } @@ -5198,179 +5152,8 @@ impl Editor { } } - pub fn toggle_inline_values( - &mut self, - _: &ToggleInlineValues, - _: &mut Window, - cx: &mut Context, - ) { - self.inline_value_cache.enabled = !self.inline_value_cache.enabled; - - self.refresh_inline_values(cx); - } - - pub fn toggle_inlay_hints( - &mut self, - _: &ToggleInlayHints, - _: &mut Window, - cx: &mut Context, - ) { - self.refresh_inlay_hints( - InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()), - cx, - ); - } - - pub fn inlay_hints_enabled(&self) -> bool { - self.inlay_hint_cache.enabled - } - - pub fn inline_values_enabled(&self) -> bool { - self.inline_value_cache.enabled - } - - #[cfg(any(test, feature = "test-support"))] - pub fn inline_value_inlays(&self, cx: &App) -> Vec { - self.display_map - .read(cx) - .current_inlays() - .filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_))) - .cloned() - .collect() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn all_inlays(&self, cx: &App) -> Vec { - self.display_map - .read(cx) - .current_inlays() - .cloned() - .collect() - } - - fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { - if self.semantics_provider.is_none() || !self.mode.is_full() { - return; - } - - let reason_description = reason.description(); - let ignore_debounce = matches!( - reason, - InlayHintRefreshReason::SettingsChange(_) - | InlayHintRefreshReason::Toggle(_) - | InlayHintRefreshReason::ExcerptsRemoved(_) - | InlayHintRefreshReason::ModifiersChanged(_) - ); - let (invalidate_cache, required_languages) = match reason { - InlayHintRefreshReason::ModifiersChanged(enabled) => { - match self.inlay_hint_cache.modifiers_override(enabled) { - Some(enabled) => { - if enabled { - (InvalidationStrategy::RefreshRequested, None) - } else { - self.clear_inlay_hints(cx); - return; - } - } - None => return, - } - } - InlayHintRefreshReason::Toggle(enabled) => { - if self.inlay_hint_cache.toggle(enabled) { - if enabled { - (InvalidationStrategy::RefreshRequested, None) - } else { - self.clear_inlay_hints(cx); - return; - } - } else { - return; - } - } - InlayHintRefreshReason::SettingsChange(new_settings) => { - match self.inlay_hint_cache.update_settings( - &self.buffer, - new_settings, - self.visible_inlay_hints(cx).cloned().collect::>(), - cx, - ) { - ControlFlow::Break(Some(InlaySplice { - to_remove, - to_insert, - })) => { - self.splice_inlays(&to_remove, to_insert, cx); - return; - } - ControlFlow::Break(None) => return, - ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), - } - } - InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { - if let Some(InlaySplice { - to_remove, - to_insert, - }) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed) - { - self.splice_inlays(&to_remove, to_insert, cx); - } - self.display_map.update(cx, |display_map, cx| { - display_map.remove_inlays_for_excerpts(&excerpts_removed, cx) - }); - return; - } - InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), - InlayHintRefreshReason::BufferEdited(buffer_languages) => { - (InvalidationStrategy::BufferEdited, Some(buffer_languages)) - } - InlayHintRefreshReason::RefreshRequested => { - (InvalidationStrategy::RefreshRequested, None) - } - }; - - let mut visible_excerpts = self.visible_excerpts(required_languages.as_ref(), cx); - visible_excerpts.retain(|_, (buffer, _, _)| { - self.registered_buffers - .contains_key(&buffer.read(cx).remote_id()) - }); - - if let Some(InlaySplice { - to_remove, - to_insert, - }) = self.inlay_hint_cache.spawn_hint_refresh( - reason_description, - visible_excerpts, - invalidate_cache, - ignore_debounce, - cx, - ) { - self.splice_inlays(&to_remove, to_insert, cx); - } - } - - pub fn clear_inlay_hints(&self, cx: &mut Context) { - self.splice_inlays( - &self - .visible_inlay_hints(cx) - .map(|inlay| inlay.id) - .collect::>(), - Vec::new(), - cx, - ); - } - - fn visible_inlay_hints<'a>( - &'a self, - cx: &'a Context, - ) -> impl Iterator { - self.display_map - .read(cx) - .current_inlays() - .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_))) - } - pub fn visible_excerpts( &self, - restrict_to_languages: Option<&HashSet>>, cx: &mut Context, ) -> HashMap, clock::Global, Range)> { let Some(project) = self.project() else { @@ -5389,9 +5172,8 @@ impl Editor { + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), Bias::Left, ); - let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; multi_buffer_snapshot - .range_to_buffer_ranges(multi_buffer_visible_range) + .range_to_buffer_ranges(multi_buffer_visible_start..multi_buffer_visible_end) .into_iter() .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| { @@ -5401,23 +5183,17 @@ impl Editor { .read(cx) .entry_for_id(buffer_file.project_entry_id()?)?; if worktree_entry.is_ignored { - return None; - } - - let language = buffer.language()?; - if let Some(restrict_to_languages) = restrict_to_languages - && !restrict_to_languages.contains(language) - { - return None; + None + } else { + Some(( + excerpt_id, + ( + multi_buffer.buffer(buffer.remote_id()).unwrap(), + buffer.version().clone(), + excerpt_visible_range, + ), + )) } - Some(( - excerpt_id, - ( - multi_buffer.buffer(buffer.remote_id()).unwrap(), - buffer.version().clone(), - excerpt_visible_range, - ), - )) }) .collect() } @@ -5433,18 +5209,6 @@ impl Editor { } } - pub fn splice_inlays( - &self, - to_remove: &[InlayId], - to_insert: Vec, - cx: &mut Context, - ) { - self.display_map.update(cx, |display_map, cx| { - display_map.splice_inlays(to_remove, to_insert, cx) - }); - cx.notify(); - } - fn trigger_on_type_formatting( &self, input: String, @@ -17618,9 +17382,9 @@ impl Editor { HashSet::default(), cx, ); - cx.emit(project::Event::RefreshInlayHints); }); }); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } } @@ -20808,18 +20572,6 @@ impl Editor { cx.notify(); } - pub(crate) fn highlight_inlays( - &mut self, - highlights: Vec, - style: HighlightStyle, - cx: &mut Context, - ) { - self.display_map.update(cx, |map, _| { - map.highlight_inlays(TypeId::of::(), highlights, style) - }); - cx.notify(); - } - pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a App, @@ -20970,38 +20722,19 @@ impl Editor { self.update_visible_edit_prediction(window, cx); } - if let Some(edited_buffer) = edited_buffer { - if edited_buffer.read(cx).file().is_none() { + if let Some(buffer) = edited_buffer { + if buffer.read(cx).file().is_none() { cx.emit(EditorEvent::TitleChanged); } - let buffer_id = edited_buffer.read(cx).remote_id(); - if let Some(project) = self.project.clone() { + if self.project.is_some() { + let buffer_id = buffer.read(cx).remote_id(); self.register_buffer(buffer_id, cx); self.update_lsp_data(Some(buffer_id), window, cx); - #[allow(clippy::mutable_key_type)] - let languages_affected = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .all_buffers() - .into_iter() - .filter_map(|buffer| { - buffer.update(cx, |buffer, cx| { - let language = buffer.language()?; - let should_discard = project.update(cx, |project, cx| { - project.is_local() - && !project.has_language_servers_for(buffer, cx) - }); - should_discard.not().then_some(language.clone()) - }) - }) - .collect::>() - }); - if !languages_affected.is_empty() { - self.refresh_inlay_hints( - InlayHintRefreshReason::BufferEdited(languages_affected), - cx, - ); - } + self.refresh_inlay_hints( + InlayHintRefreshReason::BufferEdited(buffer_id), + cx, + ); } } @@ -21048,6 +20781,9 @@ impl Editor { ids, removed_buffer_ids, } => { + if let Some(inlay_hints) = &mut self.inlay_hints { + inlay_hints.remove_inlay_chunk_data(removed_buffer_ids); + } self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); for buffer_id in removed_buffer_ids { self.registered_buffers.remove(buffer_id); @@ -21222,7 +20958,7 @@ impl Editor { if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| { colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors) }) { - if !inlay_splice.to_insert.is_empty() || !inlay_splice.to_remove.is_empty() { + if !inlay_splice.is_empty() { self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx); } self.refresh_colors_for_visible_range(None, window, cx); @@ -21684,10 +21420,6 @@ impl Editor { mouse_context_menu::deploy_context_menu(self, None, position, window, cx); } - pub fn inlay_hint_cache(&self) -> &InlayHintCache { - &self.inlay_hint_cache - } - pub fn replay_insert_event( &mut self, text: &str, @@ -21726,21 +21458,6 @@ impl Editor { self.handle_input(text, window, cx); } - pub fn supports_inlay_hints(&self, cx: &mut App) -> bool { - let Some(provider) = self.semantics_provider.as_ref() else { - return false; - }; - - let mut supports = false; - self.buffer().update(cx, |this, cx| { - this.for_each_buffer(|buffer| { - supports |= provider.supports_inlay_hints(buffer, cx); - }); - }); - - supports - } - pub fn is_focused(&self, window: &Window) -> bool { self.focus_handle.is_focused(window) } @@ -22156,12 +21873,12 @@ impl Editor { if self.ignore_lsp_data() { return; } - for (_, (visible_buffer, _, _)) in self.visible_excerpts(None, cx) { + for (_, (visible_buffer, _, _)) in self.visible_excerpts(cx) { self.register_buffer(visible_buffer.read(cx).remote_id(), cx); } } - fn register_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) -> bool { + fn register_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { if !self.registered_buffers.contains_key(&buffer_id) && let Some(project) = self.project.as_ref() { @@ -22172,13 +21889,10 @@ impl Editor { project.register_buffer_with_language_servers(&buffer, cx), ); }); - return true; } else { self.registered_buffers.remove(&buffer_id); } } - - false } fn ignore_lsp_data(&self) -> bool { @@ -22886,20 +22600,23 @@ pub trait SemanticsProvider { cx: &mut App, ) -> Option>>>; - fn inlay_hints( + fn applicable_inlay_chunks( &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>>; + buffer_id: BufferId, + ranges: &[Range], + cx: &App, + ) -> Vec>; + + fn invalidate_inlay_hints(&self, for_buffers: &HashSet, cx: &mut App); - fn resolve_inlay_hint( + fn inlay_hints( &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, + invalidate: InvalidationStrategy, + buffer: Entity, + ranges: Vec>, + known_chunks: Option<(clock::Global, HashSet>)>, cx: &mut App, - ) -> Option>>; + ) -> Option, Task>>>; fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool; @@ -23392,26 +23109,34 @@ impl SemanticsProvider for Entity { }) } - fn inlay_hints( + fn applicable_inlay_chunks( &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>> { - Some(self.update(cx, |project, cx| { - project.inlay_hints(buffer_handle, range, cx) - })) + buffer_id: BufferId, + ranges: &[Range], + cx: &App, + ) -> Vec> { + self.read(cx) + .lsp_store() + .read(cx) + .applicable_inlay_chunks(buffer_id, ranges) + } + + fn invalidate_inlay_hints(&self, for_buffers: &HashSet, cx: &mut App) { + self.read(cx).lsp_store().update(cx, |lsp_store, _| { + lsp_store.invalidate_inlay_hints(for_buffers) + }); } - fn resolve_inlay_hint( + fn inlay_hints( &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, + invalidate: InvalidationStrategy, + buffer: Entity, + ranges: Vec>, + known_chunks: Option<(clock::Global, HashSet>)>, cx: &mut App, - ) -> Option>> { - Some(self.update(cx, |project, cx| { - project.resolve_inlay_hint(hint, buffer_handle, server_id, cx) + ) -> Option, Task>>> { + Some(self.read(cx).lsp_store().update(cx, |lsp_store, cx| { + lsp_store.inlay_hints(invalidate, buffer, ranges, known_chunks, cx) })) } @@ -23460,16 +23185,6 @@ impl SemanticsProvider for Entity { } } -fn inlay_hint_settings( - location: Anchor, - snapshot: &MultiBufferSnapshot, - cx: &mut Context, -) -> InlayHintSettings { - let file = snapshot.file_at(location); - let language = snapshot.language_at(location).map(|l| l.name()); - language_settings(language, file, cx).inlay_hints -} - fn consume_contiguous_rows( contiguous_row_selections: &mut Vec>, selection: &Selection, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 7a085d4f4fe0701b1dc9117144c819aeccd9005e..ea2ae32ba9a9d589b937e3cbc7939cd8b5bc1b2a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -31,6 +31,7 @@ use language::{ tree_sitter_python, }; use language_settings::Formatter; +use languages::rust_lang; use lsp::CompletionParams; use multi_buffer::{IndentGuide, PathKey}; use parking_lot::Mutex; @@ -50,7 +51,7 @@ use std::{ iter, sync::atomic::{self, AtomicUsize}, }; -use test::{build_editor_with_project, editor_lsp_test_context::rust_lang}; +use test::build_editor_with_project; use text::ToPoint as _; use unindent::Unindent; use util::{ @@ -12640,6 +12641,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) { ); } }); + cx.run_until_parked(); // Handle formatting requests to the language server. cx.lsp diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 4a1a6a934678adb9512ee6883684d2ecb1b2d90a..f36c82b20277fc748620928e6d7fc49a2b20cd3e 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -1,19 +1,14 @@ use crate::{ Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition, - GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind, InlayId, - Navigated, PointForPosition, SelectPhase, - editor_settings::GoToDefinitionFallback, - hover_popover::{self, InlayHover}, + GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind, + Navigated, PointForPosition, SelectPhase, editor_settings::GoToDefinitionFallback, scroll::ScrollAmount, }; use gpui::{App, AsyncWindowContext, Context, Entity, Modifiers, Task, Window, px}; use language::{Bias, ToOffset}; use linkify::{LinkFinder, LinkKind}; use lsp::LanguageServerId; -use project::{ - HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project, - ResolveState, ResolvedPath, -}; +use project::{InlayId, LocationLink, Project, ResolvedPath}; use settings::Settings; use std::ops::Range; use theme::ActiveTheme as _; @@ -138,10 +133,9 @@ impl Editor { show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx); } None => { - update_inlay_link_and_hover_points( + self.update_inlay_link_and_hover_points( snapshot, point_for_position, - self, hovered_link_modifier, modifiers.shift, window, @@ -283,182 +277,6 @@ impl Editor { } } -pub fn update_inlay_link_and_hover_points( - snapshot: &EditorSnapshot, - point_for_position: PointForPosition, - editor: &mut Editor, - secondary_held: bool, - shift_held: bool, - window: &mut Window, - cx: &mut Context, -) { - 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 { - 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_before(point_for_position.previous_valid.to_point(snapshot)); - let next_valid_anchor = - buffer_snapshot.anchor_after(point_for_position.next_valid.to_point(snapshot)); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .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) = snapshot - .buffer_snapshot() - .buffer_id_for_anchor(previous_valid_anchor) - { - inlay_hint_cache.spawn_hint_resolve( - buffer_id, - excerpt_id, - hovered_hint.id, - window, - cx, - ); - } - } - ResolveState::Resolved => { - let mut extra_shift_left = 0; - let mut extra_shift_right = 0; - if cached_hint.padding_left { - extra_shift_left += 1; - extra_shift_right += 1; - } - if cached_hint.padding_right { - extra_shift_right += 1; - } - match cached_hint.label { - project::InlayHintLabel::String(_) => { - if let Some(tooltip) = cached_hint.tooltip { - hover_popover::hover_at_inlay( - editor, - InlayHover { - tooltip: match tooltip { - InlayHintTooltip::String(text) => HoverBlock { - text, - kind: HoverBlockKind::PlainText, - }, - InlayHintTooltip::MarkupContent(content) => { - HoverBlock { - text: content.value, - kind: content.kind, - } - } - }, - range: InlayHighlight { - inlay: hovered_hint.id, - inlay_position: hovered_hint.position, - range: extra_shift_left - ..hovered_hint.text().len() + extra_shift_right, - }, - }, - window, - cx, - ); - hover_updated = true; - } - } - project::InlayHintLabel::LabelParts(label_parts) => { - let hint_start = - snapshot.anchor_to_inlay_offset(hovered_hint.position); - if let Some((hovered_hint_part, part_range)) = - hover_popover::find_hovered_hint_part( - label_parts, - hint_start, - hovered_offset, - ) - { - let highlight_start = - (part_range.start - hint_start).0 + extra_shift_left; - let highlight_end = - (part_range.end - hint_start).0 + extra_shift_right; - let highlight = InlayHighlight { - inlay: hovered_hint.id, - inlay_position: hovered_hint.position, - range: highlight_start..highlight_end, - }; - if let Some(tooltip) = hovered_hint_part.tooltip { - hover_popover::hover_at_inlay( - editor, - InlayHover { - tooltip: match tooltip { - InlayHintLabelPartTooltip::String(text) => { - HoverBlock { - text, - kind: HoverBlockKind::PlainText, - } - } - InlayHintLabelPartTooltip::MarkupContent( - content, - ) => HoverBlock { - text: content.value, - kind: content.kind, - }, - }, - range: highlight.clone(), - }, - window, - cx, - ); - hover_updated = true; - } - if let Some((language_server_id, location)) = - hovered_hint_part.location - && secondary_held - && !editor.has_pending_nonempty_selection() - { - go_to_definition_updated = true; - show_link_definition( - shift_held, - editor, - TriggerPoint::InlayHint( - highlight, - location, - language_server_id, - ), - snapshot, - window, - cx, - ); - } - } - } - }; - } - ResolveState::Resolving => {} - } - } - } - } - - if !go_to_definition_updated { - editor.hide_hovered_link(cx) - } - if !hover_updated { - hover_popover::hover_at(editor, None, window, cx); - } -} - pub fn show_link_definition( shift_held: bool, editor: &mut Editor, @@ -912,7 +730,7 @@ mod tests { DisplayPoint, display_map::ToDisplayPoint, editor_tests::init_test, - inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels}, test::editor_lsp_test_context::EditorLspTestContext, }; use futures::StreamExt; @@ -1343,7 +1161,7 @@ mod tests { cx.background_executor.run_until_parked(); cx.update_editor(|editor, _window, cx| { let expected_layers = vec![hint_label.to_string()]; - assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, cached_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); }); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 9db04363b27959d8f8b81539da4ba65c75fbeb02..19213638f417d20cd54868305ea9e39d57363fca 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -986,17 +986,17 @@ impl DiagnosticPopover { mod tests { use super::*; use crate::{ - InlayId, PointForPosition, + PointForPosition, actions::ConfirmCompletion, editor_tests::{handle_completion_request, init_test}, - hover_links::update_inlay_link_and_hover_points, - inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels}, test::editor_lsp_test_context::EditorLspTestContext, }; use collections::BTreeSet; use gpui::App; use indoc::indoc; use markdown::parser::MarkdownEvent; + use project::InlayId; use settings::InlayHintSettingsContent; use smol::stream::StreamExt; use std::sync::atomic; @@ -1648,7 +1648,7 @@ mod tests { cx.background_executor.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, cached_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); }); @@ -1687,10 +1687,9 @@ mod tests { } }); cx.update_editor(|editor, window, cx| { - update_inlay_link_and_hover_points( + editor.update_inlay_link_and_hover_points( &editor.snapshot(window, cx), new_type_hint_part_hover_position, - editor, true, false, window, @@ -1758,10 +1757,9 @@ mod tests { cx.background_executor.run_until_parked(); cx.update_editor(|editor, window, cx| { - update_inlay_link_and_hover_points( + editor.update_inlay_link_and_hover_points( &editor.snapshot(window, cx), new_type_hint_part_hover_position, - editor, true, false, window, @@ -1813,10 +1811,9 @@ mod tests { } }); cx.update_editor(|editor, window, cx| { - update_inlay_link_and_hover_points( + editor.update_inlay_link_and_hover_points( &editor.snapshot(window, cx), struct_hint_part_hover_position, - editor, true, false, window, diff --git a/crates/editor/src/inlays.rs b/crates/editor/src/inlays.rs new file mode 100644 index 0000000000000000000000000000000000000000..f07bf0b315161f0ce9cdf3ef7e2f6db6d60abfb5 --- /dev/null +++ b/crates/editor/src/inlays.rs @@ -0,0 +1,193 @@ +//! The logic, responsible for managing [`Inlay`]s in the editor. +//! +//! Inlays are "not real" text that gets mixed into the "real" buffer's text. +//! They are attached to a certain [`Anchor`], and display certain contents (usually, strings) +//! between real text around that anchor. +//! +//! Inlay examples in Zed: +//! * inlay hints, received from LSP +//! * inline values, shown in the debugger +//! * inline predictions, showing the Zeta/Copilot/etc. predictions +//! * document color values, if configured to be displayed as inlays +//! * ... anything else, potentially. +//! +//! Editor uses [`crate::DisplayMap`] and [`crate::display_map::InlayMap`] to manage what's rendered inside the editor, using +//! [`InlaySplice`] to update this state. + +/// Logic, related to managing LSP inlay hint inlays. +pub mod inlay_hints; + +use std::{any::TypeId, sync::OnceLock}; + +use gpui::{Context, HighlightStyle, Hsla, Rgba, Task}; +use multi_buffer::Anchor; +use project::{InlayHint, InlayId}; +use text::Rope; + +use crate::{Editor, hover_links::InlayHighlight}; + +/// A splice to send into the `inlay_map` for updating the visible inlays on the screen. +/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes. +/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead. +/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen. +#[derive(Debug, Default)] +pub struct InlaySplice { + pub to_remove: Vec, + pub to_insert: Vec, +} + +impl InlaySplice { + pub fn is_empty(&self) -> bool { + self.to_remove.is_empty() && self.to_insert.is_empty() + } +} + +#[derive(Debug, Clone)] +pub struct Inlay { + pub id: InlayId, + pub position: Anchor, + pub content: InlayContent, +} + +#[derive(Debug, Clone)] +pub enum InlayContent { + Text(text::Rope), + Color(Hsla), +} + +impl Inlay { + pub fn hint(id: InlayId, position: Anchor, hint: &InlayHint) -> Self { + let mut text = hint.text(); + if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') { + text.push(" "); + } + if hint.padding_left && text.chars_at(0).next() != Some(' ') { + text.push_front(" "); + } + Self { + id, + position, + content: InlayContent::Text(text), + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn mock_hint(id: usize, position: Anchor, text: impl Into) -> Self { + Self { + id: InlayId::Hint(id), + position, + content: InlayContent::Text(text.into()), + } + } + + pub fn color(id: usize, position: Anchor, color: Rgba) -> Self { + Self { + id: InlayId::Color(id), + position, + content: InlayContent::Color(color.into()), + } + } + + pub fn edit_prediction>(id: usize, position: Anchor, text: T) -> Self { + Self { + id: InlayId::EditPrediction(id), + position, + content: InlayContent::Text(text.into()), + } + } + + pub fn debugger>(id: usize, position: Anchor, text: T) -> Self { + Self { + id: InlayId::DebuggerValue(id), + position, + content: InlayContent::Text(text.into()), + } + } + + pub fn text(&self) -> &Rope { + static COLOR_TEXT: OnceLock = OnceLock::new(); + match &self.content { + InlayContent::Text(text) => text, + InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("◼")), + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn get_color(&self) -> Option { + match self.content { + InlayContent::Color(color) => Some(color), + _ => None, + } + } +} + +pub struct InlineValueCache { + pub enabled: bool, + pub inlays: Vec, + pub refresh_task: Task>, +} + +impl InlineValueCache { + pub fn new(enabled: bool) -> Self { + Self { + enabled, + inlays: Vec::new(), + refresh_task: Task::ready(None), + } + } +} + +impl Editor { + /// Modify which hints are displayed in the editor. + pub fn splice_inlays( + &mut self, + to_remove: &[InlayId], + to_insert: Vec, + cx: &mut Context, + ) { + if let Some(inlay_hints) = &mut self.inlay_hints { + for id_to_remove in to_remove { + inlay_hints.added_hints.remove(id_to_remove); + } + } + self.display_map.update(cx, |display_map, cx| { + display_map.splice_inlays(to_remove, to_insert, cx) + }); + cx.notify(); + } + + pub(crate) fn highlight_inlays( + &mut self, + highlights: Vec, + style: HighlightStyle, + cx: &mut Context, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), highlights, style) + }); + cx.notify(); + } + + pub fn inline_values_enabled(&self) -> bool { + self.inline_value_cache.enabled + } + + #[cfg(any(test, feature = "test-support"))] + pub fn inline_value_inlays(&self, cx: &gpui::App) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_))) + .cloned() + .collect() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn all_inlays(&self, cx: &gpui::App) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .cloned() + .collect() + } +} diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlays/inlay_hints.rs similarity index 59% rename from crates/editor/src/inlay_hint_cache.rs rename to crates/editor/src/inlays/inlay_hints.rs index 34d737452e905449c259fb41fe03a96e34159b05..07faf8446749085ed24795451c241c0a5747335f 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlays/inlay_hints.rs @@ -1,295 +1,116 @@ -/// Stores and updates all data received from LSP textDocument/inlayHint requests. -/// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere. -/// On every update, cache may query for more inlay hints and update inlays on the screen. -/// -/// Inlays stored on screen are in [`crate::display_map::inlay_map`] and this cache is the only way to update any inlay hint data in the visible hints in the inlay map. -/// For determining the update to the `inlay_map`, the cache requires a list of visible inlay hints — all other hints are not relevant and their separate updates are not influencing the cache work. -/// -/// Due to the way the data is stored for both visible inlays and the cache, every inlay (and inlay hint) collection is editor-specific, so a single buffer may have multiple sets of inlays of open on different panes. use std::{ - cmp, + collections::hash_map, ops::{ControlFlow, Range}, sync::Arc, time::Duration, }; -use crate::{ - Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, display_map::Inlay, -}; -use anyhow::Context as _; use clock::Global; -use futures::future; -use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window}; +use collections::{HashMap, HashSet}; +use futures::future::join_all; +use gpui::{App, Entity, Task}; use language::{ - Buffer, BufferSnapshot, - language_settings::{InlayHintKind, InlayHintSettings}, + BufferRow, + language_settings::{InlayHintKind, InlayHintSettings, language_settings}, }; -use parking_lot::RwLock; -use project::{InlayHint, ResolveState}; +use lsp::LanguageServerId; +use multi_buffer::{Anchor, ExcerptId, MultiBufferSnapshot}; +use parking_lot::Mutex; +use project::{ + HoverBlock, HoverBlockKind, InlayHintLabel, InlayHintLabelPartTooltip, InlayHintTooltip, + InvalidationStrategy, ResolveState, + lsp_store::{CacheInlayHints, ResolvedHint}, +}; +use text::{Bias, BufferId}; +use ui::{Context, Window}; +use util::debug_panic; -use collections::{HashMap, HashSet, hash_map}; -use smol::lock::Semaphore; -use sum_tree::Bias; -use text::{BufferId, ToOffset, ToPoint}; -use util::{ResultExt, post_inc}; +use super::{Inlay, InlayId}; +use crate::{ + Editor, EditorSnapshot, PointForPosition, ToggleInlayHints, ToggleInlineValues, debounce_value, + hover_links::{InlayHighlight, TriggerPoint, show_link_definition}, + hover_popover::{self, InlayHover}, + inlays::InlaySplice, +}; -pub struct InlayHintCache { - hints: HashMap>>, - allowed_hint_kinds: HashSet>, - version: usize, - pub(super) enabled: bool, +pub fn inlay_hint_settings( + location: Anchor, + snapshot: &MultiBufferSnapshot, + cx: &mut Context, +) -> InlayHintSettings { + let file = snapshot.file_at(location); + let language = snapshot.language_at(location).map(|l| l.name()); + language_settings(language, file, cx).inlay_hints +} + +#[derive(Debug)] +pub struct LspInlayHintData { + enabled: bool, modifiers_override: bool, enabled_in_settings: bool, - update_tasks: HashMap, - refresh_task: Task<()>, + allowed_hint_kinds: HashSet>, invalidate_debounce: Option, append_debounce: Option, - lsp_request_limiter: Arc, -} - -#[derive(Debug)] -struct TasksForRanges { - tasks: Vec>, - sorted_ranges: Vec>, -} - -#[derive(Debug)] -struct CachedExcerptHints { - version: usize, - buffer_version: Global, - buffer_id: BufferId, - ordered_hints: Vec, - hints_by_id: HashMap, -} - -/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts. -#[derive(Debug, Clone, Copy)] -pub(super) enum InvalidationStrategy { - /// Hints reset is requested by the LSP server. - /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation. - /// - /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise. - RefreshRequested, - /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place. - /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence. - BufferEdited, - /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position. - /// No invalidation should be done at all, all new hints are added to the cache. - /// - /// A special case is the settings change: in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other). - /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen. - None, -} - -/// A splice to send into the `inlay_map` for updating the visible inlays on the screen. -/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes. -/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead. -/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen. -#[derive(Debug, Default)] -pub(super) struct InlaySplice { - pub to_remove: Vec, - pub to_insert: Vec, -} - -#[derive(Debug)] -struct ExcerptHintsUpdate { - excerpt_id: ExcerptId, - remove_from_visible: HashSet, - remove_from_cache: HashSet, - add_to_cache: Vec, -} - -#[derive(Debug, Clone, Copy)] -struct ExcerptQuery { - buffer_id: BufferId, - excerpt_id: ExcerptId, - cache_version: usize, - invalidate: InvalidationStrategy, - reason: &'static str, -} - -impl InvalidationStrategy { - fn should_invalidate(&self) -> bool { - matches!( - self, - InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited - ) - } + hint_refresh_tasks: HashMap>, Vec>>>, + hint_chunk_fetched: HashMap>)>, + pub added_hints: HashMap>, } -impl TasksForRanges { - fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { +impl LspInlayHintData { + pub fn new(settings: InlayHintSettings) -> Self { Self { - tasks: vec![task], - sorted_ranges: query_ranges.into_sorted_query_ranges(), + modifiers_override: false, + enabled: settings.enabled, + enabled_in_settings: settings.enabled, + hint_refresh_tasks: HashMap::default(), + added_hints: HashMap::default(), + hint_chunk_fetched: HashMap::default(), + invalidate_debounce: debounce_value(settings.edit_debounce_ms), + append_debounce: debounce_value(settings.scroll_debounce_ms), + allowed_hint_kinds: settings.enabled_inlay_hint_kinds(), } } - fn update_cached_tasks( - &mut self, - buffer_snapshot: &BufferSnapshot, - query_ranges: QueryRanges, - invalidate: InvalidationStrategy, - spawn_task: impl FnOnce(QueryRanges) -> Task<()>, - ) { - let query_ranges = if invalidate.should_invalidate() { - self.tasks.clear(); - self.sorted_ranges = query_ranges.clone().into_sorted_query_ranges(); - query_ranges + pub fn modifiers_override(&mut self, new_override: bool) -> Option { + if self.modifiers_override == new_override { + return None; + } + self.modifiers_override = new_override; + if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) + { + self.clear(); + Some(false) } 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() { - self.tasks.push(spawn_task(query_ranges)); + Some(true) } } - fn remove_cached_ranges_from_query( - &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); + pub fn toggle(&mut self, enabled: bool) -> bool { + if self.enabled == enabled { + return false; } - - 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)); - } + self.enabled = enabled; + self.modifiers_override = false; + if !enabled { + self.clear(); } - - ranges_to_query - } - - 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.end, buffer).is_gt() - || cached_range.end.cmp(&range.start, buffer).is_lt() - { - Some(vec![cached_range]) - } else if cached_range.start.cmp(&range.start, buffer).is_ge() - && cached_range.end.cmp(&range.end, buffer).is_le() - { - None - } 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.start, - range.end..cached_range.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.start; - Some(vec![cached_range]) - } - }) - .flatten() - .collect(); + true } -} -impl InlayHintCache { - pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self { - Self { - allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(), - enabled: inlay_hint_settings.enabled, - modifiers_override: false, - enabled_in_settings: inlay_hint_settings.enabled, - hints: HashMap::default(), - update_tasks: HashMap::default(), - refresh_task: Task::ready(()), - invalidate_debounce: debounce_value(inlay_hint_settings.edit_debounce_ms), - append_debounce: debounce_value(inlay_hint_settings.scroll_debounce_ms), - version: 0, - lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), - } + pub fn clear(&mut self) { + self.hint_refresh_tasks.clear(); + self.hint_chunk_fetched.clear(); + self.added_hints.clear(); } /// Checks inlay hint settings for enabled hint kinds and general enabled state. /// Generates corresponding inlay_map splice updates on settings changes. /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries. - pub(super) fn update_settings( + fn update_settings( &mut self, - multi_buffer: &Entity, new_hint_settings: InlayHintSettings, visible_hints: Vec, - cx: &mut Context, - ) -> ControlFlow> { + ) -> ControlFlow, Option> { let old_enabled = self.enabled; // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay // hint visibility changes when other settings change (such as theme). @@ -314,23 +135,30 @@ impl InlayHintCache { if new_allowed_hint_kinds == self.allowed_hint_kinds { ControlFlow::Break(None) } else { - let new_splice = self.new_allowed_hint_kinds_splice( - multi_buffer, - &visible_hints, - &new_allowed_hint_kinds, - cx, - ); - if new_splice.is_some() { - self.version += 1; - self.allowed_hint_kinds = new_allowed_hint_kinds; - } - ControlFlow::Break(new_splice) + self.allowed_hint_kinds = new_allowed_hint_kinds; + ControlFlow::Continue( + Some(InlaySplice { + to_remove: visible_hints + .iter() + .filter_map(|inlay| { + let inlay_kind = self.added_hints.get(&inlay.id).copied()?; + if !self.allowed_hint_kinds.contains(&inlay_kind) { + Some(inlay.id) + } else { + None + } + }) + .collect(), + to_insert: Vec::new(), + }) + .filter(|splice| !splice.is_empty()), + ) } } (true, false) => { self.modifiers_override = false; self.allowed_hint_kinds = new_allowed_hint_kinds; - if self.hints.is_empty() { + if visible_hints.is_empty() { ControlFlow::Break(None) } else { self.clear(); @@ -343,978 +171,770 @@ impl InlayHintCache { (false, true) => { self.modifiers_override = false; self.allowed_hint_kinds = new_allowed_hint_kinds; - ControlFlow::Continue(()) + ControlFlow::Continue( + Some(InlaySplice { + to_remove: visible_hints + .iter() + .filter_map(|inlay| { + let inlay_kind = self.added_hints.get(&inlay.id).copied()?; + if !self.allowed_hint_kinds.contains(&inlay_kind) { + Some(inlay.id) + } else { + None + } + }) + .collect(), + to_insert: Vec::new(), + }) + .filter(|splice| !splice.is_empty()), + ) } } } - pub(super) fn modifiers_override(&mut self, new_override: bool) -> Option { - if self.modifiers_override == new_override { - return None; - } - self.modifiers_override = new_override; - if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) - { - self.clear(); - Some(false) - } else { - Some(true) + pub(crate) fn remove_inlay_chunk_data<'a>( + &'a mut self, + removed_buffer_ids: impl IntoIterator + 'a, + ) { + for buffer_id in removed_buffer_ids { + self.hint_refresh_tasks.remove(buffer_id); + self.hint_chunk_fetched.remove(buffer_id); } } +} - pub(super) fn toggle(&mut self, enabled: bool) -> bool { - if self.enabled == enabled { +#[derive(Debug, Clone)] +pub enum InlayHintRefreshReason { + ModifiersChanged(bool), + Toggle(bool), + SettingsChange(InlayHintSettings), + NewLinesShown, + BufferEdited(BufferId), + RefreshRequested(LanguageServerId), + ExcerptsRemoved(Vec), +} + +impl Editor { + pub fn supports_inlay_hints(&self, cx: &mut App) -> bool { + let Some(provider) = self.semantics_provider.as_ref() else { return false; - } - self.enabled = enabled; - self.modifiers_override = false; - if !enabled { - self.clear(); - } - true + }; + + let mut supports = false; + self.buffer().update(cx, |this, cx| { + this.for_each_buffer(|buffer| { + supports |= provider.supports_inlay_hints(buffer, cx); + }); + }); + + supports } - /// If needed, queries LSP for new inlay hints, using the invalidation strategy given. - /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first, - /// followed by the delayed queries of the same range above and below the visible one. - /// This way, subsequent refresh invocations are less likely to trigger LSP queries for the invisible ranges. - pub(super) fn spawn_hint_refresh( + pub fn toggle_inline_values( &mut self, - reason_description: &'static str, - excerpts_to_query: HashMap, Global, Range)>, - invalidate: InvalidationStrategy, - ignore_debounce: bool, - cx: &mut Context, - ) -> Option { - if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) - { - return None; - } - let mut invalidated_hints = Vec::new(); - if invalidate.should_invalidate() { - self.update_tasks - .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); - self.hints.retain(|cached_excerpt, cached_hints| { - let retain = excerpts_to_query.contains_key(cached_excerpt); - if !retain { - invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied()); - } - retain - }); - } - if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { - return None; - } + _: &ToggleInlineValues, + _: &mut Window, + cx: &mut Context, + ) { + self.inline_value_cache.enabled = !self.inline_value_cache.enabled; - let cache_version = self.version + 1; - let debounce_duration = if ignore_debounce { - None - } else if invalidate.should_invalidate() { - self.invalidate_debounce - } else { - self.append_debounce - }; - self.refresh_task = cx.spawn(async move |editor, cx| { - if let Some(debounce_duration) = debounce_duration { - cx.background_executor().timer(debounce_duration).await; - } + self.refresh_inline_values(cx); + } - editor - .update(cx, |editor, cx| { - spawn_new_update_tasks( - editor, - reason_description, - excerpts_to_query, - invalidate, - cache_version, - cx, - ) - }) - .ok(); - }); + pub fn toggle_inlay_hints( + &mut self, + _: &ToggleInlayHints, + _: &mut Window, + cx: &mut Context, + ) { + self.refresh_inlay_hints( + InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()), + cx, + ); + } - if invalidated_hints.is_empty() { - None - } else { - Some(InlaySplice { - to_remove: invalidated_hints, - to_insert: Vec::new(), - }) - } + pub fn inlay_hints_enabled(&self) -> bool { + self.inlay_hints.as_ref().is_some_and(|cache| cache.enabled) } - fn new_allowed_hint_kinds_splice( - &self, - multi_buffer: &Entity, - visible_hints: &[Inlay], - new_kinds: &HashSet>, - cx: &mut Context, - ) -> Option { - let old_kinds = &self.allowed_hint_kinds; - if new_kinds == old_kinds { - return None; + /// Updates inlay hints for the visible ranges of the singleton buffer(s). + /// Based on its parameters, either invalidates the previous data, or appends to it. + pub(crate) fn refresh_inlay_hints( + &mut self, + reason: InlayHintRefreshReason, + cx: &mut Context, + ) { + if !self.mode.is_full() || self.inlay_hints.is_none() { + return; } + let Some(semantics_provider) = self.semantics_provider() else { + return; + }; + let Some(invalidate_cache) = self.refresh_editor_data(&reason, cx) else { + return; + }; - let mut to_remove = Vec::new(); - let mut to_insert = Vec::new(); - let mut shown_hints_to_remove = visible_hints.iter().fold( - HashMap::>::default(), - |mut current_hints, inlay| { - current_hints - .entry(inlay.position.excerpt_id) - .or_default() - .push((inlay.position, inlay.id)); - current_hints - }, - ); + let debounce = match &reason { + InlayHintRefreshReason::SettingsChange(_) + | InlayHintRefreshReason::Toggle(_) + | InlayHintRefreshReason::ExcerptsRemoved(_) + | InlayHintRefreshReason::ModifiersChanged(_) => None, + _may_need_lsp_call => self.inlay_hints.as_ref().and_then(|inlay_hints| { + if invalidate_cache.should_invalidate() { + inlay_hints.invalidate_debounce + } else { + inlay_hints.append_debounce + } + }), + }; - let multi_buffer = multi_buffer.read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - - for (excerpt_id, excerpt_cached_hints) in &self.hints { - let shown_excerpt_hints_to_remove = - shown_hints_to_remove.entry(*excerpt_id).or_default(); - let excerpt_cached_hints = excerpt_cached_hints.read(); - let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); - shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { - let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else { - return false; + let mut visible_excerpts = self.visible_excerpts(cx); + let mut all_affected_buffers = HashSet::default(); + let ignore_previous_fetches = match reason { + InlayHintRefreshReason::ModifiersChanged(_) + | InlayHintRefreshReason::Toggle(_) + | InlayHintRefreshReason::SettingsChange(_) => true, + InlayHintRefreshReason::NewLinesShown + | InlayHintRefreshReason::RefreshRequested(_) + | InlayHintRefreshReason::ExcerptsRemoved(_) => false, + InlayHintRefreshReason::BufferEdited(buffer_id) => { + let Some(affected_language) = self + .buffer() + .read(cx) + .buffer(buffer_id) + .and_then(|buffer| buffer.read(cx).language().cloned()) + else { + return; }; - let buffer_snapshot = buffer.read(cx).snapshot(); - loop { - match excerpt_cache.peek() { - Some(&cached_hint_id) => { - let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; - if cached_hint_id == shown_hint_id { - excerpt_cache.next(); - return !new_kinds.contains(&cached_hint.kind); - } - match cached_hint - .position - .cmp(&shown_anchor.text_anchor, &buffer_snapshot) - { - cmp::Ordering::Less | cmp::Ordering::Equal => { - if !old_kinds.contains(&cached_hint.kind) - && new_kinds.contains(&cached_hint.kind) - && let Some(anchor) = multi_buffer_snapshot - .anchor_in_excerpt(*excerpt_id, cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - cached_hint, - )); - } - excerpt_cache.next(); - } - cmp::Ordering::Greater => return true, + all_affected_buffers.extend( + self.buffer() + .read(cx) + .all_buffers() + .into_iter() + .filter_map(|buffer| { + let buffer = buffer.read(cx); + if buffer.language() == Some(&affected_language) { + Some(buffer.remote_id()) + } else { + None } - } - None => return true, - } - } - }); + }), + ); - for cached_hint_id in excerpt_cache { - let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; - let cached_hint_kind = maybe_missed_cached_hint.kind; - if !old_kinds.contains(&cached_hint_kind) - && new_kinds.contains(&cached_hint_kind) - && let Some(anchor) = multi_buffer_snapshot - .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - maybe_missed_cached_hint, - )); - } + semantics_provider.invalidate_inlay_hints(&all_affected_buffers, cx); + visible_excerpts.retain(|_, (visible_buffer, _, _)| { + visible_buffer.read(cx).language() == Some(&affected_language) + }); + false } - } + }; - to_remove.extend( - shown_hints_to_remove - .into_values() - .flatten() - .map(|(_, hint_id)| hint_id), - ); - if to_remove.is_empty() && to_insert.is_empty() { - None - } else { - Some(InlaySplice { - to_remove, - to_insert, - }) + let Some(inlay_hints) = self.inlay_hints.as_mut() else { + return; + }; + + if invalidate_cache.should_invalidate() { + inlay_hints.clear(); } - } - /// Completely forget of certain excerpts that were removed from the multibuffer. - pub(super) fn remove_excerpts( - &mut self, - excerpts_removed: &[ExcerptId], - ) -> Option { - 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.ordered_hints.iter().copied()); + let mut buffers_to_query = HashMap::default(); + for (excerpt_id, (buffer, buffer_version, visible_range)) in visible_excerpts { + let buffer_id = buffer.read(cx).remote_id(); + if !self.registered_buffers.contains_key(&buffer_id) { + continue; } - } - if to_remove.is_empty() { - None - } else { - self.version += 1; - Some(InlaySplice { - to_remove, - to_insert: Vec::new(), - }) - } - } - pub(super) fn clear(&mut self) { - if !self.update_tasks.is_empty() || !self.hints.is_empty() { - self.version += 1; + let buffer_snapshot = buffer.read(cx).snapshot(); + let buffer_anchor_range = buffer_snapshot.anchor_before(visible_range.start) + ..buffer_snapshot.anchor_after(visible_range.end); + + let visible_excerpts = + buffers_to_query + .entry(buffer_id) + .or_insert_with(|| VisibleExcerpts { + excerpts: Vec::new(), + ranges: Vec::new(), + buffer_version: buffer_version.clone(), + buffer: buffer.clone(), + }); + visible_excerpts.buffer_version = buffer_version; + visible_excerpts.excerpts.push(excerpt_id); + visible_excerpts.ranges.push(buffer_anchor_range); } - self.update_tasks.clear(); - self.refresh_task = Task::ready(()); - self.hints.clear(); - } - pub(super) fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { - self.hints - .get(&excerpt_id)? - .read() - .hints_by_id - .get(&hint_id) - .cloned() - } + let all_affected_buffers = Arc::new(Mutex::new(all_affected_buffers)); + for (buffer_id, visible_excerpts) in buffers_to_query { + let fetched_tasks = inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default(); + if visible_excerpts + .buffer_version + .changed_since(&fetched_tasks.0) + { + fetched_tasks.1.clear(); + fetched_tasks.0 = visible_excerpts.buffer_version.clone(); + inlay_hints.hint_refresh_tasks.remove(&buffer_id); + } - pub fn hints(&self) -> Vec { - let mut hints = Vec::new(); - for excerpt_hints in self.hints.values() { - let excerpt_hints = excerpt_hints.read(); - hints.extend( - excerpt_hints - .ordered_hints - .iter() - .map(|id| &excerpt_hints.hints_by_id[id]) - .cloned(), - ); - } - hints - } + let applicable_chunks = + semantics_provider.applicable_inlay_chunks(buffer_id, &visible_excerpts.ranges, cx); - /// Queries a certain hint from the cache for extra data via the LSP resolve request. - pub(super) fn spawn_hint_resolve( - &self, - buffer_id: BufferId, - excerpt_id: ExcerptId, - id: InlayId, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { - let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) - && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state + match inlay_hints + .hint_refresh_tasks + .entry(buffer_id) + .or_default() + .entry(applicable_chunks) { - let hint_to_resolve = cached_hint.clone(); - let server_id = *server_id; - cached_hint.resolve_state = ResolveState::Resolving; - drop(guard); - cx.spawn_in(window, async move |editor, cx| { - let resolved_hint_task = editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).buffer(buffer_id)?; - editor.semantics_provider.as_ref()?.resolve_inlay_hint( - hint_to_resolve, - buffer, - server_id, + hash_map::Entry::Occupied(mut o) => { + if invalidate_cache.should_invalidate() || ignore_previous_fetches { + o.get_mut().push(spawn_editor_hints_refresh( + buffer_id, + invalidate_cache, + ignore_previous_fetches, + debounce, + visible_excerpts, + all_affected_buffers.clone(), cx, - ) - })?; - if let Some(resolved_hint_task) = resolved_hint_task { - let mut resolved_hint = - resolved_hint_task.await.context("hint resolve task")?; - editor.read_with(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_by_id.get_mut(&id) - && cached_hint.resolve_state == ResolveState::Resolving - { - resolved_hint.resolve_state = ResolveState::Resolved; - *cached_hint = resolved_hint; - } - } - })?; + )); } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + } + hash_map::Entry::Vacant(v) => { + v.insert(Vec::new()).push(spawn_editor_hints_refresh( + buffer_id, + invalidate_cache, + ignore_previous_fetches, + debounce, + visible_excerpts, + all_affected_buffers.clone(), + cx, + )); + } } } } -} -fn debounce_value(debounce_ms: u64) -> Option { - if debounce_ms > 0 { - Some(Duration::from_millis(debounce_ms)) - } else { - None + pub fn clear_inlay_hints(&mut self, cx: &mut Context) { + let to_remove = self + .visible_inlay_hints(cx) + .into_iter() + .map(|inlay| { + let inlay_id = inlay.id; + if let Some(inlay_hints) = &mut self.inlay_hints { + inlay_hints.added_hints.remove(&inlay_id); + } + inlay_id + }) + .collect::>(); + self.splice_inlays(&to_remove, Vec::new(), cx); } -} - -fn spawn_new_update_tasks( - editor: &mut Editor, - reason: &'static str, - excerpts_to_query: HashMap, Global, Range)>, - invalidate: InvalidationStrategy, - update_cache_version: usize, - cx: &mut Context, -) { - for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in - excerpts_to_query - { - if excerpt_visible_range.is_empty() { - continue; - } - let buffer = excerpt_buffer.read(cx); - let buffer_id = buffer.remote_id(); - let buffer_snapshot = buffer.snapshot(); - if buffer_snapshot - .version() - .changed_since(&new_task_buffer_version) - { - continue; - } - - if let Some(cached_excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) { - let cached_excerpt_hints = cached_excerpt_hints.read(); - let cached_buffer_version = &cached_excerpt_hints.buffer_version; - if cached_excerpt_hints.version > update_cache_version - || cached_buffer_version.changed_since(&new_task_buffer_version) - { - continue; - } - }; - let Some(query_ranges) = editor.buffer.update(cx, |multi_buffer, cx| { - determine_query_ranges( - multi_buffer, - excerpt_id, - &excerpt_buffer, - excerpt_visible_range, - cx, - ) - }) else { - return; - }; - let query = ExcerptQuery { - buffer_id, - excerpt_id, - cache_version: update_cache_version, - invalidate, - reason, + fn refresh_editor_data( + &mut self, + reason: &InlayHintRefreshReason, + cx: &mut Context<'_, Editor>, + ) -> Option { + let visible_inlay_hints = self.visible_inlay_hints(cx); + let Some(inlay_hints) = self.inlay_hints.as_mut() else { + return None; }; - let mut new_update_task = - |query_ranges| new_update_task(query, query_ranges, excerpt_buffer.clone(), cx); - - match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { - hash_map::Entry::Occupied(mut o) => { - o.get_mut().update_cached_tasks( - &buffer_snapshot, - query_ranges, - invalidate, - new_update_task, - ); - } - hash_map::Entry::Vacant(v) => { - v.insert(TasksForRanges::new( - query_ranges.clone(), - new_update_task(query_ranges), - )); + let invalidate_cache = match reason { + InlayHintRefreshReason::ModifiersChanged(enabled) => { + match inlay_hints.modifiers_override(*enabled) { + Some(enabled) => { + if enabled { + InvalidationStrategy::None + } else { + self.clear_inlay_hints(cx); + return None; + } + } + None => return None, + } } - } - } -} - -#[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 into_sorted_query_ranges(self) -> Vec> { - let mut sorted_ranges = Vec::with_capacity( - self.before_visible.len() + self.visible.len() + self.after_visible.len(), - ); - sorted_ranges.extend(self.before_visible); - sorted_ranges.extend(self.visible); - sorted_ranges.extend(self.after_visible); - sorted_ranges - } -} - -fn determine_query_ranges( - multi_buffer: &mut MultiBuffer, - excerpt_id: ExcerptId, - excerpt_buffer: &Entity, - excerpt_visible_range: Range, - cx: &mut Context, -) -> Option { - let buffer = excerpt_buffer.read(cx); - let full_excerpt_range = multi_buffer - .excerpts_for_buffer(buffer.remote_id(), cx) - .into_iter() - .find(|(id, _)| id == &excerpt_id) - .map(|(_, range)| range.context)?; - let snapshot = buffer.snapshot(); - let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; - - let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end { - return None; - } else { - vec![ - buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left)) - ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)), - ] - }; - - let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); - 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 = after_visible_range_start - .saturating_add(excerpt_visible_len) - .min(full_excerpt_range_end_offset) - .min(buffer.len()); - vec![ - buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left)) - ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)), - ] - }; - - let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); - 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 = before_visible_range_end - .saturating_sub(excerpt_visible_len) - .max(full_excerpt_range_start_offset); - vec![ - buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left)) - ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)), - ] - }; - - Some(QueryRanges { - before_visible: before_visible_range, - visible: visible_range, - after_visible: after_visible_range, - }) -} - -const MAX_CONCURRENT_LSP_REQUESTS: usize = 5; -const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; - -fn new_update_task( - query: ExcerptQuery, - query_ranges: QueryRanges, - excerpt_buffer: Entity, - cx: &mut Context, -) -> Task<()> { - cx.spawn(async move |editor, cx| { - let visible_range_update_results = future::join_all( - query_ranges - .visible - .into_iter() - .filter_map(|visible_range| { - let fetch_task = editor - .update(cx, |_, cx| { - fetch_and_update_hints( - excerpt_buffer.clone(), - query, - visible_range.clone(), - query.invalidate.should_invalidate(), - cx, - ) - }) - .log_err()?; - Some(async move { (visible_range, fetch_task.await) }) - }), - ) - .await; - - let hint_delay = cx.background_executor().timer(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, - )); - - let query_range_failed = - |range: &Range, e: anyhow::Error, cx: &mut AsyncApp| { - log::error!("inlay hint update task for range failed: {e:#?}"); - editor - .update(cx, |editor, cx| { - if let Some(task_ranges) = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) + InlayHintRefreshReason::Toggle(enabled) => { + if inlay_hints.toggle(*enabled) { + if *enabled { + InvalidationStrategy::None + } else { + self.clear_inlay_hints(cx); + return None; + } + } else { + return None; + } + } + InlayHintRefreshReason::SettingsChange(new_settings) => { + match inlay_hints.update_settings(*new_settings, visible_inlay_hints) { + ControlFlow::Break(Some(InlaySplice { + to_remove, + to_insert, + })) => { + self.splice_inlays(&to_remove, to_insert, cx); + return None; + } + ControlFlow::Break(None) => return None, + ControlFlow::Continue(splice) => { + if let Some(InlaySplice { + to_remove, + to_insert, + }) = splice { - let buffer_snapshot = excerpt_buffer.read(cx).snapshot(); - task_ranges.invalidate_range(&buffer_snapshot, range); + self.splice_inlays(&to_remove, to_insert, cx); } - }) - .ok() - }; - - for (range, result) in visible_range_update_results { - if let Err(e) = result { - query_range_failed(&range, e, cx); + InvalidationStrategy::None + } + } } - } - - hint_delay.await; - let invisible_range_update_results = future::join_all( - query_ranges - .before_visible - .into_iter() - .chain(query_ranges.after_visible.into_iter()) - .filter_map(|invisible_range| { - let fetch_task = editor - .update(cx, |_, cx| { - fetch_and_update_hints( - excerpt_buffer.clone(), - query, - invisible_range.clone(), - false, // visible screen request already invalidated the entries - cx, - ) - }) - .log_err()?; - Some(async move { (invisible_range, fetch_task.await) }) - }), - ) - .await; - for (range, result) in invisible_range_update_results { - if let Err(e) = result { - query_range_failed(&range, e, cx); + InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { + let to_remove = self + .display_map + .read(cx) + .current_inlays() + .filter_map(|inlay| { + if excerpts_removed.contains(&inlay.position.excerpt_id) { + Some(inlay.id) + } else { + None + } + }) + .collect::>(); + self.splice_inlays(&to_remove, Vec::new(), cx); + return None; } - } - }) -} - -fn fetch_and_update_hints( - excerpt_buffer: Entity, - query: ExcerptQuery, - fetch_range: Range, - invalidate: bool, - cx: &mut Context, -) -> Task> { - cx.spawn(async move |editor, cx|{ - let buffer_snapshot = excerpt_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let (lsp_request_limiter, multi_buffer_snapshot) = - editor.update(cx, |editor, cx| { - let multi_buffer_snapshot = - editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter); - (lsp_request_limiter, multi_buffer_snapshot) - })?; - - 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), + InlayHintRefreshReason::NewLinesShown => InvalidationStrategy::None, + InlayHintRefreshReason::BufferEdited(_) => InvalidationStrategy::BufferEdited, + InlayHintRefreshReason::RefreshRequested(server_id) => { + InvalidationStrategy::RefreshRequested(*server_id) } }; - 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(cx, |editor, cx| { - if got_throttled { - let query_not_around_visible_range = match editor - .visible_excerpts(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) - { - task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); - } - return None; - } - } - let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?; + match &mut self.inlay_hints { + Some(inlay_hints) => { + if !inlay_hints.enabled + && !matches!(reason, InlayHintRefreshReason::ModifiersChanged(_)) + { + return None; + } + } + None => return None, + } - if !editor.registered_buffers.contains_key(&query.buffer_id) - && let Some(project) = editor.project.as_ref() { - project.update(cx, |project, cx| { - editor.registered_buffers.insert( - query.buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } + Some(invalidate_cache) + } - editor - .semantics_provider - .as_ref()? - .inlay_hints(buffer, fetch_range.clone(), cx) - }) - .ok() - .flatten(); + pub(crate) fn visible_inlay_hints(&self, cx: &Context) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_))) + .cloned() + .collect() + } - let cached_excerpt_hints = editor.read_with(cx, |editor, _| { - editor - .inlay_hint_cache - .hints - .get(&query.excerpt_id) - .cloned() - })?; - - let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx).cloned().collect::>())?; - let new_hints = match inlay_hints_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(()), + pub fn update_inlay_link_and_hover_points( + &mut self, + snapshot: &EditorSnapshot, + point_for_position: PointForPosition, + secondary_held: bool, + shift_held: bool, + window: &mut Window, + cx: &mut Context, + ) { + let Some(lsp_store) = self.project().map(|project| project.read(cx).lsp_store()) else { + return; }; - 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 background_fetch_range = fetch_range.clone(); - let new_update = cx.background_spawn(async move { - calculate_hint_updates( - query.excerpt_id, - invalidate, - background_fetch_range, - new_hints, - &background_task_buffer_snapshot, - cached_excerpt_hints, - &visible_hints, + 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), ) - }) - .await; - if let Some(new_update) = new_update { - log::debug!( - "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() + } else { + None + }; + let mut go_to_definition_updated = false; + let mut hover_updated = false; + if let Some(hovered_offset) = hovered_offset { + let buffer_snapshot = self.buffer().read(cx).snapshot(cx); + let previous_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.previous_valid.to_point(snapshot), + Bias::Left, ); - log::trace!("New update: {new_update:?}"); - editor - .update(cx, |editor, cx| { - apply_hint_update( - editor, - new_update, - query, - invalidate, - buffer_snapshot, - multi_buffer_snapshot, - cx, - ); + let next_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.next_valid.to_point(snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = self + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position + .cmp(&previous_valid_anchor, &buffer_snapshot) + .is_lt() }) - .ok(); - } - anyhow::Ok(()) - }) -} - -fn calculate_hint_updates( - excerpt_id: ExcerptId, - invalidate: bool, - fetch_range: Range, - new_excerpt_hints: Vec, - buffer_snapshot: &BufferSnapshot, - cached_excerpt_hints: Option>>, - visible_hints: &[Inlay], -) -> Option { - 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) { - continue; - } - let missing_from_cache = match &cached_excerpt_hints { - Some(cached_excerpt_hints) => { - let cached_excerpt_hints = cached_excerpt_hints.read(); - match cached_excerpt_hints - .ordered_hints - .binary_search_by(|probe| { - cached_excerpt_hints.hints_by_id[probe] - .position - .cmp(&new_hint.position, buffer_snapshot) - }) { - Ok(ix) => { - let mut missing_from_cache = true; - for id in &cached_excerpt_hints.ordered_hints[ix..] { - let cached_hint = &cached_excerpt_hints.hints_by_id[id]; - if new_hint - .position - .cmp(&cached_hint.position, buffer_snapshot) - .is_gt() - { - break; + .take_while(|hint| { + hint.position + .cmp(&next_valid_anchor, &buffer_snapshot) + .is_le() + }) + .max_by_key(|hint| hint.id) + { + if let Some(ResolvedHint::Resolved(cached_hint)) = + hovered_hint.position.buffer_id.and_then(|buffer_id| { + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.resolved_hint(buffer_id, hovered_hint.id, cx) + }) + }) + { + match cached_hint.resolve_state { + ResolveState::Resolved => { + let mut extra_shift_left = 0; + let mut extra_shift_right = 0; + if cached_hint.padding_left { + extra_shift_left += 1; + extra_shift_right += 1; } - if cached_hint == &new_hint { - excerpt_hints_to_persist.insert(*id, cached_hint.kind); - missing_from_cache = false; + if cached_hint.padding_right { + extra_shift_right += 1; } + match cached_hint.label { + InlayHintLabel::String(_) => { + if let Some(tooltip) = cached_hint.tooltip { + hover_popover::hover_at_inlay( + self, + InlayHover { + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + range: InlayHighlight { + inlay: hovered_hint.id, + inlay_position: hovered_hint.position, + range: extra_shift_left + ..hovered_hint.text().len() + + extra_shift_right, + }, + }, + window, + cx, + ); + hover_updated = true; + } + } + InlayHintLabel::LabelParts(label_parts) => { + let hint_start = + snapshot.anchor_to_inlay_offset(hovered_hint.position); + if let Some((hovered_hint_part, part_range)) = + hover_popover::find_hovered_hint_part( + label_parts, + hint_start, + hovered_offset, + ) + { + let highlight_start = + (part_range.start - hint_start).0 + extra_shift_left; + let highlight_end = + (part_range.end - hint_start).0 + extra_shift_right; + let highlight = InlayHighlight { + inlay: hovered_hint.id, + inlay_position: hovered_hint.position, + range: highlight_start..highlight_end, + }; + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_popover::hover_at_inlay( + self, + InlayHover { + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + range: highlight.clone(), + }, + window, + cx, + ); + hover_updated = true; + } + if let Some((language_server_id, location)) = + hovered_hint_part.location + && secondary_held + && !self.has_pending_nonempty_selection() + { + go_to_definition_updated = true; + show_link_definition( + shift_held, + self, + TriggerPoint::InlayHint( + highlight, + location, + language_server_id, + ), + snapshot, + window, + cx, + ); + } + } + } + }; } - missing_from_cache + ResolveState::CanResolve(_, _) => debug_panic!( + "Expected resolved_hint retrieval to return a resolved hint" + ), + ResolveState::Resolving => {} } - Err(_) => true, } } - None => true, - }; - if missing_from_cache { - add_to_cache.push(new_hint); + } + + if !go_to_definition_updated { + self.hide_hovered_link(cx) + } + if !hover_updated { + hover_popover::hover_at(self, None, window, cx); } } - let mut remove_from_visible = HashSet::default(); - let mut remove_from_cache = HashSet::default(); - if invalidate { - remove_from_visible.extend( - visible_hints - .iter() - .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)), - ); + fn inlay_hints_for_buffer( + &mut self, + invalidate_cache: InvalidationStrategy, + ignore_previous_fetches: bool, + buffer_excerpts: VisibleExcerpts, + cx: &mut Context, + ) -> Option, anyhow::Result)>>> { + let semantics_provider = self.semantics_provider()?; + let inlay_hints = self.inlay_hints.as_mut()?; + let buffer_id = buffer_excerpts.buffer.read(cx).remote_id(); + + let new_hint_tasks = semantics_provider + .inlay_hints( + invalidate_cache, + buffer_excerpts.buffer, + buffer_excerpts.ranges, + inlay_hints + .hint_chunk_fetched + .get(&buffer_id) + .filter(|_| !ignore_previous_fetches && !invalidate_cache.should_invalidate()) + .cloned(), + cx, + ) + .unwrap_or_default(); + + let (known_version, known_chunks) = + inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default(); + if buffer_excerpts.buffer_version.changed_since(known_version) { + known_chunks.clear(); + *known_version = buffer_excerpts.buffer_version; + } - if let Some(cached_excerpt_hints) = &cached_excerpt_hints { - let cached_excerpt_hints = cached_excerpt_hints.read(); - remove_from_cache.extend( - cached_excerpt_hints - .ordered_hints + let mut hint_tasks = Vec::new(); + for (row_range, new_hints_task) in new_hint_tasks { + let inserted = known_chunks.insert(row_range.clone()); + if inserted || ignore_previous_fetches || invalidate_cache.should_invalidate() { + hint_tasks.push(cx.spawn(async move |_, _| (row_range, new_hints_task.await))); + } + } + + Some(hint_tasks) + } + + fn apply_fetched_hints( + &mut self, + buffer_id: BufferId, + query_version: Global, + invalidate_cache: InvalidationStrategy, + new_hints: Vec<(Range, anyhow::Result)>, + all_affected_buffers: Arc>>, + cx: &mut Context, + ) { + let visible_inlay_hint_ids = self + .visible_inlay_hints(cx) + .iter() + .filter(|inlay| inlay.position.buffer_id == Some(buffer_id)) + .map(|inlay| inlay.id) + .collect::>(); + let Some(inlay_hints) = &mut self.inlay_hints else { + return; + }; + + let mut hints_to_remove = Vec::new(); + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + + // If we've received hints from the cache, it means `invalidate_cache` had invalidated whatever possible there, + // and most probably there are no more hints with IDs from `visible_inlay_hint_ids` in the cache. + // So, if we hover such hints, no resolve will happen. + // + // Another issue is in the fact that changing one buffer may lead to other buffers' hints changing, so more cache entries may be removed. + // Hence, clear all excerpts' hints in the multi buffer: later, the invalidated ones will re-trigger the LSP query, the rest will be restored + // from the cache. + if invalidate_cache.should_invalidate() { + hints_to_remove.extend(visible_inlay_hint_ids); + } + + let excerpts = self.buffer.read(cx).excerpt_ids(); + let hints_to_insert = new_hints + .into_iter() + .filter_map(|(chunk_range, hints_result)| match hints_result { + Ok(new_hints) => Some(new_hints), + Err(e) => { + log::error!( + "Failed to query inlays for buffer row range {chunk_range:?}, {e:#}" + ); + if let Some((for_version, chunks_fetched)) = + inlay_hints.hint_chunk_fetched.get_mut(&buffer_id) + { + if for_version == &query_version { + chunks_fetched.remove(&chunk_range); + } + } + None + } + }) + .flat_map(|hints| hints.into_values()) + .flatten() + .filter_map(|(hint_id, lsp_hint)| { + if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind) + && inlay_hints + .added_hints + .insert(hint_id, lsp_hint.kind) + .is_none() + { + let position = excerpts.iter().find_map(|excerpt_id| { + multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, lsp_hint.position) + })?; + return Some(Inlay::hint(hint_id, position, &lsp_hint)); + } + None + }) + .collect::>(); + + // We need to invalidate excerpts all buffers with the same language, do that once only, after first new data chunk is inserted. + let all_other_affected_buffers = all_affected_buffers + .lock() + .drain() + .filter(|id| buffer_id != *id) + .collect::>(); + if !all_other_affected_buffers.is_empty() { + hints_to_remove.extend( + self.visible_inlay_hints(cx) .iter() - .filter(|cached_inlay_id| { - !excerpt_hints_to_persist.contains_key(cached_inlay_id) + .filter(|inlay| { + inlay + .position + .buffer_id + .is_none_or(|buffer_id| all_other_affected_buffers.contains(&buffer_id)) }) - .copied(), + .map(|inlay| inlay.id), ); - remove_from_visible.extend(remove_from_cache.iter().cloned()); } - } - if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() { - None - } else { - Some(ExcerptHintsUpdate { - excerpt_id, - remove_from_visible, - remove_from_cache, - add_to_cache, - }) + self.splice_inlays(&hints_to_remove, hints_to_insert, cx); } } -fn contains_position( - range: &Range, - position: language::Anchor, - buffer_snapshot: &BufferSnapshot, -) -> bool { - range.start.cmp(&position, buffer_snapshot).is_le() - && range.end.cmp(&position, buffer_snapshot).is_ge() +#[derive(Debug)] +struct VisibleExcerpts { + excerpts: Vec, + ranges: Vec>, + buffer_version: Global, + buffer: Entity, } -fn apply_hint_update( - editor: &mut Editor, - new_update: ExcerptHintsUpdate, - query: ExcerptQuery, - invalidate: bool, - buffer_snapshot: BufferSnapshot, - multi_buffer_snapshot: MultiBufferSnapshot, - cx: &mut Context, -) { - let cached_excerpt_hints = editor - .inlay_hint_cache - .hints - .entry(new_update.excerpt_id) - .or_insert_with(|| { - Arc::new(RwLock::new(CachedExcerptHints { - version: query.cache_version, - buffer_version: buffer_snapshot.version().clone(), - buffer_id: query.buffer_id, - ordered_hints: Vec::new(), - hints_by_id: HashMap::default(), - })) - }); - let mut cached_excerpt_hints = cached_excerpt_hints.write(); - match query.cache_version.cmp(&cached_excerpt_hints.version) { - cmp::Ordering::Less => return, - cmp::Ordering::Greater | cmp::Ordering::Equal => { - cached_excerpt_hints.version = query.cache_version; +fn spawn_editor_hints_refresh( + buffer_id: BufferId, + invalidate_cache: InvalidationStrategy, + ignore_previous_fetches: bool, + debounce: Option, + buffer_excerpts: VisibleExcerpts, + all_affected_buffers: Arc>>, + cx: &mut Context<'_, Editor>, +) -> Task<()> { + cx.spawn(async move |editor, cx| { + if let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; } - } - let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); - cached_excerpt_hints - .ordered_hints - .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id)); - cached_excerpt_hints - .hints_by_id - .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id)); - let mut splice = InlaySplice::default(); - splice.to_remove.extend(new_update.remove_from_visible); - for new_hint in new_update.add_to_cache { - let insert_position = match cached_excerpt_hints - .ordered_hints - .binary_search_by(|probe| { - cached_excerpt_hints.hints_by_id[probe] - .position - .cmp(&new_hint.position, &buffer_snapshot) - }) { - Ok(i) => { - // When a hint is added to the same position where existing ones are present, - // do not deduplicate it: we split hint queries into non-overlapping ranges - // and each hint batch returned by the server should already contain unique hints. - i + cached_excerpt_hints.ordered_hints[i..].len() + 1 - } - Err(i) => i, + let query_version = buffer_excerpts.buffer_version.clone(); + let Some(hint_tasks) = editor + .update(cx, |editor, cx| { + editor.inlay_hints_for_buffer( + invalidate_cache, + ignore_previous_fetches, + buffer_excerpts, + cx, + ) + }) + .ok() + else { + return; }; - - let new_inlay_id = post_inc(&mut editor.next_inlay_id); - if editor - .inlay_hint_cache - .allowed_hint_kinds - .contains(&new_hint.kind) - && let Some(new_hint_position) = - multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position) - { - splice - .to_insert - .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); - } - let new_id = InlayId::Hint(new_inlay_id); - cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); - if cached_excerpt_hints.ordered_hints.len() <= insert_position { - cached_excerpt_hints.ordered_hints.push(new_id); - } else { - cached_excerpt_hints - .ordered_hints - .insert(insert_position, new_id); - } - - cached_inlays_changed = true; - } - cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); - drop(cached_excerpt_hints); - - 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(); - if excerpt_hints.buffer_id == query.buffer_id - && excerpt_id != &query.excerpt_id - && buffer_snapshot - .version() - .changed_since(&excerpt_hints.buffer_version) - { - outdated_excerpt_caches.insert(*excerpt_id); - splice - .to_remove - .extend(excerpt_hints.ordered_hints.iter().copied()); - } + let hint_tasks = hint_tasks.unwrap_or_default(); + if hint_tasks.is_empty() { + return; } - cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); + let new_hints = join_all(hint_tasks).await; editor - .inlay_hint_cache - .hints - .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); - } - - let InlaySplice { - to_remove, - to_insert, - } = splice; - let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); - if cached_inlays_changed || displayed_inlays_changed { - editor.inlay_hint_cache.version += 1; - } - if displayed_inlays_changed { - editor.splice_inlays(&to_remove, to_insert, cx) - } + .update(cx, |editor, cx| { + editor.apply_fetched_hints( + buffer_id, + query_version, + invalidate_cache, + new_hints, + all_affected_buffers, + cx, + ); + }) + .ok(); + }) } #[cfg(test)] pub mod tests { - use crate::SelectionEffects; use crate::editor_tests::update_test_language_settings; + use crate::inlays::inlay_hints::InlayHintRefreshReason; use crate::scroll::ScrollAmount; - use crate::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang}; - use futures::StreamExt; + use crate::{Editor, SelectionEffects}; + use crate::{ExcerptRange, scroll::Autoscroll}; + use collections::HashSet; + use futures::{StreamExt, future}; use gpui::{AppContext as _, Context, SemanticVersion, TestAppContext, WindowHandle}; use itertools::Itertools as _; + use language::language_settings::InlayHintKind; use language::{Capability, FakeLspAdapter}; use language::{Language, LanguageConfig, LanguageMatcher}; + use languages::rust_lang; use lsp::FakeLanguageServer; + use multi_buffer::MultiBuffer; use parking_lot::Mutex; + use pretty_assertions::assert_eq; use project::{FakeFs, Project}; use serde_json::json; use settings::{AllLanguageSettingsContent, InlayHintSettingsContent, SettingsStore}; + use std::ops::Range; + use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; - use text::Point; + use std::time::Duration; + use text::{OffsetRangeExt, Point}; + use ui::App; use util::path; - - use super::*; + use util::paths::natural_sort; #[gpui::test] async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { @@ -1348,7 +968,110 @@ pub mod tests { Ok(Some(vec![lsp::InlayHint { position: lsp::Position::new(0, i), label: lsp::InlayHintLabel::String(i.to_string()), - kind: None, + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }, + ); + }) + .await; + cx.executor().run_until_parked(); + + editor + .update(cx, |editor, _window, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor, cx), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + allowed_hint_kinds_for_editor(editor), + allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([13..13]) + }); + editor.handle_input("some change", window, cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor, cx), + "Should get new hints after an edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + allowed_hint_kinds_for_editor(editor), + allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + }) + .unwrap(); + + fake_server + .request::(()) + .await + .into_response() + .expect("inlay refresh request failed"); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor, cx), + "Should get new hints after hint refresh/ request" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + allowed_hint_kinds_for_editor(editor), + allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_racy_cache_updates(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + enabled: Some(true), + ..InlayHintSettingsContent::default() + }) + }); + let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| { + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server.set_request_handler::( + move |params, _| { + let task_lsp_request_count = Arc::clone(&lsp_request_count); + async move { + let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1; + assert_eq!( + params.text_document.uri, + lsp::Uri::from_file_path(file_with_hints).unwrap(), + ); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: Some(lsp::InlayHintKind::TYPE), text_edits: None, tooltip: None, padding_left: None, @@ -1360,6 +1083,7 @@ pub mod tests { ); }) .await; + cx.executor().advance_clock(Duration::from_secs(1)); cx.executor().run_until_parked(); editor @@ -1367,64 +1091,41 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its first hints when opening the editor" ); 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, - "Cache should use editor settings to get the allowed hint kinds" - ); }) .unwrap(); + // Emulate simultaneous events: both editing, refresh and, slightly after, scroll updates are triggered. editor .update(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([13..13]) - }); - editor.handle_input("some change", window, cx); + editor.handle_input("foo", window, cx); }) .unwrap(); - cx.executor().run_until_parked(); + cx.executor().advance_clock(Duration::from_millis(5)); editor .update(cx, |editor, _window, cx| { - let expected_hints = vec!["2".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get new hints after an edit" - ); - 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, - "Cache should use editor settings to get the allowed hint kinds" + editor.refresh_inlay_hints( + InlayHintRefreshReason::RefreshRequested(fake_server.server.server_id()), + cx, ); }) .unwrap(); - - fake_server - .request::(()) - .await - .into_response() - .expect("inlay refresh request failed"); + cx.executor().advance_clock(Duration::from_millis(5)); + editor + .update(cx, |editor, _window, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + }) + .unwrap(); + cx.executor().advance_clock(Duration::from_secs(1)); cx.executor().run_until_parked(); editor .update(cx, |editor, _window, cx| { - let expected_hints = vec!["3".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get new hints after hint refresh/ request" - ); + let expected_hints = vec!["2".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor, cx), "Despite multiple simultaneous refreshes, only one inlay hint query should be issued"); 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, - "Cache should use editor settings to get the allowed hint kinds" - ); }) .unwrap(); } @@ -1479,7 +1180,7 @@ pub mod tests { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its first hints when opening the editor" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1508,7 +1209,7 @@ pub mod tests { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should not update hints while the work task is running" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1528,7 +1229,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "New hints should be queried after the work task is done" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1663,7 +1364,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its first hints when opening the editor" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1688,7 +1389,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Markdown editor should have a separate version, repeating Rust editor rules" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1706,15 +1407,10 @@ pub mod tests { cx.executor().run_until_parked(); rs_editor .update(cx, |editor, _window, cx| { - // TODO: Here, we do not get "2", because inserting another language server will trigger `RefreshInlayHints` event from the `LspStore` - // A project is listened in every editor, so each of them will react to this event. - // - // We do not have language server IDs for remote projects, so cannot easily say on the editor level, - // whether we should ignore a particular `RefreshInlayHints` event. - let expected_hints = vec!["3".to_string()]; + let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Rust inlay cache should change after the edit" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1725,7 +1421,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Markdown editor should not be affected by Rust editor changes" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1746,7 +1442,7 @@ pub mod tests { let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Rust editor should not be affected by Markdown editor changes" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1754,10 +1450,10 @@ pub mod tests { .unwrap(); rs_editor .update(cx, |editor, _window, cx| { - let expected_hints = vec!["3".to_string()]; + let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Markdown editor should also change independently" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1852,16 +1548,16 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its first hints when opening the editor" ); assert_eq!( vec!["type hint".to_string(), "other hint".to_string()], visible_hint_labels(editor, cx) ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + allowed_hint_kinds_for_editor(editor), + allowed_hint_kinds, "Cache should use editor settings to get the allowed hint kinds" ); }) @@ -1886,7 +1582,7 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Cached hints should not change due to allowed hint kinds settings update" ); assert_eq!( @@ -1961,7 +1657,7 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" ); assert_eq!( @@ -1969,9 +1665,9 @@ pub mod tests { visible_hint_labels(editor, cx), "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, + allowed_hint_kinds_for_editor(editor), + new_allowed_hint_kinds, "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" ); }).unwrap(); @@ -2003,17 +1699,23 @@ pub mod tests { 2, "Should not load new hints when hints got disabled" ); - assert!( - cached_hint_labels(editor).is_empty(), - "Should clear the cache when hints got disabled" + assert_eq!( + vec![ + "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), + ], + cached_hint_labels(editor, cx), + "Should not clear the cache when hints got disabled" ); - assert!( - visible_hint_labels(editor, cx).is_empty(), + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), "Should clear visible hints when hints got disabled" ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, + allowed_hint_kinds_for_editor(editor), + another_allowed_hint_kinds, "Should update its allowed hint kinds even when hints got disabled" ); }) @@ -2032,8 +1734,15 @@ pub mod tests { 2, "Should not load new hints when they got disabled" ); - assert!(cached_hint_labels(editor).is_empty()); - assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!( + vec![ + "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), + ], + cached_hint_labels(editor, cx) + ); + assert_eq!(Vec::::new(), visible_hint_labels(editor, cx)); }) .unwrap(); @@ -2060,8 +1769,8 @@ pub mod tests { .update(cx, |editor, _, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), - 3, - "Should query for new hints when they got re-enabled" + 2, + "Should not query for new hints when they got re-enabled, as the file version did not change" ); assert_eq!( vec![ @@ -2069,7 +1778,7 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its cached hints fully repopulated after the hints got re-enabled" ); assert_eq!( @@ -2077,9 +1786,9 @@ pub mod tests { visible_hint_labels(editor, cx), "Should get its visible hints repopulated and filtered after the h" ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, + allowed_hint_kinds_for_editor(editor), + final_allowed_hint_kinds, "Cache should update editor settings when hints got re-enabled" ); }) @@ -2095,7 +1804,7 @@ pub mod tests { .update(cx, |editor, _, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), - 4, + 3, "Should query for new hints again" ); assert_eq!( @@ -2104,7 +1813,7 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), ); assert_eq!( vec!["parameter hint".to_string()], @@ -2197,7 +1906,7 @@ pub mod tests { let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get hints from the last edit landed only" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -2243,7 +1952,7 @@ pub mod tests { let expected_hints = vec!["3".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get hints from the last edit landed only" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -2289,7 +1998,7 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, initializer: Some(Box::new({ let lsp_request_ranges = lsp_request_ranges.clone(); @@ -2327,7 +2036,7 @@ pub mod tests { ); } })), - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -2339,55 +2048,20 @@ pub mod tests { .unwrap(); let editor = cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx)); - cx.executor().run_until_parked(); - let _fake_server = fake_servers.next().await.unwrap(); + cx.executor().run_until_parked(); - // 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.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.executor().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 ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert_eq!( + ranges.len(), + 1, + "Should query 1 range initially, but got: {ranges:?}" ); - let expected_initial_query_range_end = - 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, _window, cx| { - let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - 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!(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!["47".to_string(), "94".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should have hints from both LSP requests made for a big file" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); - }).unwrap(); editor .update(cx, |editor, window, cx| { @@ -2402,9 +2076,7 @@ pub mod tests { editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); + cx.executor().advance_clock(Duration::from_millis(100)); cx.executor().run_until_parked(); let visible_range_after_scrolls = editor_visible_range(&editor, cx); let visible_line_count = editor @@ -2427,37 +2099,25 @@ pub mod tests { let first_scroll = &ranges[0]; let second_scroll = &ranges[1]; assert_eq!( - first_scroll.end, second_scroll.start, + first_scroll.end.line, second_scroll.start.line, "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" ); - assert_eq!( - first_scroll.start, expected_initial_query_range_end, - "First scroll should start the query right after the end of the original scroll", - ); - assert_eq!( - second_scroll.end, - lsp::Position::new( - visible_range_after_scrolls.end.row - + visible_line_count.ceil() as u32, - 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![ - "47".to_string(), - "94".to_string(), - "139".to_string(), - "184".to_string(), - ]; assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should have hints from the new LSP response after the edit" + lsp_requests, 3, + "Should query hints initially, and after each scroll (2 times)" + ); + assert_eq!( + vec!["50".to_string(), "100".to_string(), "150".to_string()], + cached_hint_labels(editor, cx), + "Chunks of 50 line width should have been queried each time" + ); + assert_eq!( + vec!["50".to_string(), "100".to_string(), "150".to_string()], + visible_hint_labels(editor, cx), + "Editor should show only hints that it's scrolled to" ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let mut selection_in_cached_range = visible_range_after_scrolls.end; selection_in_cached_range.row -= visible_line_count.ceil() as u32; @@ -2475,9 +2135,6 @@ pub mod tests { ); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); cx.executor().run_until_parked(); editor.update(cx, |_, _, _| { let ranges = lsp_request_ranges @@ -2486,7 +2143,7 @@ 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), 4); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "No new requests should be made when selecting within cached chunks"); }).unwrap(); editor @@ -2494,38 +2151,25 @@ pub mod tests { editor.handle_input("++++more text++++", window, cx); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); cx.executor().run_until_parked(); editor.update(cx, |editor, _window, cx| { let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); ranges.sort_by_key(|r| r.start); - 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 above_query_range = &ranges[0]; - let visible_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, + assert_eq!(ranges.len(), 2, + "On edit, should scroll to selection and query a range around it: that range should split into 2 50 rows wide chunks. Instead, got query ranges {ranges:?}"); + let first_chunk = &ranges[0]; + let second_chunk = &ranges[1]; + assert!(first_chunk.end.line == second_chunk.start.line, + "First chunk {first_chunk:?} should be before second chunk {second_chunk:?}"); + assert!(first_chunk.start.line < selection_in_cached_range.row, "Hints should be queried with the selected range after the query range start"); - 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!(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!(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"); 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!["67".to_string(), "115".to_string(), "163".to_string()]; - assert_eq!(expected_hints, cached_hint_labels(editor), - "Should have hints from the new LSP response after the edit"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(lsp_requests, 5, "Two chunks should be re-queried"); + assert_eq!(vec!["100".to_string(), "150".to_string()], cached_hint_labels(editor, cx), + "Should have (less) hints from the new LSP response after the edit"); + assert_eq!(vec!["100".to_string(), "150".to_string()], visible_hint_labels(editor, cx), "Should show only visible hints (in the center) from the new cached set"); }).unwrap(); } @@ -2534,7 +2178,7 @@ pub mod tests { cx: &mut gpui::TestAppContext, ) -> Range { let ranges = editor - .update(cx, |editor, _window, cx| editor.visible_excerpts(None, cx)) + .update(cx, |editor, _window, cx| editor.visible_excerpts(cx)) .unwrap(); assert_eq!( ranges.len(), @@ -2543,14 +2187,7 @@ pub mod tests { ); let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap(); excerpt_buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let start = buffer - .anchor_before(excerpt_visible_range.start) - .to_point(&snapshot); - let end = buffer - .anchor_after(excerpt_visible_range.end) - .to_point(&snapshot); - start..end + excerpt_visible_range.to_point(&buffer.snapshot()) }) } @@ -2590,9 +2227,9 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -2724,7 +2361,7 @@ pub mod tests { ]; assert_eq!( expected_hints, - sorted_cached_hint_labels(editor), + sorted_cached_hint_labels(editor, cx), "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -2749,7 +2386,7 @@ pub mod tests { SelectionEffects::scroll(Autoscroll::Next), window, cx, - |s| s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]), + |s| s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]), ); }) .unwrap(); @@ -2764,7 +2401,7 @@ pub mod tests { "main hint #4".to_string(), "main hint #5".to_string(), ]; - assert_eq!(expected_hints, sorted_cached_hint_labels(editor), + assert_eq!(expected_hints, sorted_cached_hint_labels(editor, cx), "New hints are not shown right after scrolling, we need to wait for the buffer to be registered"); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }) @@ -2783,10 +2420,17 @@ pub mod tests { "other hint #0".to_string(), "other hint #1".to_string(), "other hint #2".to_string(), + "other hint #3".to_string(), ]; - assert_eq!(expected_hints, sorted_cached_hint_labels(editor), + assert_eq!( + expected_hints, + sorted_cached_hint_labels(editor, cx), "After scrolling to the new buffer and waiting for it to be registered, new hints should appear"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "Editor should show only visible hints", + ); }) .unwrap(); @@ -2800,9 +2444,7 @@ pub mod tests { ); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); + cx.executor().advance_clock(Duration::from_millis(100)); cx.executor().run_until_parked(); editor .update(cx, |editor, _window, cx| { @@ -2820,9 +2462,16 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_hints, sorted_cached_hint_labels(editor), - "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + expected_hints, + sorted_cached_hint_labels(editor, cx), + "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "Editor shows only hints for excerpts that were visible when scrolling" + ); }) .unwrap(); @@ -2836,9 +2485,6 @@ pub mod tests { ); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); cx.executor().run_until_parked(); editor .update(cx, |editor, _window, cx| { @@ -2856,44 +2502,301 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_hints, sorted_cached_hint_labels(editor), - "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + expected_hints, + sorted_cached_hint_labels(editor, cx), + "After multibuffer was scrolled to the end, further scrolls up should not bring more hints" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + ); }) .unwrap(); - editor_edited.store(true, Ordering::Release); + // We prepare to change the scrolling on edit, but do not scroll yet editor .update(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]) }); + }) + .unwrap(); + cx.executor().run_until_parked(); + // Edit triggers the scrolling too + editor_edited.store(true, Ordering::Release); + editor + .update(cx, |editor, window, cx| { editor.handle_input("++++more text++++", window, cx); }) .unwrap(); cx.executor().run_until_parked(); - // Wait again to trigger the inlay hints fetch on scroll - cx.executor().advance_clock(Duration::from_millis(100)); - cx.executor().run_until_parked(); + // Wait again to trigger the inlay hints fetch on scroll + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + let expected_hints = vec![ + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), + "other hint(edited) #2".to_string(), + "other hint(edited) #3".to_string(), + ]; + assert_eq!( + expected_hints, + sorted_cached_hint_labels(editor, cx), + "After multibuffer edit, editor gets scrolled back to the last selection; \ + all hints should be invalidated and required for all of its visible excerpts" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "All excerpts should get their hints" + ); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_editing_in_multi_buffer(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + enabled: Some(true), + ..InlayHintSettingsContent::default() + }) + }); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/a"), + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..200).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "lib.rs": r#"let a = 1; +let b = 2; +let c = 3;"# + }), + ) + .await; + + let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); + + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let language = rust_lang(); + language_registry.add(language); + + let closure_ranges_fetched = lsp_request_ranges.clone(); + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + initializer: Some(Box::new(move |fake_server| { + let closure_ranges_fetched = closure_ranges_fetched.clone(); + fake_server.set_request_handler::( + move |params, _| { + let closure_ranges_fetched = closure_ranges_fetched.clone(); + async move { + let prefix = if params.text_document.uri + == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap() + { + closure_ranges_fetched + .lock() + .push(("main.rs", params.range)); + "main.rs" + } else if params.text_document.uri + == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap() + { + closure_ranges_fetched.lock().push(("lib.rs", params.range)); + "lib.rs" + } else { + panic!("Unexpected file path {:?}", params.text_document.uri); + }; + Ok(Some( + (params.range.start.line..params.range.end.line) + .map(|row| lsp::InlayHint { + position: lsp::Position::new(row, 0), + label: lsp::InlayHintLabel::String(format!( + "{prefix} Inlay hint #{row}" + )), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }) + .collect(), + )) + } + }, + ); + })), + ..FakeLspAdapter::default() + }, + ); + + let (buffer_1, _handle_1) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx) + }) + .await + .unwrap(); + let (buffer_2, _handle_2) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/a/lib.rs"), cx) + }) + .await + .unwrap(); + let multi_buffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + // Have first excerpt to spawn over 2 chunks (50 lines each). + ExcerptRange::new(Point::new(49, 0)..Point::new(53, 0)), + // Have 2nd excerpt to be in the 2nd chunk only. + ExcerptRange::new(Point::new(70, 0)..Point::new(73, 0)), + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(4, 0))], + cx, + ); + multibuffer + }); + + let editor = cx.add_window(|window, cx| { + let mut editor = + Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx); + editor.change_selections(SelectionEffects::default(), window, cx, |s| { + s.select_ranges([0..0]) + }); + editor + }); + + let _fake_server = fake_servers.next().await.unwrap(); + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); + + assert_eq!( + vec![ + ( + "lib.rs", + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10)) + ), + ( + "main.rs", + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0)) + ), + ( + "main.rs", + lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 11)) + ), + ], + lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start)) + .collect::>(), + "For large buffers, should query chunks that cover both visible excerpt" + ); + editor + .update(cx, |editor, _window, cx| { + assert_eq!( + (0..2) + .map(|i| format!("lib.rs Inlay hint #{i}")) + .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}"))) + .collect::>(), + sorted_cached_hint_labels(editor, cx), + "Both chunks should provide their inlay hints" + ); + assert_eq!( + vec![ + "main.rs Inlay hint #49".to_owned(), + "main.rs Inlay hint #50".to_owned(), + "main.rs Inlay hint #51".to_owned(), + "main.rs Inlay hint #52".to_owned(), + "main.rs Inlay hint #53".to_owned(), + "main.rs Inlay hint #70".to_owned(), + "main.rs Inlay hint #71".to_owned(), + "main.rs Inlay hint #72".to_owned(), + "main.rs Inlay hint #73".to_owned(), + "lib.rs Inlay hint #0".to_owned(), + "lib.rs Inlay hint #1".to_owned(), + ], + visible_hint_labels(editor, cx), + "Only hints from visible excerpt should be added into the editor" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, window, cx| { + editor.handle_input("a", window, cx); + }) + .unwrap(); + cx.executor().advance_clock(Duration::from_millis(1000)); + cx.executor().run_until_parked(); + assert_eq!( + vec![ + ( + "lib.rs", + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10)) + ), + ( + "main.rs", + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0)) + ), + ( + "main.rs", + lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 11)) + ), + ], + lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start)) + .collect::>(), + "Same chunks should be re-queried on edit" + ); editor .update(cx, |editor, _window, cx| { - let expected_hints = vec![ - "main hint(edited) #0".to_string(), - "main hint(edited) #1".to_string(), - "main hint(edited) #2".to_string(), - "main hint(edited) #3".to_string(), - "main hint(edited) #4".to_string(), - "main hint(edited) #5".to_string(), - "other hint(edited) #0".to_string(), - "other hint(edited) #1".to_string(), - ]; assert_eq!( - expected_hints, - sorted_cached_hint_labels(editor), - "After multibuffer edit, editor gets scrolled back to the last selection; \ - all hints should be invalidated and required for all of its visible excerpts" + (0..2) + .map(|i| format!("lib.rs Inlay hint #{i}")) + .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}"))) + .collect::>(), + sorted_cached_hint_labels(editor, cx), + "Same hints should be re-inserted after the edit" + ); + assert_eq!( + vec![ + "main.rs Inlay hint #49".to_owned(), + "main.rs Inlay hint #50".to_owned(), + "main.rs Inlay hint #51".to_owned(), + "main.rs Inlay hint #52".to_owned(), + "main.rs Inlay hint #53".to_owned(), + "main.rs Inlay hint #70".to_owned(), + "main.rs Inlay hint #71".to_owned(), + "main.rs Inlay hint #72".to_owned(), + "main.rs Inlay hint #73".to_owned(), + "lib.rs Inlay hint #0".to_owned(), + "lib.rs Inlay hint #1".to_owned(), + ], + visible_hint_labels(editor, cx), + "Same hints should be re-inserted into the editor after the edit" ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }) .unwrap(); } @@ -2933,9 +2836,9 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -3040,18 +2943,29 @@ pub mod tests { }) .next() .await; + cx.executor().advance_clock(Duration::from_millis(100)); cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { assert_eq!( - vec!["main hint #0".to_string(), "other hint #0".to_string()], - sorted_cached_hint_labels(editor), - "Cache should update for both excerpts despite hints display was disabled" + vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + ], + sorted_cached_hint_labels(editor, cx), + "Cache should update for both excerpts despite hints display was disabled; after selecting 2nd buffer, it's now registered with the langserever and should get its hints" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "All hints are disabled and should not be shown despite being present in the cache" ); - assert!( - visible_hint_labels(editor, cx).is_empty(), - "All hints are disabled and should not be shown despite being present in the cache" - ); }) .unwrap(); @@ -3066,9 +2980,14 @@ pub mod tests { editor .update(cx, |editor, _, cx| { assert_eq!( - vec!["main hint #0".to_string()], - cached_hint_labels(editor), - "For the removed excerpt, should clean corresponding cached hints" + vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + ], + cached_hint_labels(editor, cx), + "For the removed excerpt, should clean corresponding cached hints as its buffer was dropped" ); assert!( visible_hint_labels(editor, cx).is_empty(), @@ -3093,16 +3012,22 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { - let expected_hints = vec!["main hint #0".to_string()]; assert_eq!( - expected_hints, - cached_hint_labels(editor), + vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + ], + cached_hint_labels(editor, cx), "Hint display settings change should not change the cache" ); assert_eq!( - expected_hints, + vec![ + "main hint #0".to_string(), + ], visible_hint_labels(editor, cx), - "Settings change should make cached hints visible" + "Settings change should make cached hints visible, but only the visible ones, from the remaining excerpt" ); }) .unwrap(); @@ -3143,7 +3068,7 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, initializer: Some(Box::new(move |fake_server| { let lsp_request_count = Arc::new(AtomicU32::new(0)); @@ -3170,7 +3095,7 @@ pub mod tests { }, ); })), - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -3195,7 +3120,7 @@ pub mod tests { editor .update(cx, |editor, _, cx| { let expected_hints = vec!["1".to_string()]; - assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, cached_hint_labels(editor, cx)); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }) .unwrap(); @@ -3228,7 +3153,7 @@ pub mod tests { lsp::Uri::from_file_path(file_with_hints).unwrap(), ); - let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1; + let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1; Ok(Some(vec![lsp::InlayHint { position: lsp::Position::new(0, i), label: lsp::InlayHintLabel::String(i.to_string()), @@ -3257,7 +3182,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should display inlays after toggle despite them disabled in settings" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -3272,11 +3197,16 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { - assert!( - cached_hint_labels(editor).is_empty(), + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Cache does not change because of toggles in the editor" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), "Should clear hints after 2nd toggle" ); - assert!(visible_hint_labels(editor, cx).is_empty()); }) .unwrap(); @@ -3296,11 +3226,11 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { - let expected_hints = vec!["2".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), - "Should query LSP hints for the 2nd time after enabling hints in settings" + cached_hint_labels(editor, cx), + "Should not query LSP hints after enabling hints in settings, as file version is the same" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }) @@ -3314,11 +3244,16 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { - assert!( - cached_hint_labels(editor).is_empty(), + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Cache does not change because of toggles in the editor" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), "Should clear hints after enabling in settings and a 3rd toggle" ); - assert!(visible_hint_labels(editor, cx).is_empty()); }) .unwrap(); @@ -3329,16 +3264,242 @@ pub mod tests { .unwrap(); cx.executor().run_until_parked(); editor.update(cx, |editor, _, cx| { - let expected_hints = vec!["3".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), - "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" + cached_hint_labels(editor,cx), + "Should not query LSP hints after enabling hints in settings and toggling them back on" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }).unwrap(); } + #[gpui::test] + async fn test_modifiers_change(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), + toggle_on_modifiers_press: None, + }) + }); + + let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| { + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server.set_request_handler::( + move |params, _| { + let lsp_request_count = lsp_request_count.clone(); + async move { + assert_eq!( + params.text_document.uri, + lsp::Uri::from_file_path(file_with_hints).unwrap(), + ); + + let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }, + ); + }) + .await; + + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Should display inlays after toggle despite them disabled in settings" + ); + assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx)); + }) + .unwrap(); + + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Nothing happens with the cache on modifiers change" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "On modifiers change and hints toggled on, should hide editor inlays" + ); + }) + .unwrap(); + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx)); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "Nothing changes on consequent modifiers change of the same kind" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "When modifiers change is off, no extra requests are sent" + ); + assert_eq!( + vec!["1".to_string()], + visible_hint_labels(editor, cx), + "When modifiers change is off, hints are back into the editor" + ); + }) + .unwrap(); + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx)); + assert_eq!( + vec!["1".to_string()], + visible_hint_labels(editor, cx), + "Nothing changes on consequent modifiers change of the same kind (2)" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, window, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx) + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Nothing happens with the cache on modifiers change" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "When toggled off, should hide editor inlays" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Nothing happens with the cache on modifiers change" + ); + assert_eq!( + vec!["1".to_string()], + visible_hint_labels(editor, cx), + "On modifiers change & hints toggled off, should show editor inlays" + ); + }) + .unwrap(); + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx)); + assert_eq!( + vec!["1".to_string()], + visible_hint_labels(editor, cx), + "Nothing changes on consequent modifiers change of the same kind" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "When modifiers change is off, no extra requests are sent" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "When modifiers change is off, editor hints are back into their toggled off state" + ); + }) + .unwrap(); + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx)); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "Nothing changes on consequent modifiers change of the same kind (3)" + ); + }) + .unwrap(); + } + #[gpui::test] async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { @@ -3485,7 +3646,7 @@ pub mod tests { ]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Editor inlay hints should repeat server's order when placed at the same spot" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -3533,10 +3694,10 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, initializer: Some(Box::new(move |server| initialize(server, file_path))), - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -3551,7 +3712,7 @@ pub mod tests { editor .update(cx, |editor, _, cx| { - assert!(cached_hint_labels(editor).is_empty()); + assert!(cached_hint_labels(editor, cx).is_empty()); assert!(visible_hint_labels(editor, cx).is_empty()); }) .unwrap(); @@ -3563,36 +3724,51 @@ pub mod tests { // Inlay hints in the cache are stored per excerpt as a key, and those keys are guaranteed to be ordered same as in the multi buffer. // Ensure a stable order for testing. - fn sorted_cached_hint_labels(editor: &Editor) -> Vec { - let mut labels = cached_hint_labels(editor); - labels.sort(); + fn sorted_cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec { + let mut labels = cached_hint_labels(editor, cx); + labels.sort_by(|a, b| natural_sort(a, b)); labels } - pub fn cached_hint_labels(editor: &Editor) -> Vec { - let mut labels = Vec::new(); - for excerpt_hints in editor.inlay_hint_cache().hints.values() { - let excerpt_hints = excerpt_hints.read(); - for id in &excerpt_hints.ordered_hints { - let hint = &excerpt_hints.hints_by_id[id]; - let mut label = hint.text().to_string(); - if hint.padding_left { - label.insert(0, ' '); - } - if hint.padding_right { - label.push_str(" "); - } - labels.push(label); - } + pub fn cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + + let mut all_cached_labels = Vec::new(); + let mut all_fetched_hints = Vec::new(); + for buffer in editor.buffer.read(cx).all_buffers() { + lsp_store.update(cx, |lsp_store, cx| { + let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints(); + all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| { + let mut label = hint.text().to_string(); + if hint.padding_left { + label.insert(0, ' '); + } + if hint.padding_right { + label.push_str(" "); + } + label + })); + all_fetched_hints.extend(hints.all_fetched_hints()); + }); } - labels + all_cached_labels } pub fn visible_hint_labels(editor: &Editor, cx: &Context) -> Vec { editor .visible_inlay_hints(cx) + .into_iter() .map(|hint| hint.text().to_string()) .collect() } + + fn allowed_hint_kinds_for_editor(editor: &Editor) -> HashSet> { + editor + .inlay_hints + .as_ref() + .unwrap() + .allowed_hint_kinds + .clone() + } } diff --git a/crates/editor/src/lsp_colors.rs b/crates/editor/src/lsp_colors.rs index e99cab2aa938614be5478bdf17ef78b1f626a6f2..050363f219ee5579a73cf168cce82778df8810ab 100644 --- a/crates/editor/src/lsp_colors.rs +++ b/crates/editor/src/lsp_colors.rs @@ -6,15 +6,15 @@ use gpui::{Hsla, Rgba, Task}; use itertools::Itertools; use language::point_from_lsp; use multi_buffer::Anchor; -use project::DocumentColor; +use project::{DocumentColor, InlayId}; use settings::Settings as _; use text::{Bias, BufferId, OffsetRangeExt as _}; use ui::{App, Context, Window}; use util::post_inc; use crate::{ - DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT, InlayId, - InlaySplice, RangeToAnchorExt, display_map::Inlay, editor_settings::DocumentColorsRenderMode, + DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT, + InlaySplice, RangeToAnchorExt, editor_settings::DocumentColorsRenderMode, inlays::Inlay, }; #[derive(Debug)] @@ -164,7 +164,7 @@ impl Editor { } let visible_buffers = self - .visible_excerpts(None, cx) + .visible_excerpts(cx) .into_values() .map(|(buffer, ..)| buffer) .filter(|editor_buffer| { @@ -400,8 +400,7 @@ impl Editor { } if colors.render_mode == DocumentColorsRenderMode::Inlay - && (!colors_splice.to_insert.is_empty() - || !colors_splice.to_remove.is_empty()) + && !colors_splice.is_empty() { editor.splice_inlays(&colors_splice.to_remove, colors_splice.to_insert, cx); updated = true; diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 486a14e3741989c1632e361e6ae6324d697cf2c7..418fa4fcb442b1de133972457497c0e592e77d15 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -872,7 +872,7 @@ mod tests { use super::*; use crate::{ Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, MultiBuffer, - display_map::Inlay, + inlays::Inlay, test::{editor_test_context::EditorTestContext, marked_display_snapshot}, }; use gpui::{AppContext as _, font, px}; diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 2d4710a8d44a023f0c3206ad0c327a34c36fdac4..d32c0412e3707de2fb20be96a4472ec82d59726a 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -1,14 +1,14 @@ use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider}; use buffer_diff::BufferDiff; -use collections::HashSet; +use collections::{HashMap, HashSet}; use futures::{channel::mpsc, future::join_all}; use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task}; -use language::{Buffer, BufferEvent, Capability}; +use language::{Buffer, BufferEvent, BufferRow, Capability}; use multi_buffer::{ExcerptRange, MultiBuffer}; -use project::Project; +use project::{InvalidationStrategy, Project, lsp_store::CacheInlayHints}; use smol::stream::StreamExt; use std::{any::TypeId, ops::Range, rc::Rc, time::Duration}; -use text::ToOffset; +use text::{BufferId, ToOffset}; use ui::{ButtonLike, KeyBinding, prelude::*}; use workspace::{ Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -436,14 +436,34 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { self.0.hover(&buffer, position, cx) } + fn applicable_inlay_chunks( + &self, + buffer_id: BufferId, + ranges: &[Range], + cx: &App, + ) -> Vec> { + self.0.applicable_inlay_chunks(buffer_id, ranges, cx) + } + + fn invalidate_inlay_hints(&self, for_buffers: &HashSet, cx: &mut App) { + self.0.invalidate_inlay_hints(for_buffers, cx); + } + fn inlay_hints( &self, + invalidate: InvalidationStrategy, buffer: Entity, - range: Range, + ranges: Vec>, + known_chunks: Option<(clock::Global, HashSet>)>, cx: &mut App, - ) -> Option>>> { - let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?; - self.0.inlay_hints(buffer, range, cx) + ) -> Option, Task>>> { + let positions = ranges + .iter() + .flat_map(|range| [range.start, range.end]) + .collect::>(); + let buffer = self.to_base(&buffer, &positions, cx)?; + self.0 + .inlay_hints(invalidate, buffer, ranges, known_chunks, cx) } fn inline_values( @@ -455,17 +475,6 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { None } - fn resolve_inlay_hint( - &self, - hint: project::InlayHint, - buffer: Entity, - server_id: lsp::LanguageServerId, - cx: &mut App, - ) -> Option>> { - let buffer = self.to_base(&buffer, &[], cx)?; - self.0.resolve_inlay_hint(hint, buffer, server_id, cx) - } - fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { if let Some(buffer) = self.to_base(buffer, &[], cx) { self.0.supports_inlay_hints(&buffer, cx) diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 72060a11f07d297f578f933b0f6fd809dc915bb5..5a850bf4cff924b85ea5599c3d75c2b602b4dd1d 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -6,6 +6,7 @@ use std::{ }; use anyhow::Result; +use language::rust_lang; use serde_json::json; use crate::{Editor, ToPoint}; @@ -32,55 +33,6 @@ pub struct EditorLspTestContext { pub buffer_lsp_url: lsp::Uri, } -pub(crate) fn rust_lang() -> Arc { - let language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()], - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_queries(LanguageQueries { - indents: Some(Cow::from(indoc! {r#" - [ - ((where_clause) _ @end) - (field_expression) - (call_expression) - (assignment_expression) - (let_declaration) - (let_chain) - (await_expression) - ] @indent - - (_ "[" "]" @end) @indent - (_ "<" ">" @end) @indent - (_ "{" "}" @end) @indent - (_ "(" ")" @end) @indent"#})), - brackets: Some(Cow::from(indoc! {r#" - ("(" @open ")" @close) - ("[" @open "]" @close) - ("{" @open "}" @close) - ("<" @open ">" @close) - ("\"" @open "\"" @close) - (closure_parameters "|" @open "|" @close)"#})), - text_objects: Some(Cow::from(indoc! {r#" - (function_item - body: (_ - "{" - (_)* @function.inside - "}" )) @function.around - "#})), - ..Default::default() - }) - .expect("Could not parse queries"); - Arc::new(language) -} - #[cfg(test)] pub(crate) fn git_commit_lang() -> Arc { Arc::new(Language::new( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2d1a274381224978246db618301606caf44a60cb..e3fb6733dd5176906f0a9a9d208305d67470ba15 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -2600,6 +2600,65 @@ pub fn range_from_lsp(range: lsp::Range) -> Range> { start..end } +#[doc(hidden)] +#[cfg(any(test, feature = "test-support"))] +pub fn rust_lang() -> Arc { + use std::borrow::Cow; + + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()], + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_queries(LanguageQueries { + indents: Some(Cow::from( + r#" +[ + ((where_clause) _ @end) + (field_expression) + (call_expression) + (assignment_expression) + (let_declaration) + (let_chain) + (await_expression) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent"#, + )), + brackets: Some(Cow::from( + r#" +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) +(closure_parameters "|" @open "|" @close)"#, + )), + text_objects: Some(Cow::from( + r#" +(function_item + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + "#, + )), + ..LanguageQueries::default() + }) + .expect("Could not parse queries"); + Arc::new(language) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 7d5bf2fcd514d081260e4dbe3d9c3521d2629e17..55742c284ddcc7dfa6669ea3924fc60a77b2e1ab 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -234,7 +234,7 @@ pub(crate) struct OnTypeFormatting { pub push_to_history: bool, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub(crate) struct InlayHints { pub range: Range, } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e8eaa493de9dc14493c95307b42f04711b4eaca0..dc082453fd74d9d6e046e99e1e75de0f7e4c544e 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -16,16 +16,20 @@ pub mod lsp_ext_command; pub mod rust_analyzer_ext; pub mod vue_language_server_ext; +mod inlay_hint_cache; + +use self::inlay_hint_cache::BufferInlayHints; use crate::{ CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse, - CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, - LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath, + CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, InlayId, LocationLink, + LspAction, LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, buffer_store::{BufferStore, BufferStoreEvent}, environment::ProjectEnvironment, lsp_command::{self, *}, lsp_store::{ self, + inlay_hint_cache::BufferChunk, log_store::{GlobalLogStore, LanguageServerKind}, }, manifest_tree::{ @@ -57,7 +61,7 @@ use gpui::{ use http_client::HttpClient; use itertools::Itertools as _; use language::{ - Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, + Bias, BinaryStatus, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, LspInstaller, ManifestDelegate, ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, @@ -85,7 +89,7 @@ use parking_lot::Mutex; use postage::{mpsc, sink::Sink, stream::Stream, watch}; use rand::prelude::*; use rpc::{ - AnyProtoClient, + AnyProtoClient, ErrorCode, ErrorExt as _, proto::{LspRequestId, LspRequestMessage as _}, }; use serde::Serialize; @@ -106,11 +110,14 @@ use std::{ path::{self, Path, PathBuf}, pin::pin, rc::Rc, - sync::Arc, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, time::{Duration, Instant}, }; use sum_tree::Dimensions; -use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _}; +use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, Point, ToPoint as _}; use util::{ ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into, @@ -121,6 +128,7 @@ use util::{ pub use fs::*; pub use language::Location; +pub use lsp_store::inlay_hint_cache::{CacheInlayHints, InvalidationStrategy}; #[cfg(any(test, feature = "test-support"))] pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; pub use worktree::{ @@ -565,8 +573,7 @@ impl LocalLspStore { } fn setup_lsp_messages( - this: WeakEntity, - + lsp_store: WeakEntity, language_server: &LanguageServer, delegate: Arc, adapter: Arc, @@ -576,7 +583,7 @@ impl LocalLspStore { language_server .on_notification::({ let adapter = adapter.clone(); - let this = this.clone(); + let this = lsp_store.clone(); move |mut params, cx| { let adapter = adapter.clone(); if let Some(this) = this.upgrade() { @@ -620,8 +627,7 @@ impl LocalLspStore { .on_request::({ let adapter = adapter.adapter.clone(); let delegate = delegate.clone(); - let this = this.clone(); - + let this = lsp_store.clone(); move |params, cx| { let adapter = adapter.clone(); let delegate = delegate.clone(); @@ -666,7 +672,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |_, cx| { let this = this.clone(); let cx = cx.clone(); @@ -694,7 +700,7 @@ impl LocalLspStore { // to these requests when initializing. language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { let this = this.clone(); let mut cx = cx.clone(); @@ -715,7 +721,7 @@ impl LocalLspStore { language_server .on_request::({ - let lsp_store = this.clone(); + let lsp_store = lsp_store.clone(); move |params, cx| { let lsp_store = lsp_store.clone(); let mut cx = cx.clone(); @@ -744,7 +750,7 @@ impl LocalLspStore { language_server .on_request::({ - let lsp_store = this.clone(); + let lsp_store = lsp_store.clone(); move |params, cx| { let lsp_store = lsp_store.clone(); let mut cx = cx.clone(); @@ -773,7 +779,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { let mut cx = cx.clone(); let this = this.clone(); @@ -792,18 +798,22 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let lsp_store = lsp_store.clone(); move |(), cx| { - let this = this.clone(); + let this = lsp_store.clone(); let mut cx = cx.clone(); async move { - this.update(&mut cx, |this, cx| { - cx.emit(LspStoreEvent::RefreshInlayHints); - this.downstream_client.as_ref().map(|(client, project_id)| { - client.send(proto::RefreshInlayHints { - project_id: *project_id, + this.update(&mut cx, |lsp_store, cx| { + cx.emit(LspStoreEvent::RefreshInlayHints(server_id)); + lsp_store + .downstream_client + .as_ref() + .map(|(client, project_id)| { + client.send(proto::RefreshInlayHints { + project_id: *project_id, + server_id: server_id.to_proto(), + }) }) - }) })? .transpose()?; Ok(()) @@ -814,7 +824,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |(), cx| { let this = this.clone(); let mut cx = cx.clone(); @@ -836,7 +846,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |(), cx| { let this = this.clone(); let mut cx = cx.clone(); @@ -862,7 +872,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); let name = name.to_string(); move |params, cx| { let this = this.clone(); @@ -900,7 +910,7 @@ impl LocalLspStore { .detach(); language_server .on_notification::({ - let this = this.clone(); + let this = lsp_store.clone(); let name = name.to_string(); move |params, cx| { let this = this.clone(); @@ -932,7 +942,7 @@ impl LocalLspStore { language_server .on_notification::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { @@ -951,7 +961,7 @@ impl LocalLspStore { language_server .on_notification::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { if let Some(this) = this.upgrade() { this.update(cx, |_, cx| { @@ -969,7 +979,7 @@ impl LocalLspStore { language_server .on_notification::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { let mut cx = cx.clone(); if let Some(this) = this.upgrade() { @@ -988,10 +998,10 @@ impl LocalLspStore { }) .detach(); - vue_language_server_ext::register_requests(this.clone(), language_server); - json_language_server_ext::register_requests(this.clone(), language_server); - rust_analyzer_ext::register_notifications(this.clone(), language_server); - clangd_ext::register_notifications(this, language_server, adapter); + vue_language_server_ext::register_requests(lsp_store.clone(), language_server); + json_language_server_ext::register_requests(lsp_store.clone(), language_server); + rust_analyzer_ext::register_notifications(lsp_store.clone(), language_server); + clangd_ext::register_notifications(lsp_store, language_server, adapter); } fn shutdown_language_servers_on_quit( @@ -3498,9 +3508,55 @@ pub struct LspStore { diagnostic_summaries: HashMap, HashMap>>, pub lsp_server_capabilities: HashMap, - lsp_document_colors: HashMap, - lsp_code_lens: HashMap, - running_lsp_requests: HashMap>)>, + lsp_data: HashMap, + next_hint_id: Arc, +} + +#[derive(Debug)] +pub struct BufferLspData { + buffer_version: Global, + document_colors: Option, + code_lens: Option, + inlay_hints: BufferInlayHints, + lsp_requests: HashMap>>, + chunk_lsp_requests: HashMap>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct LspKey { + request_type: TypeId, + server_queried: Option, +} + +impl BufferLspData { + fn new(buffer: &Entity, cx: &mut App) -> Self { + Self { + buffer_version: buffer.read(cx).version(), + document_colors: None, + code_lens: None, + inlay_hints: BufferInlayHints::new(buffer, cx), + lsp_requests: HashMap::default(), + chunk_lsp_requests: HashMap::default(), + } + } + + fn remove_server_data(&mut self, for_server: LanguageServerId) { + if let Some(document_colors) = &mut self.document_colors { + document_colors.colors.remove(&for_server); + document_colors.cache_version += 1; + } + + if let Some(code_lens) = &mut self.code_lens { + code_lens.lens.remove(&for_server); + } + + self.inlay_hints.remove_server_data(for_server); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn inlay_hints(&self) -> &BufferInlayHints { + &self.inlay_hints + } } #[derive(Debug, Default, Clone)] @@ -3514,7 +3570,6 @@ type CodeLensTask = Shared>, Arc #[derive(Debug, Default)] struct DocumentColorData { - colors_for_version: Global, colors: HashMap>, cache_version: usize, colors_update: Option<(Global, DocumentColorTask)>, @@ -3522,7 +3577,6 @@ struct DocumentColorData { #[derive(Debug, Default)] struct CodeLensData { - lens_for_version: Global, lens: HashMap>, update: Option<(Global, CodeLensTask)>, } @@ -3543,7 +3597,7 @@ pub enum LspStoreEvent { new_language: Option>, }, Notification(String), - RefreshInlayHints, + RefreshInlayHints(LanguageServerId), RefreshCodeLens, DiagnosticsUpdated { server_id: LanguageServerId, @@ -3615,7 +3669,6 @@ impl LspStore { client.add_entity_request_handler(Self::handle_apply_code_action_kind); client.add_entity_request_handler(Self::handle_resolve_completion_documentation); client.add_entity_request_handler(Self::handle_apply_code_action); - client.add_entity_request_handler(Self::handle_inlay_hints); client.add_entity_request_handler(Self::handle_get_project_symbols); client.add_entity_request_handler(Self::handle_resolve_inlay_hint); client.add_entity_request_handler(Self::handle_get_color_presentation); @@ -3765,9 +3818,8 @@ impl LspStore { nonce: StdRng::from_os_rng().random(), diagnostic_summaries: HashMap::default(), lsp_server_capabilities: HashMap::default(), - lsp_document_colors: HashMap::default(), - lsp_code_lens: HashMap::default(), - running_lsp_requests: HashMap::default(), + lsp_data: HashMap::default(), + next_hint_id: Arc::default(), active_entry: None, _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), @@ -3826,9 +3878,8 @@ impl LspStore { nonce: StdRng::from_os_rng().random(), diagnostic_summaries: HashMap::default(), lsp_server_capabilities: HashMap::default(), - lsp_document_colors: HashMap::default(), - lsp_code_lens: HashMap::default(), - running_lsp_requests: HashMap::default(), + next_hint_id: Arc::default(), + lsp_data: HashMap::default(), active_entry: None, _maintain_workspace_config, @@ -4025,8 +4076,7 @@ impl LspStore { *refcount }; if refcount == 0 { - lsp_store.lsp_document_colors.remove(&buffer_id); - lsp_store.lsp_code_lens.remove(&buffer_id); + lsp_store.lsp_data.remove(&buffer_id); let local = lsp_store.as_local_mut().unwrap(); local.registered_buffers.remove(&buffer_id); local.buffers_opened_in_servers.remove(&buffer_id); @@ -4293,7 +4343,7 @@ impl LspStore { &self, buffer: &Entity, request: &R, - cx: &Context, + cx: &App, ) -> bool where R: LspCommand, @@ -4314,7 +4364,7 @@ impl LspStore { &self, buffer: &Entity, check: F, - cx: &Context, + cx: &App, ) -> bool where F: Fn(&lsp::ServerCapabilities) -> bool, @@ -4800,7 +4850,65 @@ impl LspStore { } } - pub fn resolve_inlay_hint( + pub fn resolved_hint( + &mut self, + buffer_id: BufferId, + id: InlayId, + cx: &mut Context, + ) -> Option { + let buffer = self.buffer_store.read(cx).get(buffer_id)?; + + let lsp_data = self.lsp_data.get_mut(&buffer_id)?; + let buffer_lsp_hints = &mut lsp_data.inlay_hints; + let hint = buffer_lsp_hints.hint_for_id(id)?.clone(); + let (server_id, resolve_data) = match &hint.resolve_state { + ResolveState::Resolved => return Some(ResolvedHint::Resolved(hint)), + ResolveState::Resolving => { + return Some(ResolvedHint::Resolving( + buffer_lsp_hints.hint_resolves.get(&id)?.clone(), + )); + } + ResolveState::CanResolve(server_id, resolve_data) => (*server_id, resolve_data.clone()), + }; + + let resolve_task = self.resolve_inlay_hint(hint, buffer, server_id, cx); + let buffer_lsp_hints = &mut self.lsp_data.get_mut(&buffer_id)?.inlay_hints; + let previous_task = buffer_lsp_hints.hint_resolves.insert( + id, + cx.spawn(async move |lsp_store, cx| { + let resolved_hint = resolve_task.await; + lsp_store + .update(cx, |lsp_store, _| { + if let Some(old_inlay_hint) = lsp_store + .lsp_data + .get_mut(&buffer_id) + .and_then(|buffer_lsp_data| buffer_lsp_data.inlay_hints.hint_for_id(id)) + { + match resolved_hint { + Ok(resolved_hint) => { + *old_inlay_hint = resolved_hint; + } + Err(e) => { + old_inlay_hint.resolve_state = + ResolveState::CanResolve(server_id, resolve_data); + log::error!("Inlay hint resolve failed: {e:#}"); + } + } + } + }) + .ok(); + }) + .shared(), + ); + debug_assert!( + previous_task.is_none(), + "Did not change hint's resolve state after spawning its resolve" + ); + buffer_lsp_hints.hint_for_id(id)?.resolve_state = ResolveState::Resolving; + None + } + + fn resolve_inlay_hint( &self, mut hint: InlayHint, buffer: Entity, @@ -5149,6 +5257,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5214,6 +5323,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5279,6 +5389,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5344,6 +5455,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5410,6 +5522,7 @@ impl LspStore { let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5477,6 +5590,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5538,32 +5652,38 @@ impl LspStore { ) -> CodeLensTask { let version_queried_for = buffer.read(cx).version(); let buffer_id = buffer.read(cx).remote_id(); + let existing_servers = self.as_local().map(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + }); - if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) - && !version_queried_for.changed_since(&cached_data.lens_for_version) - { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.lens.keys().copied().collect() - }); - if !has_different_servers { - return Task::ready(Ok(Some( - cached_data.lens.values().flatten().cloned().collect(), - ))) - .shared(); + if let Some(lsp_data) = self.current_lsp_data(buffer_id) { + if let Some(cached_lens) = &lsp_data.code_lens { + if !version_queried_for.changed_since(&lsp_data.buffer_version) { + let has_different_servers = existing_servers.is_some_and(|existing_servers| { + existing_servers != cached_lens.lens.keys().copied().collect() + }); + if !has_different_servers { + return Task::ready(Ok(Some( + cached_lens.lens.values().flatten().cloned().collect(), + ))) + .shared(); + } + } else if let Some((updating_for, running_update)) = cached_lens.update.as_ref() { + if !version_queried_for.changed_since(updating_for) { + return running_update.clone(); + } + } } } - let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.update - && !version_queried_for.changed_since(updating_for) - { - return running_update.clone(); - } + let lens_lsp_data = self + .latest_lsp_data(buffer, cx) + .code_lens + .get_or_insert_default(); let buffer = buffer.clone(); let query_version_queried_for = version_queried_for.clone(); let new_task = cx @@ -5582,7 +5702,13 @@ impl LspStore { Err(e) => { lsp_store .update(cx, |lsp_store, _| { - lsp_store.lsp_code_lens.entry(buffer_id).or_default().update = None; + if let Some(lens_lsp_data) = lsp_store + .lsp_data + .get_mut(&buffer_id) + .and_then(|lsp_data| lsp_data.code_lens.as_mut()) + { + lens_lsp_data.update = None; + } }) .ok(); return Err(e); @@ -5591,25 +5717,26 @@ impl LspStore { lsp_store .update(cx, |lsp_store, _| { - let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); + let lsp_data = lsp_store.current_lsp_data(buffer_id)?; + let code_lens = lsp_data.code_lens.as_mut()?; if let Some(fetched_lens) = fetched_lens { - if lsp_data.lens_for_version == query_version_queried_for { - lsp_data.lens.extend(fetched_lens); + if lsp_data.buffer_version == query_version_queried_for { + code_lens.lens.extend(fetched_lens); } else if !lsp_data - .lens_for_version + .buffer_version .changed_since(&query_version_queried_for) { - lsp_data.lens_for_version = query_version_queried_for; - lsp_data.lens = fetched_lens; + lsp_data.buffer_version = query_version_queried_for; + code_lens.lens = fetched_lens; } } - lsp_data.update = None; - Some(lsp_data.lens.values().flatten().cloned().collect()) + code_lens.update = None; + Some(code_lens.lens.values().flatten().cloned().collect()) }) .map_err(Arc::new) }) .shared(); - lsp_data.update = Some((version_queried_for, new_task.clone())); + lens_lsp_data.update = Some((version_queried_for, new_task.clone())); new_task } @@ -5625,6 +5752,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -6327,6 +6455,7 @@ impl LspStore { } let request_task = client.request_lsp( upstream_project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), @@ -6369,58 +6498,308 @@ impl LspStore { } } + pub fn applicable_inlay_chunks( + &self, + buffer_id: BufferId, + ranges: &[Range], + ) -> Vec> { + self.lsp_data + .get(&buffer_id) + .map(|data| { + data.inlay_hints + .applicable_chunks(ranges) + .map(|chunk| chunk.start..chunk.end) + .collect() + }) + .unwrap_or_default() + } + + pub fn invalidate_inlay_hints<'a>( + &'a mut self, + for_buffers: impl IntoIterator + 'a, + ) { + for buffer_id in for_buffers { + if let Some(lsp_data) = self.lsp_data.get_mut(buffer_id) { + lsp_data.inlay_hints.clear(); + } + } + } + pub fn inlay_hints( &mut self, + invalidate: InvalidationStrategy, buffer: Entity, - range: Range, + ranges: Vec>, + known_chunks: Option<(clock::Global, HashSet>)>, cx: &mut Context, - ) -> Task>> { - let range_start = range.start; - let range_end = range.end; - let buffer_id = buffer.read(cx).remote_id().into(); - let request = InlayHints { range }; + ) -> HashMap, Task>> { + let buffer_snapshot = buffer.read(cx).snapshot(); + let for_server = if let InvalidationStrategy::RefreshRequested(server_id) = invalidate { + Some(server_id) + } else { + None + }; + let invalidate_cache = invalidate.should_invalidate(); + let next_hint_id = self.next_hint_id.clone(); + let lsp_data = self.latest_lsp_data(&buffer, cx); + let existing_inlay_hints = &mut lsp_data.inlay_hints; + let known_chunks = known_chunks + .filter(|(known_version, _)| !lsp_data.buffer_version.changed_since(known_version)) + .map(|(_, known_chunks)| known_chunks) + .unwrap_or_default(); - if let Some((client, project_id)) = self.upstream_client() { - if !self.is_capable_for_proto_request(&buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + let mut hint_fetch_tasks = Vec::new(); + let mut cached_inlay_hints = HashMap::default(); + let mut ranges_to_query = Vec::new(); + let applicable_chunks = existing_inlay_hints + .applicable_chunks(ranges.as_slice()) + .filter(|chunk| !known_chunks.contains(&(chunk.start..chunk.end))) + .collect::>(); + if applicable_chunks.is_empty() { + return HashMap::default(); + } + + let last_chunk_number = applicable_chunks.len() - 1; + + for (i, row_chunk) in applicable_chunks.into_iter().enumerate() { + match ( + existing_inlay_hints + .cached_hints(&row_chunk) + .filter(|_| !invalidate_cache) + .cloned(), + existing_inlay_hints + .fetched_hints(&row_chunk) + .as_ref() + .filter(|_| !invalidate_cache) + .cloned(), + ) { + (None, None) => { + let end = if last_chunk_number == i { + Point::new(row_chunk.end, buffer_snapshot.line_len(row_chunk.end)) + } else { + Point::new(row_chunk.end, 0) + }; + ranges_to_query.push(( + row_chunk, + buffer_snapshot.anchor_before(Point::new(row_chunk.start, 0)) + ..buffer_snapshot.anchor_after(end), + )); + } + (None, Some(fetched_hints)) => { + hint_fetch_tasks.push((row_chunk, fetched_hints.clone())) + } + (Some(cached_hints), None) => { + for (server_id, cached_hints) in cached_hints { + if for_server.is_none_or(|for_server| for_server == server_id) { + cached_inlay_hints + .entry(row_chunk.start..row_chunk.end) + .or_insert_with(HashMap::default) + .entry(server_id) + .or_insert_with(Vec::new) + .extend(cached_hints); + } + } + } + (Some(cached_hints), Some(fetched_hints)) => { + hint_fetch_tasks.push((row_chunk, fetched_hints.clone())); + for (server_id, cached_hints) in cached_hints { + if for_server.is_none_or(|for_server| for_server == server_id) { + cached_inlay_hints + .entry(row_chunk.start..row_chunk.end) + .or_insert_with(HashMap::default) + .entry(server_id) + .or_insert_with(Vec::new) + .extend(cached_hints); + } + } + } } - let proto_request = proto::InlayHints { - project_id, - buffer_id, - start: Some(serialize_anchor(&range_start)), - end: Some(serialize_anchor(&range_end)), - version: serialize_version(&buffer.read(cx).version()), - }; - cx.spawn(async move |project, cx| { - let response = client - .request(proto_request) - .await - .context("inlay hints proto request")?; - LspCommand::response_from_proto( - request, - response, - project.upgrade().context("No project")?, - buffer.clone(), - cx.clone(), + } + + let cached_chunk_data = cached_inlay_hints + .into_iter() + .map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints)))) + .collect(); + if hint_fetch_tasks.is_empty() && ranges_to_query.is_empty() { + cached_chunk_data + } else { + if invalidate_cache { + lsp_data.inlay_hints.clear(); + } + + for (chunk, range_to_query) in ranges_to_query { + let next_hint_id = next_hint_id.clone(); + let buffer = buffer.clone(); + let new_inlay_hints = cx + .spawn(async move |lsp_store, cx| { + let new_fetch_task = lsp_store.update(cx, |lsp_store, cx| { + lsp_store.fetch_inlay_hints(for_server, &buffer, range_to_query, cx) + })?; + new_fetch_task + .await + .and_then(|new_hints_by_server| { + lsp_store.update(cx, |lsp_store, cx| { + let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); + let update_cache = !lsp_data + .buffer_version + .changed_since(&buffer.read(cx).version()); + new_hints_by_server + .into_iter() + .map(|(server_id, new_hints)| { + let new_hints = new_hints + .into_iter() + .map(|new_hint| { + ( + InlayId::Hint(next_hint_id.fetch_add( + 1, + atomic::Ordering::AcqRel, + )), + new_hint, + ) + }) + .collect::>(); + if update_cache { + lsp_data.inlay_hints.insert_new_hints( + chunk, + server_id, + new_hints.clone(), + ); + } + (server_id, new_hints) + }) + .collect() + }) + }) + .map_err(Arc::new) + }) + .shared(); + + let fetch_task = lsp_data.inlay_hints.fetched_hints(&chunk); + *fetch_task = Some(new_inlay_hints.clone()); + hint_fetch_tasks.push((chunk, new_inlay_hints)); + } + + let mut combined_data = cached_chunk_data; + combined_data.extend(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| { + ( + chunk.start..chunk.end, + cx.spawn(async move |_, _| { + hints_fetch.await.map_err(|e| { + if e.error_code() != ErrorCode::Internal { + anyhow!(e.error_code()) + } else { + anyhow!("{e:#}") + } + }) + }), ) - .await - .context("inlay hints proto response conversion") + })); + combined_data + } + } + + fn fetch_inlay_hints( + &mut self, + for_server: Option, + buffer: &Entity, + range: Range, + cx: &mut Context, + ) -> Task>>> { + let request = InlayHints { + range: range.clone(), + }; + if let Some((upstream_client, project_id)) = self.upstream_client() { + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(HashMap::default())); + } + let request_task = upstream_client.request_lsp( + project_id, + for_server.map(|id| id.to_proto()), + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); + let buffer = buffer.clone(); + cx.spawn(async move |weak_lsp_store, cx| { + let Some(lsp_store) = weak_lsp_store.upgrade() else { + return Ok(HashMap::default()); + }; + let Some(responses) = request_task.await? else { + return Ok(HashMap::default()); + }; + + let inlay_hints = join_all(responses.payload.into_iter().map(|response| { + let lsp_store = lsp_store.clone(); + let buffer = buffer.clone(); + let cx = cx.clone(); + let request = request.clone(); + async move { + ( + LanguageServerId::from_proto(response.server_id), + request + .response_from_proto(response.response, lsp_store, buffer, cx) + .await, + ) + } + })) + .await; + + let mut has_errors = false; + let inlay_hints = inlay_hints + .into_iter() + .filter_map(|(server_id, inlay_hints)| match inlay_hints { + Ok(inlay_hints) => Some((server_id, inlay_hints)), + Err(e) => { + has_errors = true; + log::error!("{e:#}"); + None + } + }) + .collect::>(); + anyhow::ensure!( + !has_errors || !inlay_hints.is_empty(), + "Failed to fetch inlay hints" + ); + Ok(inlay_hints) }) } else { - let lsp_request_task = self.request_lsp( - buffer.clone(), - LanguageServerToQuery::FirstCapable, - request, - cx, - ); - cx.spawn(async move |_, cx| { - buffer - .update(cx, |buffer, _| { - buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp]) - })? + let inlay_hints_task = match for_server { + Some(server_id) => { + let server_task = self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + request, + cx, + ); + cx.background_spawn(async move { + let mut responses = Vec::new(); + match server_task.await { + Ok(response) => responses.push((server_id, response)), + Err(e) => log::error!( + "Error handling response for inlay hints request: {e:#}" + ), + } + responses + }) + } + None => self.request_multiple_lsp_locally(buffer, None::, request, cx), + }; + let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + cx.background_spawn(async move { + Ok(inlay_hints_task .await - .context("waiting for inlay hint request range edits")?; - lsp_request_task.await.context("inlay hints LSP request") + .into_iter() + .map(|(server_id, mut new_hints)| { + new_hints.retain(|hint| { + hint.position.is_valid(&buffer_snapshot) + && range.start.is_valid(&buffer_snapshot) + && range.end.is_valid(&buffer_snapshot) + && hint.position.cmp(&range.start, &buffer_snapshot).is_ge() + && hint.position.cmp(&range.end, &buffer_snapshot).is_le() + }); + (server_id, new_hints) + }) + .collect()) }) } } @@ -6531,39 +6910,55 @@ impl LspStore { let version_queried_for = buffer.read(cx).version(); let buffer_id = buffer.read(cx).remote_id(); - if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) - && !version_queried_for.changed_since(&cached_data.colors_for_version) - { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.colors.keys().copied().collect() - }); - if !has_different_servers { - if Some(cached_data.cache_version) == known_cache_version { - return None; - } else { - return Some( - Task::ready(Ok(DocumentColors { - colors: cached_data.colors.values().flatten().cloned().collect(), - cache_version: Some(cached_data.cache_version), - })) - .shared(), - ); + let current_language_servers = self.as_local().map(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + }); + + if let Some(lsp_data) = self.current_lsp_data(buffer_id) { + if let Some(cached_colors) = &lsp_data.document_colors { + if !version_queried_for.changed_since(&lsp_data.buffer_version) { + let has_different_servers = + current_language_servers.is_some_and(|current_language_servers| { + current_language_servers + != cached_colors.colors.keys().copied().collect() + }); + if !has_different_servers { + let cache_version = cached_colors.cache_version; + if Some(cache_version) == known_cache_version { + return None; + } else { + return Some( + Task::ready(Ok(DocumentColors { + colors: cached_colors + .colors + .values() + .flatten() + .cloned() + .collect(), + cache_version: Some(cache_version), + })) + .shared(), + ); + } + } } } } - let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.colors_update + let color_lsp_data = self + .latest_lsp_data(&buffer, cx) + .document_colors + .get_or_insert_default(); + if let Some((updating_for, running_update)) = &color_lsp_data.colors_update && !version_queried_for.changed_since(updating_for) { return Some(running_update.clone()); } - let query_version_queried_for = version_queried_for.clone(); + let buffer_version_queried_for = version_queried_for.clone(); let new_task = cx .spawn(async move |lsp_store, cx| { cx.background_executor() @@ -6581,7 +6976,7 @@ impl LspStore { if Some(true) == buffer .update(cx, |buffer, _| { - buffer.version() != query_version_queried_for + buffer.version() != buffer_version_queried_for }) .ok() { @@ -6592,11 +6987,11 @@ impl LspStore { Err(e) => { lsp_store .update(cx, |lsp_store, _| { - lsp_store - .lsp_document_colors - .entry(buffer_id) - .or_default() - .colors_update = None; + if let Some(lsp_data) = lsp_store.lsp_data.get_mut(&buffer_id) { + if let Some(document_colors) = &mut lsp_data.document_colors { + document_colors.colors_update = None; + } + } }) .ok(); return Err(e); @@ -6604,24 +6999,25 @@ impl LspStore { }; lsp_store - .update(cx, |lsp_store, _| { - let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default(); + .update(cx, |lsp_store, cx| { + let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); + let lsp_colors = lsp_data.document_colors.get_or_insert_default(); if let Some(fetched_colors) = fetched_colors { - if lsp_data.colors_for_version == query_version_queried_for { - lsp_data.colors.extend(fetched_colors); - lsp_data.cache_version += 1; + if lsp_data.buffer_version == buffer_version_queried_for { + lsp_colors.colors.extend(fetched_colors); + lsp_colors.cache_version += 1; } else if !lsp_data - .colors_for_version - .changed_since(&query_version_queried_for) + .buffer_version + .changed_since(&buffer_version_queried_for) { - lsp_data.colors_for_version = query_version_queried_for; - lsp_data.colors = fetched_colors; - lsp_data.cache_version += 1; + lsp_data.buffer_version = buffer_version_queried_for; + lsp_colors.colors = fetched_colors; + lsp_colors.cache_version += 1; } } - lsp_data.colors_update = None; - let colors = lsp_data + lsp_colors.colors_update = None; + let colors = lsp_colors .colors .values() .flatten() @@ -6629,13 +7025,13 @@ impl LspStore { .collect::>(); DocumentColors { colors, - cache_version: Some(lsp_data.cache_version), + cache_version: Some(lsp_colors.cache_version), } }) .map_err(Arc::new) }) .shared(); - lsp_data.colors_update = Some((version_queried_for, new_task.clone())); + color_lsp_data.colors_update = Some((version_queried_for, new_task.clone())); Some(new_task) } @@ -6652,6 +7048,7 @@ impl LspStore { let request_task = client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -6730,6 +7127,7 @@ impl LspStore { } let request_task = client.request_lsp( upstream_project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), @@ -6793,6 +7191,7 @@ impl LspStore { } let request_task = client.request_lsp( upstream_project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), @@ -7899,8 +8298,9 @@ impl LspStore { cx.background_spawn(async move { let mut responses = Vec::with_capacity(response_results.len()); while let Some((server_id, response_result)) = response_results.next().await { - if let Some(response) = response_result.log_err() { - responses.push((server_id, response)); + match response_result { + Ok(response) => responses.push((server_id, response)), + Err(e) => log::error!("Error handling response for request {request:?}: {e:#}"), } } responses @@ -7958,27 +8358,30 @@ impl LspStore { let sender_id = envelope.original_sender_id().unwrap_or_default(); let lsp_query = envelope.payload; let lsp_request_id = LspRequestId(lsp_query.lsp_request_id); + let server_id = lsp_query.server_id.map(LanguageServerId::from_proto); match lsp_query.request.context("invalid LSP query request")? { Request::GetReferences(get_references) => { let position = get_references.position.clone().and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_references, position, - cx.clone(), + &mut cx, ) .await?; } Request::GetDocumentColor(get_document_color) => { Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_document_color, None, - cx.clone(), + &mut cx, ) .await?; } @@ -7986,22 +8389,24 @@ impl LspStore { let position = get_hover.position.clone().and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_hover, position, - cx.clone(), + &mut cx, ) .await?; } Request::GetCodeActions(get_code_actions) => { Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_code_actions, None, - cx.clone(), + &mut cx, ) .await?; } @@ -8012,22 +8417,24 @@ impl LspStore { .and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_signature_help, position, - cx.clone(), + &mut cx, ) .await?; } Request::GetCodeLens(get_code_lens) => { Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_code_lens, None, - cx.clone(), + &mut cx, ) .await?; } @@ -8035,11 +8442,12 @@ impl LspStore { let position = get_definition.position.clone().and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_definition, position, - cx.clone(), + &mut cx, ) .await?; } @@ -8050,11 +8458,12 @@ impl LspStore { .and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_declaration, position, - cx.clone(), + &mut cx, ) .await?; } @@ -8065,11 +8474,12 @@ impl LspStore { .and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_type_definition, position, - cx.clone(), + &mut cx, ) .await?; } @@ -8080,15 +8490,15 @@ impl LspStore { .and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_implementation, position, - cx.clone(), + &mut cx, ) .await?; } - // Diagnostics pull synchronizes internally via the buffer state, and cannot be handled generically as the other requests. Request::GetDocumentDiagnostics(get_document_diagnostics) => { let buffer_id = BufferId::new(get_document_diagnostics.buffer_id())?; let version = deserialize_version(get_document_diagnostics.buffer_version()); @@ -8101,16 +8511,20 @@ impl LspStore { })? .await?; lsp_store.update(&mut cx, |lsp_store, cx| { - let existing_queries = lsp_store - .running_lsp_requests - .entry(TypeId::of::()) - .or_default(); + let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); + let key = LspKey { + request_type: TypeId::of::(), + server_queried: server_id, + }; if ::ProtoRequest::stop_previous_requests( - ) || buffer.read(cx).version.changed_since(&existing_queries.0) - { - existing_queries.1.clear(); + ) { + if let Some(lsp_requests) = lsp_data.lsp_requests.get_mut(&key) { + lsp_requests.clear(); + }; } - existing_queries.1.insert( + + let existing_queries = lsp_data.lsp_requests.entry(key).or_default(); + existing_queries.insert( lsp_request_id, cx.spawn(async move |lsp_store, cx| { let diagnostics_pull = lsp_store @@ -8128,6 +8542,39 @@ impl LspStore { ); })?; } + Request::InlayHints(inlay_hints) => { + let query_start = inlay_hints + .start + .clone() + .and_then(deserialize_anchor) + .context("invalid inlay hints range start")?; + let query_end = inlay_hints + .end + .clone() + .and_then(deserialize_anchor) + .context("invalid inlay hints range end")?; + Self::deduplicate_range_based_lsp_requests::( + &lsp_store, + server_id, + lsp_request_id, + &inlay_hints, + query_start..query_end, + &mut cx, + ) + .await + .context("preparing inlay hints request")?; + Self::query_lsp_locally::( + lsp_store, + server_id, + sender_id, + lsp_request_id, + inlay_hints, + None, + &mut cx, + ) + .await + .context("querying for inlay hints")? + } } Ok(proto::Ack {}) } @@ -9043,7 +9490,7 @@ impl LspStore { if let Some(work) = status.pending_work.remove(&token) && !work.is_disk_based_diagnostics_progress { - cx.emit(LspStoreEvent::RefreshInlayHints); + cx.emit(LspStoreEvent::RefreshInlayHints(language_server_id)); } cx.notify(); } @@ -9175,12 +9622,14 @@ impl LspStore { } async fn handle_refresh_inlay_hints( - this: Entity, - _: TypedEnvelope, + lsp_store: Entity, + envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::RefreshInlayHints); + lsp_store.update(&mut cx, |_, cx| { + cx.emit(LspStoreEvent::RefreshInlayHints( + LanguageServerId::from_proto(envelope.payload.server_id), + )); })?; Ok(proto::Ack {}) } @@ -9197,51 +9646,6 @@ impl LspStore { Ok(proto::Ack {}) } - async fn handle_inlay_hints( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = this.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&envelope.payload.version)) - })? - .await - .with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?; - - let start = envelope - .payload - .start - .and_then(deserialize_anchor) - .context("missing range start")?; - let end = envelope - .payload - .end - .and_then(deserialize_anchor) - .context("missing range end")?; - let buffer_hints = this - .update(&mut cx, |lsp_store, cx| { - lsp_store.inlay_hints(buffer.clone(), start..end, cx) - })? - .await - .context("inlay hints fetch")?; - - this.update(&mut cx, |project, cx| { - InlayHints::response_to_proto( - buffer_hints, - project, - sender_id, - &buffer.read(cx).version(), - cx, - ) - }) - } - async fn handle_get_color_presentation( lsp_store: Entity, envelope: TypedEnvelope, @@ -9307,7 +9711,7 @@ impl LspStore { } async fn handle_resolve_inlay_hint( - this: Entity, + lsp_store: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { @@ -9317,13 +9721,13 @@ impl LspStore { .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); let hint = InlayHints::proto_to_project_hint(proto_hint) .context("resolved proto inlay hint conversion")?; - let buffer = this.update(&mut cx, |this, cx| { + let buffer = lsp_store.update(&mut cx, |lsp_store, cx| { let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - this.buffer_store.read(cx).get_existing(buffer_id) + lsp_store.buffer_store.read(cx).get_existing(buffer_id) })??; - let response_hint = this - .update(&mut cx, |this, cx| { - this.resolve_inlay_hint( + let response_hint = lsp_store + .update(&mut cx, |lsp_store, cx| { + lsp_store.resolve_inlay_hint( hint, buffer, LanguageServerId(envelope.payload.language_server_id as usize), @@ -10429,7 +10833,7 @@ impl LspStore { language_server.name(), Some(key.worktree_id), )); - cx.emit(LspStoreEvent::RefreshInlayHints); + cx.emit(LspStoreEvent::RefreshInlayHints(server_id)); let server_capabilities = language_server.capabilities(); if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { @@ -11047,12 +11451,8 @@ impl LspStore { fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) { self.lsp_server_capabilities.remove(&for_server); - for buffer_colors in self.lsp_document_colors.values_mut() { - buffer_colors.colors.remove(&for_server); - buffer_colors.cache_version += 1; - } - for buffer_lens in self.lsp_code_lens.values_mut() { - buffer_lens.lens.remove(&for_server); + for lsp_data in self.lsp_data.values_mut() { + lsp_data.remove_server_data(for_server); } if let Some(local) = self.as_local_mut() { local.buffer_pull_diagnostics_result_ids.remove(&for_server); @@ -11661,13 +12061,71 @@ impl LspStore { Ok(()) } + async fn deduplicate_range_based_lsp_requests( + lsp_store: &Entity, + server_id: Option, + lsp_request_id: LspRequestId, + proto_request: &T::ProtoRequest, + range: Range, + cx: &mut AsyncApp, + ) -> Result<()> + where + T: LspCommand, + T::ProtoRequest: proto::LspRequestMessage, + { + let buffer_id = BufferId::new(proto_request.buffer_id())?; + let version = deserialize_version(proto_request.buffer_version()); + let buffer = lsp_store.update(cx, |this, cx| { + this.buffer_store.read(cx).get_existing(buffer_id) + })??; + buffer + .update(cx, |buffer, _| buffer.wait_for_version(version))? + .await?; + lsp_store.update(cx, |lsp_store, cx| { + let lsp_data = lsp_store + .lsp_data + .entry(buffer_id) + .or_insert_with(|| BufferLspData::new(&buffer, cx)); + let chunks_queried_for = lsp_data + .inlay_hints + .applicable_chunks(&[range]) + .collect::>(); + match chunks_queried_for.as_slice() { + &[chunk] => { + let key = LspKey { + request_type: TypeId::of::(), + server_queried: server_id, + }; + let previous_request = lsp_data + .chunk_lsp_requests + .entry(key) + .or_default() + .insert(chunk, lsp_request_id); + if let Some((previous_request, running_requests)) = + previous_request.zip(lsp_data.lsp_requests.get_mut(&key)) + { + running_requests.remove(&previous_request); + } + } + _ambiguous_chunks => { + // Have not found a unique chunk for the query range — be lenient and let the query to be spawned, + // there, a buffer version-based check will be performed and outdated requests discarded. + } + } + anyhow::Ok(()) + })??; + + Ok(()) + } + async fn query_lsp_locally( lsp_store: Entity, + for_server_id: Option, sender_id: proto::PeerId, lsp_request_id: LspRequestId, proto_request: T::ProtoRequest, position: Option, - mut cx: AsyncApp, + cx: &mut AsyncApp, ) -> Result<()> where T: LspCommand + Clone, @@ -11677,30 +12135,48 @@ impl LspStore { { let buffer_id = BufferId::new(proto_request.buffer_id())?; let version = deserialize_version(proto_request.buffer_version()); - let buffer = lsp_store.update(&mut cx, |this, cx| { + let buffer = lsp_store.update(cx, |this, cx| { this.buffer_store.read(cx).get_existing(buffer_id) })??; buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(version.clone()) - })? + .update(cx, |buffer, _| buffer.wait_for_version(version.clone()))? .await?; - let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; + let buffer_version = buffer.read_with(cx, |buffer, _| buffer.version())?; let request = T::from_proto(proto_request, lsp_store.clone(), buffer.clone(), cx.clone()).await?; - lsp_store.update(&mut cx, |lsp_store, cx| { - let request_task = - lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx); - let existing_queries = lsp_store - .running_lsp_requests - .entry(TypeId::of::()) - .or_default(); - if T::ProtoRequest::stop_previous_requests() - || buffer_version.changed_since(&existing_queries.0) - { - existing_queries.1.clear(); + let key = LspKey { + request_type: TypeId::of::(), + server_queried: for_server_id, + }; + lsp_store.update(cx, |lsp_store, cx| { + let request_task = match for_server_id { + Some(server_id) => { + let server_task = lsp_store.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + request.clone(), + cx, + ); + cx.background_spawn(async move { + let mut responses = Vec::new(); + match server_task.await { + Ok(response) => responses.push((server_id, response)), + Err(e) => log::error!( + "Error handling response for request {request:?}: {e:#}" + ), + } + responses + }) + } + None => lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx), + }; + let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); + if T::ProtoRequest::stop_previous_requests() { + if let Some(lsp_requests) = lsp_data.lsp_requests.get_mut(&key) { + lsp_requests.clear(); + } } - existing_queries.1.insert( + lsp_data.lsp_requests.entry(key).or_default().insert( lsp_request_id, cx.spawn(async move |lsp_store, cx| { let response = request_task.await; @@ -11759,8 +12235,15 @@ impl LspStore { #[cfg(any(test, feature = "test-support"))] pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option { - let data = self.lsp_code_lens.get_mut(&buffer_id)?; - Some(data.update.take()?.1) + Some( + self.lsp_data + .get_mut(&buffer_id)? + .code_lens + .take()? + .update + .take()? + .1, + ) } pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> { @@ -11770,6 +12253,26 @@ impl LspStore { pub fn worktree_store(&self) -> Entity { self.worktree_store.clone() } + + /// Gets what's stored in the LSP data for the given buffer. + pub fn current_lsp_data(&mut self, buffer_id: BufferId) -> Option<&mut BufferLspData> { + self.lsp_data.get_mut(&buffer_id) + } + + /// Gets the most recent LSP data for the given buffer: if the data is absent or out of date, + /// new [`BufferLspData`] will be created to replace the previous state. + pub fn latest_lsp_data(&mut self, buffer: &Entity, cx: &mut App) -> &mut BufferLspData { + let (buffer_id, buffer_version) = + buffer.read_with(cx, |buffer, _| (buffer.remote_id(), buffer.version())); + let lsp_data = self + .lsp_data + .entry(buffer_id) + .or_insert_with(|| BufferLspData::new(buffer, cx)); + if buffer_version.changed_since(&lsp_data.buffer_version) { + *lsp_data = BufferLspData::new(buffer, cx); + } + lsp_data + } } // Registration with registerOptions as null, should fallback to true. @@ -12523,6 +13026,11 @@ impl From for CompletionDocumentation { } } +pub enum ResolvedHint { + Resolved(InlayHint), + Resolving(Shared>), +} + fn glob_literal_prefix(glob: &Path) -> PathBuf { glob.components() .take_while(|component| match component { diff --git a/crates/project/src/lsp_store/inlay_hint_cache.rs b/crates/project/src/lsp_store/inlay_hint_cache.rs new file mode 100644 index 0000000000000000000000000000000000000000..0d527b83d2eef03b9473edc2711041c0ebccadb6 --- /dev/null +++ b/crates/project/src/lsp_store/inlay_hint_cache.rs @@ -0,0 +1,221 @@ +use std::{collections::hash_map, ops::Range, sync::Arc}; + +use collections::HashMap; +use futures::future::Shared; +use gpui::{App, Entity, Task}; +use language::{Buffer, BufferRow, BufferSnapshot}; +use lsp::LanguageServerId; +use text::OffsetRangeExt; + +use crate::{InlayHint, InlayId}; + +pub type CacheInlayHints = HashMap>; +pub type CacheInlayHintsTask = Shared>>>; + +/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts. +#[derive(Debug, Clone, Copy)] +pub enum InvalidationStrategy { + /// Language servers reset hints via request. + /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation. + /// + /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise. + RefreshRequested(LanguageServerId), + /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place. + /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence. + BufferEdited, + /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position. + /// No invalidation should be done at all, all new hints are added to the cache. + /// + /// A special case is the editor toggles and settings change: + /// in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other) and toggling hints. + /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen. + None, +} + +impl InvalidationStrategy { + pub fn should_invalidate(&self) -> bool { + matches!( + self, + InvalidationStrategy::RefreshRequested(_) | InvalidationStrategy::BufferEdited + ) + } +} + +pub struct BufferInlayHints { + snapshot: BufferSnapshot, + buffer_chunks: Vec, + hints_by_chunks: Vec>, + fetches_by_chunks: Vec>, + hints_by_id: HashMap, + pub(super) hint_resolves: HashMap>>, +} + +#[derive(Debug, Clone, Copy)] +struct HintForId { + chunk_id: usize, + server_id: LanguageServerId, + position: usize, +} + +/// An range of rows, exclusive as [`lsp::Range`] and +/// +/// denote. +/// +/// Represents an area in a text editor, adjacent to other ones. +/// Together, chunks form entire document at a particular version [`clock::Global`]. +/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BufferChunk { + id: usize, + pub start: BufferRow, + pub end: BufferRow, +} + +impl std::fmt::Debug for BufferInlayHints { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BufferInlayHints") + .field("buffer_chunks", &self.buffer_chunks) + .field("hints_by_chunks", &self.hints_by_chunks) + .field("fetches_by_chunks", &self.fetches_by_chunks) + .field("hints_by_id", &self.hints_by_id) + .finish_non_exhaustive() + } +} + +const MAX_ROWS_IN_A_CHUNK: u32 = 50; + +impl BufferInlayHints { + pub fn new(buffer: &Entity, cx: &mut App) -> Self { + let buffer = buffer.read(cx); + let snapshot = buffer.snapshot(); + let buffer_point_range = (0..buffer.len()).to_point(&snapshot); + let last_row = buffer_point_range.end.row; + let buffer_chunks = (buffer_point_range.start.row..=last_row) + .step_by(MAX_ROWS_IN_A_CHUNK as usize) + .enumerate() + .map(|(id, chunk_start)| BufferChunk { + id, + start: chunk_start, + end: (chunk_start + MAX_ROWS_IN_A_CHUNK).min(last_row), + }) + .collect::>(); + + Self { + hints_by_chunks: vec![None; buffer_chunks.len()], + fetches_by_chunks: vec![None; buffer_chunks.len()], + hints_by_id: HashMap::default(), + hint_resolves: HashMap::default(), + snapshot, + buffer_chunks, + } + } + + pub fn applicable_chunks( + &self, + ranges: &[Range], + ) -> impl Iterator { + let row_ranges = ranges + .iter() + .map(|range| range.to_point(&self.snapshot)) + .map(|point_range| point_range.start.row..=point_range.end.row) + .collect::>(); + self.buffer_chunks + .iter() + .filter(move |chunk| -> bool { + // Be lenient and yield multiple chunks if they "touch" the exclusive part of the range. + // This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around. + let chunk_range = chunk.start..=chunk.end; + row_ranges.iter().any(|row_range| { + chunk_range.contains(&row_range.start()) + || chunk_range.contains(&row_range.end()) + }) + }) + .copied() + } + + pub fn cached_hints(&mut self, chunk: &BufferChunk) -> Option<&CacheInlayHints> { + self.hints_by_chunks[chunk.id].as_ref() + } + + pub fn fetched_hints(&mut self, chunk: &BufferChunk) -> &mut Option { + &mut self.fetches_by_chunks[chunk.id] + } + + #[cfg(any(test, feature = "test-support"))] + pub fn all_cached_hints(&self) -> Vec { + self.hints_by_chunks + .iter() + .filter_map(|hints| hints.as_ref()) + .flat_map(|hints| hints.values().cloned()) + .flatten() + .map(|(_, hint)| hint) + .collect() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn all_fetched_hints(&self) -> Vec { + self.fetches_by_chunks + .iter() + .filter_map(|fetches| fetches.clone()) + .collect() + } + + pub fn remove_server_data(&mut self, for_server: LanguageServerId) { + for (chunk_index, hints) in self.hints_by_chunks.iter_mut().enumerate() { + if let Some(hints) = hints { + if hints.remove(&for_server).is_some() { + self.fetches_by_chunks[chunk_index] = None; + } + } + } + } + + pub fn clear(&mut self) { + self.hints_by_chunks = vec![None; self.buffer_chunks.len()]; + self.fetches_by_chunks = vec![None; self.buffer_chunks.len()]; + self.hints_by_id.clear(); + self.hint_resolves.clear(); + } + + pub fn insert_new_hints( + &mut self, + chunk: BufferChunk, + server_id: LanguageServerId, + new_hints: Vec<(InlayId, InlayHint)>, + ) { + let existing_hints = self.hints_by_chunks[chunk.id] + .get_or_insert_default() + .entry(server_id) + .or_insert_with(Vec::new); + let existing_count = existing_hints.len(); + existing_hints.extend(new_hints.into_iter().enumerate().filter_map( + |(i, (id, new_hint))| { + let new_hint_for_id = HintForId { + chunk_id: chunk.id, + server_id, + position: existing_count + i, + }; + if let hash_map::Entry::Vacant(vacant_entry) = self.hints_by_id.entry(id) { + vacant_entry.insert(new_hint_for_id); + Some((id, new_hint)) + } else { + None + } + }, + )); + *self.fetched_hints(&chunk) = None; + } + + pub fn hint_for_id(&mut self, id: InlayId) -> Option<&mut InlayHint> { + let hint_for_id = self.hints_by_id.get(&id)?; + let (hint_id, hint) = self + .hints_by_chunks + .get_mut(hint_for_id.chunk_id)? + .as_mut()? + .get_mut(&hint_for_id.server_id)? + .get_mut(hint_for_id.position)?; + debug_assert_eq!(*hint_id, id, "Invalid pointer {hint_for_id:?}"); + Some(hint) + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f301c7800a5b098ddc93a7badc1617f7842e62d1..f9a3f20fa77c41d1ec6405ea0ee7b245fe4e0845 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -145,9 +145,9 @@ pub use task_inventory::{ pub use buffer_store::ProjectTransaction; pub use lsp_store::{ - DiagnosticSummary, LanguageServerLogType, LanguageServerProgress, LanguageServerPromptRequest, - LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent, - SERVER_PROGRESS_THROTTLE_TIMEOUT, + DiagnosticSummary, InvalidationStrategy, LanguageServerLogType, LanguageServerProgress, + LanguageServerPromptRequest, LanguageServerStatus, LanguageServerToQuery, LspStore, + LspStoreEvent, SERVER_PROGRESS_THROTTLE_TIMEOUT, }; pub use toolchain_store::{ToolchainStore, Toolchains}; const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500; @@ -338,7 +338,7 @@ pub enum Event { HostReshared, Reshared, Rejoined, - RefreshInlayHints, + RefreshInlayHints(LanguageServerId), RefreshCodeLens, RevealInProjectPanel(ProjectEntryId), SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>), @@ -402,6 +402,26 @@ pub enum PrepareRenameResponse { InvalidPosition, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InlayId { + EditPrediction(usize), + DebuggerValue(usize), + // LSP + Hint(usize), + Color(usize), +} + +impl InlayId { + pub fn id(&self) -> usize { + match self { + Self::EditPrediction(id) => *id, + Self::DebuggerValue(id) => *id, + Self::Hint(id) => *id, + Self::Color(id) => *id, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { pub position: language::Anchor, @@ -3058,7 +3078,9 @@ impl Project { return; }; } - LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints), + LspStoreEvent::RefreshInlayHints(server_id) => { + cx.emit(Event::RefreshInlayHints(*server_id)) + } LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens), LspStoreEvent::LanguageServerPrompt(prompt) => { cx.emit(Event::LanguageServerPrompt(prompt.clone())) @@ -3978,31 +4000,6 @@ impl Project { }) } - pub fn inlay_hints( - &mut self, - buffer_handle: Entity, - range: Range, - cx: &mut Context, - ) -> Task>> { - let buffer = buffer_handle.read(cx); - let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); - self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.inlay_hints(buffer_handle, range, cx) - }) - } - - pub fn resolve_inlay_hint( - &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, - cx: &mut Context, - ) -> Task> { - self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.resolve_inlay_hint(hint, buffer_handle, server_id, cx) - }) - } - pub fn search(&mut self, query: SearchQuery, cx: &mut Context) -> Receiver { let (result_tx, result_rx) = smol::channel::unbounded(); @@ -5262,6 +5259,7 @@ impl Project { }) } + #[cfg(any(test, feature = "test-support"))] pub fn has_language_servers_for(&self, buffer: &Buffer, cx: &mut App) -> bool { self.lsp_store.update(cx, |this, cx| { this.language_servers_for_local_buffer(buffer, cx) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 7f504a676c8ef6bd46efdd5f4fd570e69921d652..89a49c6fb0185d36cf2dab3f07cfc6efedd1b6d1 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1815,7 +1815,10 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { fake_server .start_progress(format!("{}/0", progress_token)) .await; - assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints); + assert_eq!( + events.next().await.unwrap(), + Event::RefreshInlayHints(fake_server.server.server_id()) + ); assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsStarted { @@ -1954,7 +1957,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC Some(worktree_id) ) ); - assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints); + assert_eq!( + events.next().await.unwrap(), + Event::RefreshInlayHints(fake_server.server.server_id()) + ); fake_server.start_progress(progress_token).await; assert_eq!( events.next().await.unwrap(), diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index d50c1924cdf237d603b78062b3335354a6d6127f..7e446a915febbc03f2dd5920faf12a58a5d9b639 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -465,6 +465,7 @@ message ResolveInlayHintResponse { message RefreshInlayHints { uint64 project_id = 1; + uint64 server_id = 2; } message CodeLens { @@ -781,6 +782,7 @@ message TextEdit { message LspQuery { uint64 project_id = 1; uint64 lsp_request_id = 2; + optional uint64 server_id = 15; oneof request { GetReferences get_references = 3; GetDocumentColor get_document_color = 4; @@ -793,6 +795,7 @@ message LspQuery { GetDeclaration get_declaration = 11; GetTypeDefinition get_type_definition = 12; GetImplementation get_implementation = 13; + InlayHints inlay_hints = 14; } } @@ -815,6 +818,7 @@ message LspResponse { GetTypeDefinitionResponse get_type_definition_response = 10; GetImplementationResponse get_implementation_response = 11; GetReferencesResponse get_references_response = 12; + InlayHintsResponse inlay_hints_response = 13; } uint64 server_id = 7; } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 3710d77262f872d6a422827c9cf1829d21d8f221..433c4c355c6e5c7d32713f6b37060e6a47a4c687 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -517,6 +517,7 @@ lsp_messages!( (GetDeclaration, GetDeclarationResponse, true), (GetTypeDefinition, GetTypeDefinitionResponse, true), (GetImplementation, GetImplementationResponse, true), + (InlayHints, InlayHintsResponse, false), ); entity_messages!( @@ -847,6 +848,7 @@ impl LspQuery { Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false), Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false), Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false), + Some(lsp_query::Request::InlayHints(_)) => ("InlayHints", false), None => ("", true), } } diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs index a90797ff5dfb44c22fa7aa61751ad3baefd2b745..d7e3ba1e461b28ac264afcc05a8ae941e6da0c32 100644 --- a/crates/rpc/src/proto_client.rs +++ b/crates/rpc/src/proto_client.rs @@ -226,6 +226,7 @@ impl AnyProtoClient { pub fn request_lsp( &self, project_id: u64, + server_id: Option, timeout: Duration, executor: BackgroundExecutor, request: T, @@ -247,6 +248,7 @@ impl AnyProtoClient { let query = proto::LspQuery { project_id, + server_id, lsp_request_id: new_id.0, request: Some(request.to_proto_query()), }; @@ -361,6 +363,9 @@ impl AnyProtoClient { Response::GetImplementationResponse(response) => { to_any_envelope(&envelope, response) } + Response::InlayHintsResponse(response) => { + to_any_envelope(&envelope, response) + } }; Some(proto::ProtoLspResponse { server_id, diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index e6bea2e4dd634ca915242f8d86fc31e22bb61c95..7d8efbb11a5f1461da5b63152e2277a38ad272b4 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -47,5 +47,7 @@ zed_actions.workspace = true client = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +language = { workspace = true, features = ["test-support"] } +lsp.workspace = true unindent.workspace = true workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index e4ff5e6e540fa5626699e98725c5713a09e7cce8..97882994d2f8ea452e45dd830b777ec445d3768f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2357,9 +2357,10 @@ pub mod tests { use super::*; use editor::{DisplayPoint, display_map::DisplayRow}; use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle}; + use language::{FakeLspAdapter, rust_lang}; use project::FakeFs; use serde_json::json; - use settings::SettingsStore; + use settings::{InlayHintSettingsContent, SettingsStore}; use util::{path, paths::PathStyle, rel_path::rel_path}; use util_macros::perf; use workspace::DeploySearch; @@ -4226,6 +4227,101 @@ pub mod tests { .unwrap(); } + #[perf] + #[gpui::test] + async fn test_search_with_inlays(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.inlay_hints = + Some(InlayHintSettingsContent { + enabled: Some(true), + ..InlayHintSettingsContent::default() + }) + }); + }); + }); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/dir"), + // `\n` , a trailing line on the end, is important for the test case + json!({ + "main.rs": "fn main() { let a = 2; }\n", + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let language = rust_lang(); + language_registry.add(language); + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + initializer: Some(Box::new(|fake_server| { + fake_server.set_request_handler::( + move |_, _| async move { + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, 17), + label: lsp::InlayHintLabel::String(": i32".to_owned()), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + }, + ); + })), + ..FakeLspAdapter::default() + }, + ); + + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let workspace = window.root(cx).unwrap(); + let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); + let search_view = cx.add_window(|window, cx| { + ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) + }); + + perform_search(search_view, "let ", cx); + let _fake_server = fake_servers.next().await.unwrap(); + cx.executor().advance_clock(Duration::from_secs(1)); + cx.executor().run_until_parked(); + search_view + .update(cx, |search_view, _, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nfn main() { let a: i32 = 2; }\n" + ); + }) + .unwrap(); + + // Can do the 2nd search without any panics + perform_search(search_view, "let ", cx); + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); + search_view + .update(cx, |search_view, _, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nfn main() { let a: i32 = 2; }\n" + ); + }) + .unwrap(); + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings = SettingsStore::test(cx); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 59efaff200ce98fd2150932b24492e42a07fa265..0743601839cc31e0e3a4c9d6c936aab7edce5837 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -934,7 +934,7 @@ where /// 2. When encountering digits, treating consecutive digits as a single number /// 3. Comparing numbers by their numeric value rather than lexicographically /// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority -fn natural_sort(a: &str, b: &str) -> Ordering { +pub fn natural_sort(a: &str, b: &str) -> Ordering { let mut a_iter = a.chars().peekable(); let mut b_iter = b.chars().peekable(); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e44039914f801848ce362b081f6e1bcd18b3c1fa..1a617e36c18ffa52906cac06d4b9eddb11a91f8e 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -3083,7 +3083,7 @@ mod test { state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; - use editor::display_map::Inlay; + use editor::Inlay; use indoc::indoc; use language::Point; use multi_buffer::MultiBufferRow;