@@ -7957,7 +7957,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
);
assert_eq!(
inlay_cache.version, edits_made,
- "Host editor should track its own inlay cache history, which should be incremented after every cache/view change"
+ "Host editor update the cache version after every cache/view change",
);
});
let workspace_b = client_b.build_workspace(&project_b, cx_b);
@@ -7984,7 +7984,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
);
assert_eq!(
inlay_cache.version, edits_made,
- "Client editor should track its own inlay cache history, which should be incremented after every cache/view change"
+ "Guest editor update the cache version after every cache/view change"
);
});
@@ -8011,16 +8011,21 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
editor_b.update(cx_b, |editor, _| {
assert_eq!(
- vec!["0".to_string(), "1".to_string(), "2".to_string(), "3".to_string()],
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string()
+ ],
extract_hint_labels(editor),
"Guest should get hints the 1st edit and 2nd LSP query"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds, "Inlay kinds settings never change during the test");
assert_eq!(
- inlay_cache.version, edits_made,
- "Each editor should track its own inlay cache history, which should be incremented after every cache/view change"
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
);
+ assert_eq!(inlay_cache.version, edits_made);
});
editor_a.update(cx_a, |editor, cx| {
@@ -8033,17 +8038,23 @@ async fn test_mutual_editor_inlay_hint_cache_update(
cx_b.foreground().run_until_parked();
editor_a.update(cx_a, |editor, _| {
assert_eq!(
- vec!["0".to_string(), "1".to_string(), "2".to_string(), "3".to_string(), "4".to_string()],
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string()
+ ],
extract_hint_labels(editor),
"Host should get hints from 3rd edit, 5th LSP query: \
4th query was made by guest (but not applied) due to cache invalidation logic"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds, "Inlay kinds settings never change during the test");
assert_eq!(
- inlay_cache.version, edits_made,
- "Each editor should track its own inlay cache history, which should be incremented after every cache/view change"
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
);
+ assert_eq!(inlay_cache.version, edits_made);
});
editor_b.update(cx_b, |editor, _| {
assert_eq!(
@@ -8063,10 +8074,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Inlay kinds settings never change during the test"
);
- assert_eq!(
- inlay_cache.version, edits_made,
- "Guest should have a version increment"
- );
+ assert_eq!(inlay_cache.version, edits_made);
});
fake_language_server
@@ -134,7 +134,7 @@ struct ExcerptHintsUpdate {
cache_version: usize,
remove_from_visible: Vec<InlayId>,
remove_from_cache: HashSet<InlayId>,
- add_to_cache: Vec<InlayHint>,
+ add_to_cache: HashSet<InlayHint>,
}
impl InlayHintCache {
@@ -413,14 +413,13 @@ fn spawn_new_update_tasks(
let update_task = o.get_mut();
if update_task.is_running() {
match (update_task.invalidation_strategy(), invalidation_strategy) {
- (InvalidationStrategy::Forced, InvalidationStrategy::Forced)
+ (InvalidationStrategy::Forced, _)
| (_, InvalidationStrategy::OnConflict) => {
o.insert(UpdateTask::new(
invalidation_strategy,
new_update_task(None),
));
}
- (InvalidationStrategy::Forced, _) => {}
(_, InvalidationStrategy::Forced) => {
if cache_is_empty {
o.insert(UpdateTask::new(
@@ -660,8 +659,7 @@ fn calculate_hint_updates(
cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
visible_hints: &[Inlay],
) -> Option<ExcerptHintsUpdate> {
- let mut add_to_cache: Vec<InlayHint> = Vec::new();
-
+ let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
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) {
@@ -688,7 +686,7 @@ fn calculate_hint_updates(
None => true,
};
if missing_from_cache {
- add_to_cache.push(new_hint);
+ add_to_cache.insert(new_hint);
}
}
@@ -785,3 +783,331 @@ fn contains_position(
range.start.cmp(&position, buffer_snapshot).is_le()
&& range.end.cmp(&position, buffer_snapshot).is_ge()
}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::atomic::{AtomicU32, Ordering};
+
+ use crate::serde_json::json;
+ use futures::StreamExt;
+ use gpui::{TestAppContext, ViewHandle};
+ use language::{
+ language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
+ };
+ use lsp::FakeLanguageServer;
+ use project::{FakeFs, Project};
+ use settings::SettingsStore;
+ use workspace::Workspace;
+
+ use crate::{editor_tests::update_test_settings, EditorSettings};
+
+ use super::*;
+
+ #[gpui::test]
+ async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ let (file_with_hints, editor, fake_server) =
+ prepare_test_objects(cx, &allowed_hint_kinds).await;
+
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&lsp_request_count);
+ async move {
+ 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))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().finish_waiting();
+ cx.foreground().run_until_parked();
+ let mut edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should get its first hints when opening the editor"
+ );
+ assert_eq!(expected_layers, 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"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("some change", cx);
+ edits_made += 1;
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string(), "1".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should get new hints after an edit"
+ );
+ assert_eq!(expected_layers, 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"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ edits_made += 1;
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should get new hints after hint refresh/ request"
+ );
+ assert_eq!(expected_layers, 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"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+ }
+
+ async fn prepare_test_objects(
+ cx: &mut TestAppContext,
+ allowed_hint_kinds: &HashSet<Option<InlayHintKind>>,
+ ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
+ cx.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<EditorSettings>(cx, |settings| {
+ settings.inlay_hints = Some(crate::InlayHintsContent {
+ enabled: Some(true),
+ show_type_hints: Some(
+ allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ ),
+ show_parameter_hints: Some(
+ allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ ),
+ show_other_hints: Some(allowed_hint_kinds.contains(&None)),
+ })
+ });
+ });
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ cx.foreground().start_waiting();
+ let editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let fake_server = fake_servers.next().await.unwrap();
+
+ ("/a/main.rs", editor, fake_server)
+ }
+
+ #[gpui::test]
+ async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ let (file_with_hints, editor, fake_server) =
+ prepare_test_objects(cx, &allowed_hint_kinds).await;
+
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ Ok(Some(vec![
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 1),
+ label: lsp::InlayHintLabel::String("type hint".to_string()),
+ kind: Some(lsp::InlayHintKind::TYPE),
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 2),
+ label: lsp::InlayHintLabel::String("parameter hint".to_string()),
+ kind: Some(lsp::InlayHintKind::PARAMETER),
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 3),
+ label: lsp::InlayHintLabel::String("other hint".to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ ]))
+ })
+ .next()
+ .await;
+ cx.foreground().finish_waiting();
+ cx.foreground().run_until_parked();
+
+ let edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ vec![
+ "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!["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,
+ "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"
+ );
+ });
+
+ //
+ }
+
+ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
+ cx.foreground().forbid_parking();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ client::init_settings(cx);
+ language::init(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
+ crate::init(cx);
+ });
+
+ update_test_settings(cx, f);
+ }
+
+ fn cached_hint_labels(editor: &Editor) -> Vec<String> {
+ let mut labels = Vec::new();
+ for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+ let excerpt_hints = excerpt_hints.read();
+ for (_, inlay) in excerpt_hints.hints.iter() {
+ match &inlay.label {
+ project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+ _ => unreachable!(),
+ }
+ }
+ }
+ labels
+ }
+
+ fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
+ editor
+ .visible_inlay_hints(cx)
+ .into_iter()
+ .map(|hint| hint.text.to_string())
+ .collect()
+ }
+}