diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 350b1d41be..a9b8f5b0a7 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1637,12 +1637,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( extract_hint_labels(editor), "Host should get its first hints when opens an editor" ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Host editor update the cache version after every cache/view change", - ); }); let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let editor_b = workspace_b @@ -1661,12 +1655,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( extract_hint_labels(editor), "Client should get its first hints when opens an editor" ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Guest editor update the cache version after every cache/view change" - ); }); let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; @@ -1682,16 +1670,12 @@ async fn test_mutual_editor_inlay_hint_cache_update( vec![after_client_edit.to_string()], extract_hint_labels(editor), ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 2); }); editor_b.update(cx_b, |editor, _| { assert_eq!( vec![after_client_edit.to_string()], extract_hint_labels(editor), ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 2); }); let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; @@ -1707,16 +1691,12 @@ async fn test_mutual_editor_inlay_hint_cache_update( vec![after_host_edit.to_string()], extract_hint_labels(editor), ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 3); }); editor_b.update(cx_b, |editor, _| { assert_eq!( vec![after_host_edit.to_string()], extract_hint_labels(editor), ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 3); }); let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; @@ -1732,12 +1712,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( extract_hint_labels(editor), "Host should react to /refresh LSP request" ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 4, - "Host should accepted all edits and bump its cache version every time" - ); }); editor_b.update(cx_b, |editor, _| { assert_eq!( @@ -1745,12 +1719,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( extract_hint_labels(editor), "Guest should get a /refresh LSP request propagated by host" ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 4, - "Guest should accepted all edits and bump its cache version every time" - ); }); } @@ -1906,12 +1874,6 @@ async fn test_inlay_hint_refresh_is_forwarded( extract_hint_labels(editor).is_empty(), "Host should get no hints due to them turned off" ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 0, - "Turned off hints should not generate version updates" - ); }); executor.run_until_parked(); @@ -1921,12 +1883,6 @@ async fn test_inlay_hint_refresh_is_forwarded( extract_hint_labels(editor), "Client should get its first hints when opens an editor" ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Should update cache version after first hints" - ); }); other_hints.fetch_or(true, atomic::Ordering::Release); @@ -1938,13 +1894,7 @@ async fn test_inlay_hint_refresh_is_forwarded( editor_a.update(cx_a, |editor, _| { assert!( extract_hint_labels(editor).is_empty(), - "Host should get nop hints due to them turned off, even after the /refresh" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 0, - "Turned off hints should not generate version updates, again" + "Host should get no hints due to them turned off, even after the /refresh" ); }); @@ -1955,12 +1905,6 @@ async fn test_inlay_hint_refresh_is_forwarded( extract_hint_labels(editor), "Guest should get a /refresh LSP request propagated by host despite host hints are off" ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 2, - "Guest should accepted all edits and bump its cache version every time" - ); }); } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 01dd12cc98..cbf75943ba 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -569,6 +569,7 @@ impl InlayMap { probe .position .cmp(&inlay_to_insert.position, &snapshot.buffer) + .then(std::cmp::Ordering::Less) }) { Ok(ix) | Err(ix) => { self.inlays.insert(ix, inlay_to_insert); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bc2735b891..2f7962e57e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12441,6 +12441,7 @@ impl Editor { cx.emit(EditorEvent::ExcerptsEdited { ids: ids.clone() }) } multi_buffer::Event::ExcerptsExpanded { ids } => { + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() }) } multi_buffer::Event::Reparsed(buffer_id) => { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 327ebbb5e2..8df011e7dd 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1,5 +1,5 @@ /// 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. +/// 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. @@ -38,7 +38,7 @@ pub struct InlayHintCache { pub(super) enabled: bool, enabled_in_settings: bool, update_tasks: HashMap, - refresh_task: Option>, + refresh_task: Task<()>, invalidate_debounce: Option, append_debounce: Option, lsp_request_limiter: Arc, @@ -116,13 +116,9 @@ impl InvalidationStrategy { impl TasksForRanges { fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { - let mut sorted_ranges = Vec::new(); - sorted_ranges.extend(query_ranges.before_visible); - sorted_ranges.extend(query_ranges.visible); - sorted_ranges.extend(query_ranges.after_visible); Self { tasks: vec![task], - sorted_ranges, + sorted_ranges: query_ranges.into_sorted_query_ranges(), } } @@ -135,7 +131,7 @@ impl TasksForRanges { ) { let query_ranges = if invalidate.should_invalidate() { self.tasks.clear(); - self.sorted_ranges.clear(); + self.sorted_ranges = query_ranges.clone().into_sorted_query_ranges(); query_ranges } else { let mut non_cached_query_ranges = query_ranges; @@ -272,7 +268,7 @@ impl InlayHintCache { enabled_in_settings: inlay_hint_settings.enabled, hints: HashMap::default(), update_tasks: HashMap::default(), - refresh_task: None, + 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, @@ -384,7 +380,7 @@ impl InlayHintCache { } else { self.append_debounce }; - self.refresh_task = Some(cx.spawn(|editor, mut cx| async move { + self.refresh_task = cx.spawn(|editor, mut cx| async move { if let Some(debounce_duration) = debounce_duration { cx.background_executor().timer(debounce_duration).await; } @@ -401,7 +397,7 @@ impl InlayHintCache { ) }) .ok(); - })); + }); if invalidated_hints.is_empty() { None @@ -580,10 +576,6 @@ impl InlayHintCache { hints } - pub fn version(&self) -> usize { - self.version - } - /// Queries a certain hint from the cache for extra data via the LSP resolve request. pub(super) fn spawn_hint_resolve( &self, @@ -731,6 +723,16 @@ 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( @@ -940,7 +942,7 @@ fn fetch_and_update_hints( 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."); + 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 @@ -1008,12 +1010,12 @@ fn fetch_and_update_hints( }) .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() - // ); + 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() + ); log::trace!("New update: {new_update:?}"); editor .update(&mut cx, |editor, cx| { @@ -1181,48 +1183,39 @@ fn apply_hint_update( .cmp(&new_hint.position, &buffer_snapshot) }) { Ok(i) => { - let mut insert_position = Some(i); - for id in &cached_excerpt_hints.ordered_hints[i..] { - let cached_hint = &cached_excerpt_hints.hints_by_id[id]; - if new_hint - .position - .cmp(&cached_hint.position, &buffer_snapshot) - .is_gt() - { - break; - } - if cached_hint.text() == new_hint.text() { - insert_position = None; - break; - } - } - insert_position + // 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) => Some(i), + Err(i) => i, }; - if let Some(insert_position) = insert_position { - let new_inlay_id = post_inc(&mut editor.next_inlay_id); - if editor - .inlay_hint_cache - .allowed_hint_kinds - .contains(&new_hint.kind) + let new_inlay_id = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + if let Some(new_hint_position) = + multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position) { - if 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)); - } + 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); + } + 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_inlays_changed = true; } cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); drop(cached_excerpt_hints); @@ -1266,15 +1259,19 @@ fn apply_hint_update( #[cfg(test)] pub mod tests { use crate::editor_tests::update_test_language_settings; + use crate::scroll::ScrollAmount; use crate::{scroll::Autoscroll, test::editor_lsp_test_context::rust_lang, ExcerptRange}; use futures::StreamExt; use gpui::{Context, SemanticVersion, TestAppContext, WindowHandle}; + use itertools::Itertools as _; use language::{language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter}; + use language::{Language, LanguageConfig, LanguageMatcher}; use lsp::FakeLanguageServer; + use parking_lot::Mutex; use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; - use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use text::Point; use super::*; @@ -1293,50 +1290,35 @@ pub mod tests { show_background: false, }) }); - let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| { let lsp_request_count = Arc::new(AtomicU32::new(0)); fake_server.handle_request::(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::Url::from_file_path(file_with_hints).unwrap(), ); - let current_call_id = - Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); - let mut new_hints = Vec::with_capacity(2 * current_call_id as usize); - for _ in 0..2 { - let mut i = current_call_id; - loop { - new_hints.push(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, - }); - if i == 0 { - break; - } - i -= 1; - } - } - - Ok(Some(new_hints)) + 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(); - let mut edits_made = 1; editor .update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, cached_hint_labels(editor), @@ -1348,10 +1330,6 @@ pub mod tests { inlay_cache.allowed_hint_kinds, allowed_hint_kinds, "Cache should use editor settings to get the allowed hint kinds" ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor update the cache version after every cache/view change" - ); }) .unwrap(); @@ -1359,13 +1337,12 @@ pub mod tests { .update(cx, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); editor.handle_input("some change", cx); - edits_made += 1; }) .unwrap(); cx.executor().run_until_parked(); editor .update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string(), "1".to_string()]; + let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, cached_hint_labels(editor), @@ -1377,10 +1354,6 @@ pub mod tests { inlay_cache.allowed_hint_kinds, allowed_hint_kinds, "Cache should use editor settings to get the allowed hint kinds" ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor update the cache version after every cache/view change" - ); }) .unwrap(); @@ -1388,11 +1361,10 @@ pub mod tests { .request::(()) .await .expect("inlay refresh request failed"); - edits_made += 1; cx.executor().run_until_parked(); editor .update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + let expected_hints = vec!["3".to_string()]; assert_eq!( expected_hints, cached_hint_labels(editor), @@ -1404,10 +1376,6 @@ pub mod tests { inlay_cache.allowed_hint_kinds, allowed_hint_kinds, "Cache should use editor settings to get the allowed hint kinds" ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor update the cache version after every cache/view change" - ); }) .unwrap(); } @@ -1453,7 +1421,6 @@ pub mod tests { .await; cx.executor().run_until_parked(); - let mut edits_made = 1; editor .update(cx, |editor, cx| { let expected_hints = vec!["0".to_string()]; @@ -1463,11 +1430,6 @@ pub mod tests { "Should get its first hints when opening the editor" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - edits_made, - "The editor update the cache version after every cache/view change" - ); }) .unwrap(); @@ -1496,11 +1458,6 @@ pub mod tests { "Should not update hints while the work task is running" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - edits_made, - "Should not update the cache while the work task is running" - ); }) .unwrap(); @@ -1512,7 +1469,6 @@ pub mod tests { }); cx.executor().run_until_parked(); - edits_made += 1; editor .update(cx, |editor, cx| { let expected_hints = vec!["1".to_string()]; @@ -1522,246 +1478,223 @@ pub mod tests { "New hints should be queried after the work task is done" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - edits_made, - "Cache version should update once after the work task is done" - ); }) .unwrap(); } - // #[gpui::test] - // async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { - // init_test(cx, |settings| { - // settings.defaults.inlay_hints = Some(InlayHintSettings { - // enabled: true, - // edit_debounce_ms: 0, - // scroll_debounce_ms: 0, - // show_type_hints: true, - // show_parameter_hints: true, - // show_other_hints: true, - // show_background: false, - // }) - // }); + #[gpui::test] + async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + show_background: false, + }) + }); - // let fs = FakeFs::new(cx.background_executor.clone()); - // fs.insert_tree( - // "/a", - // json!({ - // "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", - // "other.md": "Test md file with some text", - // }), - // ) - // .await; + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.md": "Test md file with some text", + }), + ) + .await; - // let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; - // let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - // let mut rs_fake_servers = None; - // let mut md_fake_servers = None; - // for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { - // language_registry.add(Arc::new(Language::new( - // LanguageConfig { - // name: name.into(), - // matcher: LanguageMatcher { - // path_suffixes: vec![path_suffix.to_string()], - // ..Default::default() - // }, - // ..Default::default() - // }, - // Some(tree_sitter_rust::LANGUAGE.into()), - // ))); - // let fake_servers = language_registry.register_fake_lsp( - // name, - // FakeLspAdapter { - // name, - // capabilities: lsp::ServerCapabilities { - // inlay_hint_provider: Some(lsp::OneOf::Left(true)), - // ..Default::default() - // }, - // ..Default::default() - // }, - // ); - // match name { - // "Rust" => rs_fake_servers = Some(fake_servers), - // "Markdown" => md_fake_servers = Some(fake_servers), - // _ => unreachable!(), - // } - // } + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let mut rs_fake_servers = None; + let mut md_fake_servers = None; + for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { + language_registry.add(Arc::new(Language::new( + LanguageConfig { + name: name.into(), + matcher: LanguageMatcher { + path_suffixes: vec![path_suffix.to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ))); + let fake_servers = language_registry.register_fake_lsp( + name, + FakeLspAdapter { + name, + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + initializer: Some(Box::new({ + move |fake_server| { + let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); + let md_lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server.handle_request::( + move |params, _| { + let i = match name { + "Rust" => { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + rs_lsp_request_count.fetch_add(1, Ordering::Release) + 1 + } + "Markdown" => { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/other.md").unwrap(), + ); + md_lsp_request_count.fetch_add(1, Ordering::Release) + 1 + } + unexpected => panic!("Unexpected language: {unexpected}"), + }; - // let rs_buffer = project - // .update(cx, |project, cx| { - // project.open_local_buffer("/a/main.rs", cx) - // }) - // .await - // .unwrap(); - // let rs_editor = - // cx.add_window(|cx| Editor::for_buffer(rs_buffer, Some(project.clone()), cx)); + async move { + let query_start = params.range.start; + Ok(Some(vec![lsp::InlayHint { + position: query_start, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }, + ); + } + })), + ..Default::default() + }, + ); + match name { + "Rust" => rs_fake_servers = Some(fake_servers), + "Markdown" => md_fake_servers = Some(fake_servers), + _ => unreachable!(), + } + } - // cx.executor().run_until_parked(); - // cx.executor().start_waiting(); - // let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); - // let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); - // rs_fake_server - // .handle_request::(move |params, _| { - // let task_lsp_request_count = Arc::clone(&rs_lsp_request_count); - // async move { - // assert_eq!( - // params.text_document.uri, - // lsp::Url::from_file_path("/a/main.rs").unwrap(), - // ); - // let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); - // 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, - // }])) - // } - // }) - // .next() - // .await; - // cx.executor().run_until_parked(); - // rs_editor - // .update(cx, |editor, cx| { - // let expected_hints = vec!["0".to_string()]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "Should get its first hints when opening the editor" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!( - // editor.inlay_hint_cache().version, - // 1, - // "Rust editor update the cache version after every cache/view change" - // ); - // }) - // .unwrap(); + let rs_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let rs_editor = + cx.add_window(|cx| Editor::for_buffer(rs_buffer, Some(project.clone()), cx)); + cx.executor().run_until_parked(); - // cx.executor().run_until_parked(); - // let md_buffer = project - // .update(cx, |project, cx| { - // project.open_local_buffer("/a/other.md", cx) - // }) - // .await - // .unwrap(); - // let md_editor = cx.add_window(|cx| Editor::for_buffer(md_buffer, Some(project), cx)); + let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); + cx.executor().run_until_parked(); + rs_editor + .update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); - // cx.executor().run_until_parked(); - // cx.executor().start_waiting(); - // let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); - // let md_lsp_request_count = Arc::new(AtomicU32::new(0)); - // md_fake_server - // .handle_request::(move |params, _| { - // let task_lsp_request_count = Arc::clone(&md_lsp_request_count); - // async move { - // assert_eq!( - // params.text_document.uri, - // lsp::Url::from_file_path("/a/other.md").unwrap(), - // ); - // let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); - // 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, - // }])) - // } - // }) - // .next() - // .await; - // cx.executor().run_until_parked(); - // md_editor - // .update(cx, |editor, cx| { - // let expected_hints = vec!["0".to_string()]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "Markdown editor should have a separate version, repeating Rust editor rules" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, 1); - // }) - // .unwrap(); + cx.executor().run_until_parked(); + let md_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/other.md", cx) + }) + .await + .unwrap(); + let md_editor = cx.add_window(|cx| Editor::for_buffer(md_buffer, Some(project), cx)); + cx.executor().run_until_parked(); - // rs_editor - // .update(cx, |editor, cx| { - // editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - // editor.handle_input("some rs change", cx); - // }) - // .unwrap(); - // cx.executor().run_until_parked(); - // rs_editor - // .update(cx, |editor, cx| { - // let expected_hints = vec!["1".to_string()]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "Rust inlay cache should change after the edit" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!( - // editor.inlay_hint_cache().version, - // 2, - // "Every time hint cache changes, cache version should be incremented" - // ); - // }) - // .unwrap(); - // md_editor - // .update(cx, |editor, cx| { - // let expected_hints = vec!["0".to_string()]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "Markdown editor should not be affected by Rust editor changes" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, 1); - // }) - // .unwrap(); + let _md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); + cx.executor().run_until_parked(); + md_editor + .update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should have a separate version, repeating Rust editor rules" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); - // md_editor - // .update(cx, |editor, cx| { - // editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - // editor.handle_input("some md change", cx); - // }) - // .unwrap(); - // cx.executor().run_until_parked(); - // md_editor - // .update(cx, |editor, cx| { - // let expected_hints = vec!["1".to_string()]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "Rust editor should not be affected by Markdown editor changes" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, 2); - // }) - // .unwrap(); - // rs_editor - // .update(cx, |editor, cx| { - // let expected_hints = vec!["1".to_string()]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "Markdown editor should also change independently" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, 2); - // }) - // .unwrap(); - // } + rs_editor + .update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some rs change", cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + rs_editor + .update(cx, |editor, 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()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Rust inlay cache should change after the edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); + md_editor + .update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should not be affected by Rust editor changes" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); + + md_editor + .update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some md change", cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + md_editor + .update(cx, |editor, cx| { + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Rust editor should not be affected by Markdown editor changes" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); + rs_editor + .update(cx, |editor, cx| { + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should also change independently" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); + } #[gpui::test] async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { @@ -1778,14 +1711,14 @@ pub mod tests { }) }); - let lsp_request_count = Arc::new(AtomicU32::new(0)); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); let (_, editor, fake_server) = prepare_test_objects(cx, { let lsp_request_count = lsp_request_count.clone(); move |fake_server, file_with_hints| { let lsp_request_count = lsp_request_count.clone(); fake_server.handle_request::( move |params, _| { - lsp_request_count.fetch_add(1, Ordering::SeqCst); + lsp_request_count.fetch_add(1, Ordering::Release); async move { assert_eq!( params.text_document.uri, @@ -1833,7 +1766,6 @@ pub mod tests { .await; cx.executor().run_until_parked(); - let mut edits_made = 1; editor .update(cx, |editor, cx| { assert_eq!( @@ -1843,15 +1775,15 @@ pub mod tests { ); assert_eq!( vec![ - "other hint".to_string(), - "parameter hint".to_string(), "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), ], cached_hint_labels(editor), "Should get its first hints when opening the editor" ); assert_eq!( - vec!["other hint".to_string(), "type hint".to_string()], + vec!["type hint".to_string(), "other hint".to_string()], visible_hint_labels(editor, cx) ); let inlay_cache = editor.inlay_hint_cache(); @@ -1859,10 +1791,6 @@ pub mod tests { inlay_cache.allowed_hint_kinds, allowed_hint_kinds, "Cache should use editor settings to get the allowed hint kinds" ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor update the cache version after every cache/view change" - ); }) .unwrap(); @@ -1880,22 +1808,17 @@ pub mod tests { ); assert_eq!( vec![ - "other hint".to_string(), - "parameter hint".to_string(), "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), ], cached_hint_labels(editor), "Cached hints should not change due to allowed hint kinds settings update" ); assert_eq!( - vec!["other hint".to_string(), "type hint".to_string()], + vec!["type hint".to_string(), "other hint".to_string()], visible_hint_labels(editor, cx) ); - assert_eq!( - editor.inlay_hint_cache().version, - edits_made, - "Should not update cache version due to new loaded hints being the same" - ); }) .unwrap(); @@ -1911,15 +1834,15 @@ pub mod tests { ), ( HashSet::from_iter([None, Some(InlayHintKind::Type)]), - vec!["other hint".to_string(), "type hint".to_string()], + vec!["type hint".to_string(), "other hint".to_string()], ), ( HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), - vec!["other hint".to_string(), "parameter hint".to_string()], + vec!["parameter hint".to_string(), "other hint".to_string()], ), ( HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), - vec!["parameter hint".to_string(), "type hint".to_string()], + vec!["type hint".to_string(), "parameter hint".to_string()], ), ( HashSet::from_iter([ @@ -1928,13 +1851,12 @@ pub mod tests { Some(InlayHintKind::Parameter), ]), vec![ - "other hint".to_string(), - "parameter hint".to_string(), "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), ], ), ] { - edits_made += 1; update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, @@ -1956,9 +1878,9 @@ pub mod tests { ); assert_eq!( vec![ - "other hint".to_string(), - "parameter hint".to_string(), "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), ], cached_hint_labels(editor), "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" @@ -1973,14 +1895,9 @@ pub mod tests { inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change" - ); }).unwrap(); } - edits_made += 1; let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { @@ -2015,10 +1932,6 @@ pub mod tests { inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, "Should update its allowed hint kinds even when hints got disabled" ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor should update the cache version after hints got disabled" - ); }) .unwrap(); @@ -2027,22 +1940,19 @@ pub mod tests { .await .expect("inlay refresh request failed"); cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 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!( - editor.inlay_hint_cache().version, edits_made, - "The editor should not update the cache version after /refresh query without updates" - ); - }).unwrap(); + editor + .update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 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()); + }) + .unwrap(); let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); - edits_made += 1; update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, @@ -2065,9 +1975,9 @@ pub mod tests { ); assert_eq!( vec![ - "other hint".to_string(), - "parameter hint".to_string(), "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), ], cached_hint_labels(editor), "Should get its cached hints fully repopulated after the hints got re-enabled" @@ -2082,10 +1992,6 @@ pub mod tests { inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, "Cache should update editor settings when hints got re-enabled" ); - assert_eq!( - inlay_cache.version, edits_made, - "Cache should update its version after hints got re-enabled" - ); }) .unwrap(); @@ -2103,9 +2009,9 @@ pub mod tests { ); assert_eq!( vec![ - "other hint".to_string(), - "parameter hint".to_string(), "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), ], cached_hint_labels(editor), ); @@ -2113,7 +2019,6 @@ pub mod tests { vec!["parameter hint".to_string()], visible_hint_labels(editor, cx), ); - assert_eq!(editor.inlay_hint_cache().version, edits_made); }) .unwrap(); } @@ -2180,27 +2085,29 @@ pub mod tests { cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { - let current_text = editor.text(cx); - for change in &expected_changes { - assert!( - current_text.contains(change), - "Should apply all changes made" + editor + .update(cx, |editor, cx| { + let current_text = editor.text(cx); + for change in &expected_changes { + assert!( + current_text.contains(change), + "Should apply all changes made" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should query new hints twice: for editor init and for the last edit that interrupted all others" ); - } - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 2, - "Should query new hints twice: for editor init and for the last edit that interrupted all others" - ); - let expected_hints = vec!["2".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get hints from the last edit landed only" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - }).unwrap(); + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get hints from the last edit landed only" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); let mut edits = Vec::new(); for async_later_change in [ @@ -2247,309 +2154,300 @@ pub mod tests { .unwrap(); } - // #[gpui::test(iterations = 10)] - // async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { - // init_test(cx, |settings| { - // settings.defaults.inlay_hints = Some(InlayHintSettings { - // enabled: true, - // edit_debounce_ms: 0, - // scroll_debounce_ms: 0, - // show_type_hints: true, - // show_parameter_hints: true, - // show_other_hints: true, - // show_background: false, - // }) - // }); + #[gpui::test(iterations = 10)] + async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + show_background: false, + }) + }); - // let fs = FakeFs::new(cx.background_executor.clone()); - // fs.insert_tree( - // "/a", - // json!({ - // "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), - // "other.rs": "// Test file", - // }), - // ) - // .await; + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; - // let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; - // let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - // language_registry.add(rust_lang()); + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); - // let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); - // let lsp_request_count = Arc::new(AtomicUsize::new(0)); - // let mut fake_servers = language_registry.register_fake_lsp( - // "Rust", - // FakeLspAdapter { - // capabilities: lsp::ServerCapabilities { - // inlay_hint_provider: Some(lsp::OneOf::Left(true)), - // ..Default::default() - // }, - // initializer: Some(Box::new({ - // let lsp_request_ranges = lsp_request_ranges.clone(); - // let lsp_request_count = lsp_request_count.clone(); - // move |fake_server| { - // let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); - // let closure_lsp_request_count = Arc::clone(&lsp_request_count); - // fake_server.handle_request::( - // move |params, _| { - // let task_lsp_request_ranges = - // Arc::clone(&closure_lsp_request_ranges); - // let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); - // async move { - // assert_eq!( - // params.text_document.uri, - // lsp::Url::from_file_path("/a/main.rs").unwrap(), - // ); + let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + initializer: Some(Box::new({ + let lsp_request_ranges = lsp_request_ranges.clone(); + let lsp_request_count = lsp_request_count.clone(); + move |fake_server| { + let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server.handle_request::( + move |params, _| { + let task_lsp_request_ranges = + Arc::clone(&closure_lsp_request_ranges); + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); - // task_lsp_request_ranges.lock().push(params.range); - // let i = Arc::clone(&task_lsp_request_count) - // .fetch_add(1, Ordering::Release) - // + 1; - // Ok(Some(vec![lsp::InlayHint { - // position: params.range.end, - // label: lsp::InlayHintLabel::String(i.to_string()), - // kind: None, - // text_edits: None, - // tooltip: None, - // padding_left: None, - // padding_right: None, - // data: None, - // }])) - // } - // }, - // ); - // } - // })), - // ..Default::default() - // }, - // ); + task_lsp_request_ranges.lock().push(params.range); + task_lsp_request_count.fetch_add(1, Ordering::Release); + Ok(Some(vec![lsp::InlayHint { + position: params.range.end, + label: lsp::InlayHintLabel::String( + params.range.end.line.to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }, + ); + } + })), + ..Default::default() + }, + ); - // let buffer = project - // .update(cx, |project, cx| { - // project.open_local_buffer("/a/main.rs", cx) - // }) - // .await - // .unwrap(); - // let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); - // cx.executor().run_until_parked(); + cx.executor().run_until_parked(); - // fn editor_visible_range( - // editor: &WindowHandle, - // cx: &mut gpui::TestAppContext, - // ) -> Range { - // let ranges = editor - // .update(cx, |editor, cx| { - // editor.excerpts_for_inlay_hints_query(None, cx) - // }) - // .unwrap(); - // assert_eq!( - // ranges.len(), - // 1, - // "Single buffer should produce a single excerpt with visible range" - // ); - // let (_, (excerpt_buffer, _, excerpt_visible_range)) = - // ranges.into_iter().next().unwrap(); - // excerpt_buffer.update(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 - // }) - // } + let _fake_server = fake_servers.next().await.unwrap(); - // // 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 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, 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]; + // 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 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, 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"); + assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); + assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); - // let requests_count = lsp_request_count.load(Ordering::Acquire); - // assert_eq!(requests_count, 2, "Visible + invisible request"); - // let expected_hints = vec!["1".to_string(), "2".to_string()]; - // assert_eq!( - // expected_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"); - // assert_eq!( - // editor.inlay_hint_cache().version, requests_count, - // "LSP queries should've bumped the cache version" - // ); - // }).unwrap(); + 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, cx| { - // editor.scroll_screen(&ScrollAmount::Page(1.0), cx); - // }) - // .unwrap(); - // cx.executor().run_until_parked(); - // editor - // .update(cx, |editor, cx| { - // editor.scroll_screen(&ScrollAmount::Page(1.0), cx); - // }) - // .unwrap(); - // cx.executor().advance_clock(Duration::from_millis( - // INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - // )); - // cx.executor().run_until_parked(); - // let visible_range_after_scrolls = editor_visible_range(&editor, cx); - // let visible_line_count = editor - // .update(cx, |editor, _| editor.visible_line_count().unwrap()) - // .unwrap(); - // let selection_in_cached_range = editor - // .update(cx, |editor, cx| { - // let ranges = lsp_request_ranges - // .lock() - // .drain(..) - // .sorted_by_key(|r| r.start) - // .collect::>(); - // assert_eq!( - // ranges.len(), - // 2, - // "Should query 2 ranges after both scrolls, but got: {ranges:?}" - // ); - // let first_scroll = &ranges[0]; - // let second_scroll = &ranges[1]; - // assert_eq!( - // first_scroll.end, second_scroll.start, - // "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" - // ); + editor + .update(cx, |editor, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + }) + .unwrap(); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let visible_range_after_scrolls = editor_visible_range(&editor, cx); + let visible_line_count = editor + .update(cx, |editor, _| editor.visible_line_count().unwrap()) + .unwrap(); + let selection_in_cached_range = editor + .update(cx, |editor, cx| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert_eq!( + ranges.len(), + 2, + "Should query 2 ranges after both scrolls, but got: {ranges:?}" + ); + let first_scroll = &ranges[0]; + let second_scroll = &ranges[1]; + assert_eq!( + first_scroll.end, second_scroll.start, + "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![ - // "1".to_string(), - // "2".to_string(), - // "3".to_string(), - // "4".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!( - // editor.inlay_hint_cache().version, - // lsp_requests, - // "Should update the cache for every LSP response with hints added" - // ); + 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" + ); + 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; - // selection_in_cached_range - // }) - // .unwrap(); + let mut selection_in_cached_range = visible_range_after_scrolls.end; + selection_in_cached_range.row -= visible_line_count.ceil() as u32; + selection_in_cached_range + }) + .unwrap(); - // editor - // .update(cx, |editor, cx| { - // editor.change_selections(Some(Autoscroll::center()), cx, |s| { - // s.select_ranges([selection_in_cached_range..selection_in_cached_range]) - // }); - // }) - // .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 - // .lock() - // .drain(..) - // .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); - // }).unwrap(); + editor + .update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }) + .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 + .lock() + .drain(..) + .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); + }).unwrap(); - // editor - // .update(cx, |editor, cx| { - // editor.handle_input("++++more text++++", 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, cx| { - // let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - // ranges.sort_by_key(|r| r.start); + editor + .update(cx, |editor, cx| { + editor.handle_input("++++more text++++", 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, 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, - // "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"); + 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, + "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!["5".to_string(), "6".to_string(), "7".to_string()]; - // assert_eq!(expected_hints, cached_hint_labels(editor), - // "Should have hints from the new LSP response after the edit"); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); - // }).unwrap(); - // } + 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)); + }).unwrap(); + } + + fn editor_visible_range( + editor: &WindowHandle, + cx: &mut gpui::TestAppContext, + ) -> Range { + let ranges = editor + .update(cx, |editor, cx| { + editor.excerpts_for_inlay_hints_query(None, cx) + }) + .unwrap(); + assert_eq!( + ranges.len(), + 1, + "Single buffer should produce a single excerpt with visible range" + ); + let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap(); + excerpt_buffer.update(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 + }) + } #[gpui::test] async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { @@ -2723,8 +2621,8 @@ pub mod tests { lsp::InlayHint { position, label: lsp::InlayHintLabel::String(format!( - "{hint_text}{} #{i}", - if edited { "(edited)" } else { "" }, + "{hint_text}{E} #{i}", + E = if edited { "(edited)" } else { "" }, )), kind: None, text_edits: None, @@ -2742,7 +2640,8 @@ pub mod tests { .await; cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { + editor + .update(cx, |editor, cx| { let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), @@ -2753,12 +2652,12 @@ pub mod tests { ]; assert_eq!( expected_hints, - cached_hint_labels(editor), + sorted_cached_hint_labels(editor), "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the version"); - }).unwrap(); + }) + .unwrap(); editor .update(cx, |editor, cx| { @@ -2774,7 +2673,8 @@ pub mod tests { }) .unwrap(); cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { + editor + .update(cx, |editor, cx| { let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), @@ -2786,12 +2686,11 @@ pub mod tests { "other hint #1".to_string(), "other hint #2".to_string(), ]; - assert_eq!(expected_hints, cached_hint_labels(editor), + assert_eq!(expected_hints, sorted_cached_hint_labels(editor), "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), - "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); - }).unwrap(); + }) + .unwrap(); editor .update(cx, |editor, cx| { @@ -2804,7 +2703,8 @@ pub mod tests { INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, )); cx.executor().run_until_parked(); - let last_scroll_update_version = editor.update(cx, |editor, cx| { + editor + .update(cx, |editor, cx| { let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), @@ -2819,12 +2719,11 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_hints, cached_hint_labels(editor), + 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!(editor.inlay_hint_cache().version, expected_hints.len()); - expected_hints.len() - }).unwrap(); + }) + .unwrap(); editor .update(cx, |editor, cx| { @@ -2837,7 +2736,8 @@ pub mod tests { INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, )); cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { + editor + .update(cx, |editor, cx| { let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), @@ -2852,11 +2752,11 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_hints, cached_hint_labels(editor), + 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!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scrolled buffer"); - }).unwrap(); + }) + .unwrap(); editor_edited.store(true, Ordering::Release); editor @@ -2868,35 +2768,28 @@ pub mod tests { }) .unwrap(); cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec![ - "main hint #0".to_string(), - "main hint #1".to_string(), - "main hint #2".to_string(), - "main hint #3".to_string(), - "main hint #4".to_string(), - "main hint #5".to_string(), - "other hint(edited) #0".to_string(), - "other hint(edited) #1".to_string(), - "other hint(edited) #2".to_string(), - ]; - assert_eq!( - expected_hints, - 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" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - - let current_cache_version = editor.inlay_hint_cache().version; - // We expect three new hints for the excerpts from `other.rs`: - let expected_version = last_scroll_update_version + 3; - assert_eq!( - current_cache_version, - expected_version, - "We should have updated cache N times == N of new hints arrived (separately from each edited excerpt)" - ); - }).unwrap(); + editor + .update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), + "other hint(edited) #2".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" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); } #[gpui::test] @@ -3056,11 +2949,6 @@ pub mod tests { visible_hint_labels(editor, cx).is_empty(), "All hints are disabled and should not be shown despite being present in the cache" ); - assert_eq!( - editor.inlay_hint_cache().version, - 2, - "Cache should update once per excerpt query" - ); }) .unwrap(); @@ -3083,11 +2971,6 @@ pub mod tests { visible_hint_labels(editor, cx).is_empty(), "All hints are disabled and should not be shown despite being present in the cache" ); - assert_eq!( - editor.inlay_hint_cache().version, - 3, - "Excerpt removal should trigger a cache update" - ); }) .unwrap(); @@ -3116,11 +2999,6 @@ pub mod tests { visible_hint_labels(editor, cx), "Settings change should make cached hints visible" ); - assert_eq!( - editor.inlay_hint_cache().version, - 4, - "Settings change should trigger a cache update" - ); }) .unwrap(); } @@ -3197,9 +3075,6 @@ pub mod tests { .unwrap(); let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); - cx.executor().run_until_parked(); - cx.executor().start_waiting(); - cx.executor().run_until_parked(); editor .update(cx, |editor, cx| { @@ -3214,7 +3089,6 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!(expected_hints, cached_hint_labels(editor)); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 1); }) .unwrap(); } @@ -3264,7 +3138,6 @@ pub mod tests { editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) }) .unwrap(); - cx.executor().start_waiting(); cx.executor().run_until_parked(); editor @@ -3276,11 +3149,6 @@ pub mod tests { "Should display inlays after toggle despite them disabled in settings" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - 1, - "First toggle should be cache's first update" - ); }) .unwrap(); @@ -3297,7 +3165,6 @@ pub mod tests { "Should clear hints after 2nd toggle" ); assert!(visible_hint_labels(editor, cx).is_empty()); - assert_eq!(editor.inlay_hint_cache().version, 2); }) .unwrap(); @@ -3322,7 +3189,6 @@ pub mod tests { "Should query LSP hints for the 2nd time after enabling hints in settings" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 3); }) .unwrap(); @@ -3339,7 +3205,6 @@ pub mod tests { "Should clear hints after enabling in settings and a 3rd toggle" ); assert!(visible_hint_labels(editor, cx).is_empty()); - assert_eq!(editor.inlay_hint_cache().version, 4); }) .unwrap(); @@ -3357,10 +3222,160 @@ pub mod tests { "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 5); }).unwrap(); } + #[gpui::test] + async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + show_background: false, + }) + }); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { + let x = 42; + std::thread::scope(|s| { + s.spawn(|| { + let _x = x; + }); + }); + }", + "other.rs": "// Test file", + }), + ) + .await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + initializer: Some(Box::new(move |fake_server| { + fake_server.handle_request::( + move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + Ok(Some( + serde_json::from_value(json!([ + { + "position": { + "line": 3, + "character": 16 + }, + "label": "move", + "paddingLeft": false, + "paddingRight": false + }, + { + "position": { + "line": 3, + "character": 16 + }, + "label": "(", + "paddingLeft": false, + "paddingRight": false + }, + { + "position": { + "line": 3, + "character": 16 + }, + "label": [ + { + "value": "&x" + } + ], + "paddingLeft": false, + "paddingRight": false, + "data": { + "file_id": 0 + } + }, + { + "position": { + "line": 3, + "character": 16 + }, + "label": ")", + "paddingLeft": false, + "paddingRight": true + }, + // not a correct syntax, but checks that same symbols at the same place + // are not deduplicated + { + "position": { + "line": 3, + "character": 16 + }, + "label": ")", + "paddingLeft": false, + "paddingRight": true + }, + ])) + .unwrap(), + )) + }, + ); + })), + ..FakeLspAdapter::default() + }, + ); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + + cx.executor().run_until_parked(); + editor + .update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) + }) + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, cx| { + let expected_hints = vec![ + "move".to_string(), + "(".to_string(), + "&x".to_string(), + ") ".to_string(), + ") ".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Editor inlay hints should repeat server's order when placed at the same spot" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + }) + .unwrap(); + } + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); @@ -3420,38 +3435,47 @@ pub mod tests { .update(cx, |editor, cx| { assert!(cached_hint_labels(editor).is_empty()); assert!(visible_hint_labels(editor, cx).is_empty()); - assert_eq!(editor.inlay_hint_cache().version, 0); }) .unwrap(); cx.executor().run_until_parked(); - cx.executor().start_waiting(); let fake_server = fake_servers.next().await.unwrap(); - cx.executor().finish_waiting(); - (file_path, editor, fake_server) } + // 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(); + 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 { - labels.push(excerpt_hints.hints_by_id[id].text()); + let hint = &excerpt_hints.hints_by_id[id]; + let mut label = hint.text(); + if hint.padding_left { + label.insert(0, ' '); + } + if hint.padding_right { + label.push_str(" "); + } + labels.push(label); } } - labels.sort(); labels } pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext) -> Vec { - let mut hints = editor + editor .visible_inlay_hints(cx) .into_iter() .map(|hint| hint.text.to_string()) - .collect::>(); - hints.sort(); - hints + .collect() } }