From a9c17e7407bdb0eeb7119bda4febf715a721e5d3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 13 Nov 2023 12:10:34 +0200 Subject: [PATCH] Uncomment all inlay hint cache code and tests --- crates/editor2/src/editor_tests.rs | 24 +- crates/editor2/src/inlay_hint_cache.rs | 5122 ++++++++++++------------ crates/workspace2/src/workspace2.rs | 38 +- 3 files changed, 2588 insertions(+), 2596 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 5b5a40ba8e..0ba0158045 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -1,3 +1,7 @@ +use gpui::TestAppContext; +use language::language_settings::{AllLanguageSettings, AllLanguageSettingsContent}; +use settings::SettingsStore; + // use super::*; // use crate::{ // scroll::scroll_amount::ScrollAmount, @@ -8152,16 +8156,16 @@ // }); // } -// pub(crate) fn update_test_language_settings( -// cx: &mut TestAppContext, -// f: impl Fn(&mut AllLanguageSettingsContent), -// ) { -// cx.update(|cx| { -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, f); -// }); -// }); -// } +pub(crate) fn update_test_language_settings( + cx: &mut TestAppContext, + f: impl Fn(&mut AllLanguageSettingsContent), +) { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + }); +} // pub(crate) fn update_test_project_settings( // cx: &mut TestAppContext, diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index addd3bf3ac..486cc32bbd 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -553,18 +553,17 @@ impl InlayHintCache { let mut resolved_hint = resolved_hint_task.await.context("hint resolve task")?; editor.update(&mut cx, |editor, _| { - todo!() - // 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) { - // if cached_hint.resolve_state == ResolveState::Resolving { - // resolved_hint.resolve_state = ResolveState::Resolved; - // *cached_hint = resolved_hint; - // } - // } - // } + 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) { + if cached_hint.resolve_state == ResolveState::Resolving { + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; + } + } + } })?; } @@ -585,91 +584,89 @@ fn spawn_new_update_tasks( update_cache_version: usize, cx: &mut ViewContext<'_, Editor>, ) { - todo!("old version below"); + let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); + 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; + } + + let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); + if let Some(cached_excerpt_hints) = &cached_excerpt_hints { + 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 (multi_buffer_snapshot, Some(query_ranges)) = + editor.buffer.update(cx, |multi_buffer, cx| { + ( + multi_buffer.snapshot(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, + }; + + let new_update_task = |query_ranges| { + new_update_task( + query, + query_ranges, + multi_buffer_snapshot, + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints, + Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), + 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 visible_hints = Arc::new(editor.visible_inlay_hints(cx)); -// 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; -// } - -// let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); -// if let Some(cached_excerpt_hints) = &cached_excerpt_hints { -// 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 (multi_buffer_snapshot, Some(query_ranges)) = -// editor.buffer.update(cx, |multi_buffer, cx| { -// ( -// multi_buffer.snapshot(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, -// }; - -// let new_update_task = |query_ranges| { -// new_update_task( -// query, -// query_ranges, -// multi_buffer_snapshot, -// buffer_snapshot.clone(), -// Arc::clone(&visible_hints), -// cached_excerpt_hints, -// Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), -// 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), -// )); -// } -// } -// } -// } #[derive(Debug, Clone)] struct QueryRanges { @@ -765,209 +762,208 @@ fn new_update_task( lsp_request_limiter: Arc, cx: &mut ViewContext<'_, Editor>, ) -> Task<()> { - todo!() - // cx.spawn(|editor, mut cx| async move { - // let closure_cx = cx.clone(); - // let fetch_and_update_hints = |invalidate, range| { - // fetch_and_update_hints( - // editor.clone(), - // multi_buffer_snapshot.clone(), - // buffer_snapshot.clone(), - // Arc::clone(&visible_hints), - // cached_excerpt_hints.as_ref().map(Arc::clone), - // query, - // invalidate, - // range, - // Arc::clone(&lsp_request_limiter), - // closure_cx.clone(), - // ) - // }; - // let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( - // |visible_range| async move { - // ( - // visible_range.clone(), - // fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) - // .await, - // ) - // }, - // )) - // .await; + cx.spawn(|editor, mut cx| async move { + let closure_cx = cx.clone(); + let fetch_and_update_hints = |invalidate, range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + invalidate, + range, + Arc::clone(&lsp_request_limiter), + closure_cx.clone(), + ) + }; + let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( + |visible_range| async move { + ( + visible_range.clone(), + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + .await, + ) + }, + )) + .await; - // let hint_delay = cx.background().timer(Duration::from_millis( - // INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, - // )); + let hint_delay = cx.background_executor().timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )); - // let mut query_range_failed = |range: &Range, e: anyhow::Error| { - // log::error!("inlay hint update task for range {range:?} failed: {e:#}"); - // editor - // .update(&mut cx, |editor, _| { - // if let Some(task_ranges) = editor - // .inlay_hint_cache - // .update_tasks - // .get_mut(&query.excerpt_id) - // { - // task_ranges.invalidate_range(&buffer_snapshot, &range); - // } - // }) - // .ok() - // }; + let mut query_range_failed = |range: &Range, e: anyhow::Error| { + log::error!("inlay hint update task for range {range:?} failed: {e:#}"); + editor + .update(&mut cx, |editor, _| { + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &range); + } + }) + .ok() + }; - // for (range, result) in visible_range_update_results { - // if let Err(e) = result { - // query_range_failed(&range, e); - // } - // } + for (range, result) in visible_range_update_results { + if let Err(e) = result { + query_range_failed(&range, e); + } + } - // hint_delay.await; - // let invisible_range_update_results = future::join_all( - // query_ranges - // .before_visible - // .into_iter() - // .chain(query_ranges.after_visible.into_iter()) - // .map(|invisible_range| async move { - // ( - // invisible_range.clone(), - // fetch_and_update_hints(false, invisible_range).await, - // ) - // }), - // ) - // .await; - // for (range, result) in invisible_range_update_results { - // if let Err(e) = result { - // query_range_failed(&range, e); - // } - // } - // }) + hint_delay.await; + let invisible_range_update_results = future::join_all( + query_ranges + .before_visible + .into_iter() + .chain(query_ranges.after_visible.into_iter()) + .map(|invisible_range| async move { + ( + invisible_range.clone(), + fetch_and_update_hints(false, invisible_range).await, + ) + }), + ) + .await; + for (range, result) in invisible_range_update_results { + if let Err(e) = result { + query_range_failed(&range, e); + } + } + }) } -// async fn fetch_and_update_hints( -// editor: gpui::WeakView, -// multi_buffer_snapshot: MultiBufferSnapshot, -// buffer_snapshot: BufferSnapshot, -// visible_hints: Arc>, -// cached_excerpt_hints: Option>>, -// query: ExcerptQuery, -// invalidate: bool, -// fetch_range: Range, -// lsp_request_limiter: Arc, -// mut cx: gpui::AsyncAppContext, -// ) -> anyhow::Result<()> { -// let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { -// (None, false) -// } else { -// match lsp_request_limiter.try_acquire() { -// Some(guard) => (Some(guard), false), -// None => (Some(lsp_request_limiter.acquire().await), true), -// } -// }; -// let fetch_range_to_log = -// fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); -// let inlay_hints_fetch_task = editor -// .update(&mut cx, |editor, cx| { -// if got_throttled { -// let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { -// Some((_, _, current_visible_range)) => { -// let visible_offset_length = current_visible_range.len(); -// let double_visible_range = current_visible_range -// .start -// .saturating_sub(visible_offset_length) -// ..current_visible_range -// .end -// .saturating_add(visible_offset_length) -// .min(buffer_snapshot.len()); -// !double_visible_range -// .contains(&fetch_range.start.to_offset(&buffer_snapshot)) -// && !double_visible_range -// .contains(&fetch_range.end.to_offset(&buffer_snapshot)) -// }, -// None => true, -// }; -// if query_not_around_visible_range { -// log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); -// if let Some(task_ranges) = editor -// .inlay_hint_cache -// .update_tasks -// .get_mut(&query.excerpt_id) -// { -// task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); -// } -// return None; -// } -// } -// editor -// .buffer() -// .read(cx) -// .buffer(query.buffer_id) -// .and_then(|buffer| { -// let project = editor.project.as_ref()?; -// Some(project.update(cx, |project, cx| { -// project.inlay_hints(buffer, fetch_range.clone(), cx) -// })) -// }) -// }) -// .ok() -// .flatten(); -// 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(()), -// }; -// drop(lsp_request_guard); -// log::debug!( -// "Fetched {} hints for range {fetch_range_to_log:?}", -// new_hints.len() -// ); -// log::trace!("Fetched hints: {new_hints:?}"); +async fn fetch_and_update_hints( + editor: gpui::WeakView, + multi_buffer_snapshot: MultiBufferSnapshot, + buffer_snapshot: BufferSnapshot, + visible_hints: Arc>, + cached_excerpt_hints: Option>>, + query: ExcerptQuery, + invalidate: bool, + fetch_range: Range, + lsp_request_limiter: Arc, + mut cx: gpui::AsyncWindowContext, +) -> anyhow::Result<()> { + let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { + (None, false) + } else { + match lsp_request_limiter.try_acquire() { + Some(guard) => (Some(guard), false), + None => (Some(lsp_request_limiter.acquire().await), true), + } + }; + let fetch_range_to_log = + fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); + let inlay_hints_fetch_task = editor + .update(&mut cx, |editor, cx| { + if got_throttled { + let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + Some((_, _, current_visible_range)) => { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + }, + None => true, + }; + if query_not_around_visible_range { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); + } + return None; + } + } + editor + .buffer() + .read(cx) + .buffer(query.buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.inlay_hints(buffer, fetch_range.clone(), cx) + })) + }) + }) + .ok() + .flatten(); + 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(()), + }; + drop(lsp_request_guard); + log::debug!( + "Fetched {} hints for range {fetch_range_to_log:?}", + new_hints.len() + ); + log::trace!("Fetched hints: {new_hints:?}"); -// let background_task_buffer_snapshot = buffer_snapshot.clone(); -// let backround_fetch_range = fetch_range.clone(); -// let new_update = cx -// .background() -// .spawn(async move { -// calculate_hint_updates( -// query.excerpt_id, -// invalidate, -// backround_fetch_range, -// new_hints, -// &background_task_buffer_snapshot, -// cached_excerpt_hints, -// &visible_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::trace!("New update: {new_update:?}"); -// editor -// .update(&mut cx, |editor, cx| { -// apply_hint_update( -// editor, -// new_update, -// query, -// invalidate, -// buffer_snapshot, -// multi_buffer_snapshot, -// cx, -// ); -// }) -// .ok(); -// } -// Ok(()) -// } + let background_task_buffer_snapshot = buffer_snapshot.clone(); + let backround_fetch_range = fetch_range.clone(); + let new_update = cx + .background_executor() + .spawn(async move { + calculate_hint_updates( + query.excerpt_id, + invalidate, + backround_fetch_range, + new_hints, + &background_task_buffer_snapshot, + cached_excerpt_hints, + &visible_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::trace!("New update: {new_update:?}"); + editor + .update(&mut cx, |editor, cx| { + apply_hint_update( + editor, + new_update, + query, + invalidate, + buffer_snapshot, + multi_buffer_snapshot, + cx, + ); + }) + .ok(); + } + Ok(()) +} fn calculate_hint_updates( excerpt_id: ExcerptId, @@ -1077,2279 +1073,2271 @@ fn apply_hint_update( multi_buffer_snapshot: MultiBufferSnapshot, cx: &mut ViewContext<'_, Editor>, ) { - todo!("old implementation commented below") + 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; + } + } + + 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 { + to_remove: new_update.remove_from_visible, + to_insert: Vec::new(), + }; + 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) => { + 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 + } + Err(i) => Some(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_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); + 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()); + } + } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); + 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_inlay_hints(to_remove, to_insert, cx) + } +} + +#[cfg(test)] +pub mod tests { + use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; + + use crate::{ + scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, + ExcerptRange, + }; + use futures::StreamExt; + use gpui::{Context, TestAppContext, View}; + use itertools::Itertools; + use language::{ + language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, + }; + use lsp::FakeLanguageServer; + use parking_lot::Mutex; + use project::{FakeFs, Project}; + use serde_json::json; + use settings::SettingsStore; + use text::{Point, ToPoint}; + use workspace::Workspace; + + use crate::editor_tests::update_test_language_settings; + + use super::*; + + #[gpui::test] + async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + show_other_hints: allowed_hint_kinds.contains(&None), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + 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 { + 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.executor().run_until_parked(); + + let mut edits_made = 1; + 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)); + 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.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string(), "1".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" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + fake_server + .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()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get new hints after hint refresh/ request" + ); + 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" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + } + + #[gpui::test] + async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + 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 { + 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); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, current_call_id), + label: lsp::InlayHintLabel::String(current_call_id.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + let mut edits_made = 1; + 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, + edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + let progress_token = "test_progress_token"; + fake_server + .request::(lsp::WorkDoneProgressCreateParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + }) + .await + .expect("work done progress create request failed"); + cx.executor().run_until_parked(); + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( + lsp::WorkDoneProgressBegin::default(), + )), + }); + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "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" + ); + }); + + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( + lsp::WorkDoneProgressEnd::default(), + )), + }); + cx.executor().run_until_parked(); + + edits_made += 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "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 udpate once after the work task is done" + ); + }); + } + + #[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, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + 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 workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let mut rs_fake_servers = None; + let mut md_fake_servers = None; + for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { + let mut language = Language::new( + LanguageConfig { + name: name.into(), + path_suffixes: vec![path_suffix.to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name, + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + match name { + "Rust" => rs_fake_servers = Some(fake_servers), + "Markdown" => md_fake_servers = Some(fake_servers), + _ => unreachable!(), + } + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + } + + let _rs_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); + let rs_editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .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" + ); + }); + + cx.executor().run_until_parked(); + let _md_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/other.md", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); + let md_editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "other.md"), None, true, cx) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .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 verison, repeating Rust editor rules" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + + rs_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some rs change", cx); + }); + 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" + ); + }); + 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); + }); + + md_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some md change", cx); + }); + 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); + }); + 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); + }); + } + + #[gpui::test] + async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + show_other_hints: allowed_hint_kinds.contains(&None), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + 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.executor().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 1, + "Should query new hints once" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type 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()], + 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::(()) + .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 load new hints twice" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type 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()], + 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" + ); + }); + + for (new_allowed_hint_kinds, expected_visible_hints) in [ + (HashSet::from_iter([None]), vec!["other hint".to_string()]), + ( + HashSet::from_iter([Some(InlayHintKind::Type)]), + vec!["type hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Type)]), + vec!["other hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), + vec!["other hint".to_string(), "parameter hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([ + None, + Some(InlayHintKind::Type), + Some(InlayHintKind::Parameter), + ]), + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + ), + ] { + edits_made += 1; + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: new_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: new_allowed_hint_kinds.contains(&None), + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + expected_visible_hints, + 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, + "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" + ); + }); + } + + 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 { + enabled: false, + show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: another_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: another_allowed_hint_kinds.contains(&None), + }) + }); + 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 hints got disabled" + ); + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear the cache when hints got disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "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, + "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" + ); + }); + + fake_server + .request::(()) + .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" + ); + }); + + 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, + show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: final_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: final_allowed_hint_kinds.contains(&None), + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 3, + "Should query for new hints when they got reenabled" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints fully repopulated after the hints got reenabled" + ); + assert_eq!( + vec!["parameter hint".to_string()], + 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, + "Cache should update editor settings when hints got reenabled" + ); + assert_eq!( + inlay_cache.version, edits_made, + "Cache should update its version after hints got reenabled" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 4, + "Should query for new hints again" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + ); + assert_eq!( + vec!["parameter hint".to_string()], + visible_hint_labels(editor, cx), + ); + assert_eq!(editor.inlay_hint_cache().version, edits_made); + }); + } + + #[gpui::test] + async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let fake_server = Arc::new(fake_server); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + 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, 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; + + let mut expected_changes = Vec::new(); + for change_after_opening in [ + "initial change #1", + "initial change #2", + "initial change #3", + ] { + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(change_after_opening, cx); + }); + expected_changes.push(change_after_opening); + } + + 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" + ); + } + 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)); + assert_eq!( + editor.inlay_hint_cache().version, 1, + "Only one update should be registered in the cache after all cancellations" + ); + }); + + let mut edits = Vec::new(); + for async_later_change in [ + "another change #1", + "another change #2", + "another change #3", + ] { + expected_changes.push(async_later_change); + let task_editor = editor.clone(); + edits.push(cx.spawn(|mut cx| async move { + task_editor.update(&mut cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(async_later_change, cx); + }); + })); + } + let _ = future::join_all(edits).await; + 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" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::SeqCst), + 3, + "Should query new hints one more time, for the last edit only" + ); + let expected_hints = vec!["3".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)); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Should update the cache version once more, for the new change" + ); + }); + } + + #[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, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + 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_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; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); + let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .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, + }])) + } + }) + .next() + .await; + + fn editor_visible_range( + editor: &View, + cx: &mut gpui::TestAppContext, + ) -> Range { + let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); + 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 + }) + } + + // 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"); + + 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" + ); + }); + + editor.update(cx, |editor, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + }); + 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()); + 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 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 + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }); + 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); + }); + + editor.update(cx, |editor, cx| { + editor.handle_input("++++more text++++", cx); + }); + 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"); + + 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"); + }); + } + + #[gpui::test(iterations = 10)] + async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut 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 language = Arc::new(language); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 0)..Point::new(11, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 0)..Point::new(33, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 0)..Point::new(55, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 0)..Point::new(66, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 0)..Point::new(77, 0), + primary: None, + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 1)..Point::new(11, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 1)..Point::new(33, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 1)..Point::new(55, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 1)..Point::new(66, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 1)..Point::new(77, 1), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + // one hint per excerpt + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + 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(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) + }); + }); + 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 #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); + assert_eq!(expected_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"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) + }); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let last_scroll_update_version = 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 #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() + }).unwrap(); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + }); + 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 #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); + }); + + editor_edited.store(true, Ordering::Release); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + }); + editor.handle_input("++++more text++++", cx); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, 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, + cached_hint_labels(editor), + "After multibuffer edit, editor gets scolled back to the last selection; \ +all hints should be invalidated and requeried for all of its visible excerpts" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + + let current_cache_version = editor.inlay_hint_cache().version; + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); + assert!( + current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, + "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" + ); + }); + } + + #[gpui::test] + async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + + 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 language = Arc::new(language); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { + let buffer_1_excerpts = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + let buffer_2_excerpts = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }], + cx, + ); + (buffer_1_excerpts, buffer_2_excerpts) + }); + + assert!(!buffer_1_excerpts.is_empty()); + assert!(!buffer_2_excerpts.is_empty()); + + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string(), "other hint #0".to_string()], + cached_hint_labels(editor), + "Cache should update for both excerpts despite hints display was disabled" + ); + assert!( + 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" + ); + }); + + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(buffer_2_excerpts, cx) + }) + }); + cx.executor().run_until_parked(); + 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" + ); + assert!( + 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" + ); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + 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), + "Hint display settings change should not change the cache" + ); + assert_eq!( + expected_hints, + 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" + ); + }); + } + + #[gpui::test] + async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut 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_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), + "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.clone(), cx)); + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + 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 query_start = params.range.start; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; + Ok(Some(vec![lsp::InlayHint { + position: query_start, + 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(); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + } + + #[gpui::test] + async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().start_waiting(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 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, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "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" + ); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear hints after 2nd toggle" + ); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "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); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "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); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["3".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" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 5); + }); + } + + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + 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_language_settings(cx, f); + } + + async fn prepare_test_objects( + cx: &mut TestAppContext, + ) -> (&'static str, View, FakeLanguageServer) { + 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_executor.clone()); + 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.clone(), cx)); + + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + editor.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); + }); + + ("/a/main.rs", editor, fake_server) + } + + pub fn cached_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + for id in &excerpt_hints.ordered_hints { + labels.push(excerpt_hints.hints_by_id[id].text()); + } + } + + labels.sort(); + labels + } + + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, Editor>) -> Vec { + let mut hints = editor + .visible_inlay_hints(cx) + .into_iter() + .map(|hint| hint.text.to_string()) + .collect::>(); + hints.sort(); + hints + } } -// 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; -// } -// } - -// 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 { -// to_remove: new_update.remove_from_visible, -// to_insert: Vec::new(), -// }; -// 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) => { -// 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 -// } -// Err(i) => Some(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_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); -// 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()); -// } -// } -// cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); -// 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_inlay_hints(to_remove, to_insert, cx) -// } -// } - -// #[cfg(test)] -// pub mod tests { -// use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; - -// use crate::{ -// scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, -// serde_json::json, -// ExcerptRange, -// }; -// use futures::StreamExt; -// use gpui::{executor::Deterministic, TestAppContext, View}; -// use itertools::Itertools; -// use language::{ -// language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, -// }; -// use lsp::FakeLanguageServer; -// use parking_lot::Mutex; -// use project::{FakeFs, Project}; -// use settings::SettingsStore; -// use text::{Point, ToPoint}; -// use workspace::Workspace; - -// use crate::editor_tests::update_test_language_settings; - -// use super::*; - -// #[gpui::test] -// async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { -// let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: allowed_hint_kinds.contains(&None), -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// 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 { -// 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().run_until_parked(); - -// let mut edits_made = 1; -// 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)); -// 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_hints = vec!["0".to_string(), "1".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" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// edits_made += 1; -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get new hints after hint refresh/ request" -// ); -// 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" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// 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 { -// 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); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, current_call_id), -// label: lsp::InlayHintLabel::String(current_call_id.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// let mut edits_made = 1; -// 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, -// edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// let progress_token = "test_progress_token"; -// fake_server -// .request::(lsp::WorkDoneProgressCreateParams { -// token: lsp::ProgressToken::String(progress_token.to_string()), -// }) -// .await -// .expect("work done progress create request failed"); -// cx.foreground().run_until_parked(); -// fake_server.notify::(lsp::ProgressParams { -// token: lsp::ProgressToken::String(progress_token.to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( -// lsp::WorkDoneProgressBegin::default(), -// )), -// }); -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "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" -// ); -// }); - -// fake_server.notify::(lsp::ProgressParams { -// token: lsp::ProgressToken::String(progress_token.to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( -// lsp::WorkDoneProgressEnd::default(), -// )), -// }); -// cx.foreground().run_until_parked(); - -// edits_made += 1; -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "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 udpate once after the work task is done" -// ); -// }); -// } - -// #[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, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// 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.md": "Test md file with some text", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let mut rs_fake_servers = None; -// let mut md_fake_servers = None; -// for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { -// let mut language = Language::new( -// LanguageConfig { -// name: name.into(), -// path_suffixes: vec![path_suffix.to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name, -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// match name { -// "Rust" => rs_fake_servers = Some(fake_servers), -// "Markdown" => md_fake_servers = Some(fake_servers), -// _ => unreachable!(), -// } -// project.update(cx, |project, _| { -// project.languages().add(Arc::new(language)); -// }); -// } - -// let _rs_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); -// let rs_editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .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.foreground().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" -// ); -// }); - -// cx.foreground().run_until_parked(); -// let _md_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/other.md", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); -// let md_editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "other.md"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .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.foreground().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 verison, repeating Rust editor rules" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 1); -// }); - -// rs_editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("some rs change", cx); -// }); -// cx.foreground().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" -// ); -// }); -// 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); -// }); - -// md_editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("some md change", cx); -// }); -// cx.foreground().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); -// }); -// 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); -// }); -// } - -// #[gpui::test] -// async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { -// let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: allowed_hint_kinds.contains(&None), -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let another_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&another_lsp_request_count); -// async move { -// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// 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().run_until_parked(); - -// let mut edits_made = 1; -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 1, -// "Should query new hints once" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type 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()], -// 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::(()) -// .await -// .expect("inlay refresh request failed"); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should load new hints twice" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type 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()], -// 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" -// ); -// }); - -// for (new_allowed_hint_kinds, expected_visible_hints) in [ -// (HashSet::from_iter([None]), vec!["other hint".to_string()]), -// ( -// HashSet::from_iter([Some(InlayHintKind::Type)]), -// vec!["type hint".to_string()], -// ), -// ( -// HashSet::from_iter([Some(InlayHintKind::Parameter)]), -// vec!["parameter hint".to_string()], -// ), -// ( -// HashSet::from_iter([None, Some(InlayHintKind::Type)]), -// vec!["other hint".to_string(), "type hint".to_string()], -// ), -// ( -// HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), -// vec!["other hint".to_string(), "parameter hint".to_string()], -// ), -// ( -// HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), -// vec!["parameter hint".to_string(), "type hint".to_string()], -// ), -// ( -// HashSet::from_iter([ -// None, -// Some(InlayHintKind::Type), -// Some(InlayHintKind::Parameter), -// ]), -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// ), -// ] { -// edits_made += 1; -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: new_allowed_hint_kinds -// .contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: new_allowed_hint_kinds.contains(&None), -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" -// ); -// assert_eq!( -// expected_visible_hints, -// 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, -// "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" -// ); -// }); -// } - -// 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 { -// enabled: false, -// show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: another_allowed_hint_kinds -// .contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: another_allowed_hint_kinds.contains(&None), -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 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!( -// visible_hint_labels(editor, cx).is_empty(), -// "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, -// "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" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// cx.foreground().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" -// ); -// }); - -// 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, -// show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: final_allowed_hint_kinds -// .contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: final_allowed_hint_kinds.contains(&None), -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 3, -// "Should query for new hints when they got reenabled" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Should get its cached hints fully repopulated after the hints got reenabled" -// ); -// assert_eq!( -// vec!["parameter hint".to_string()], -// 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, -// "Cache should update editor settings when hints got reenabled" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "Cache should update its version after hints got reenabled" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 4, -// "Should query for new hints again" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// ); -// assert_eq!( -// vec!["parameter hint".to_string()], -// visible_hint_labels(editor, cx), -// ); -// assert_eq!(editor.inlay_hint_cache().version, edits_made); -// }); -// } - -// #[gpui::test] -// async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let fake_server = Arc::new(fake_server); -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let another_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&another_lsp_request_count); -// async move { -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; -// 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, 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; - -// let mut expected_changes = Vec::new(); -// for change_after_opening in [ -// "initial change #1", -// "initial change #2", -// "initial change #3", -// ] { -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(change_after_opening, cx); -// }); -// expected_changes.push(change_after_opening); -// } - -// cx.foreground().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" -// ); -// } -// 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)); -// assert_eq!( -// editor.inlay_hint_cache().version, 1, -// "Only one update should be registered in the cache after all cancellations" -// ); -// }); - -// let mut edits = Vec::new(); -// for async_later_change in [ -// "another change #1", -// "another change #2", -// "another change #3", -// ] { -// expected_changes.push(async_later_change); -// let task_editor = editor.clone(); -// let mut task_cx = cx.clone(); -// edits.push(cx.foreground().spawn(async move { -// task_editor.update(&mut task_cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(async_later_change, cx); -// }); -// })); -// } -// let _ = future::join_all(edits).await; -// cx.foreground().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" -// ); -// } -// assert_eq!( -// lsp_request_count.load(Ordering::SeqCst), -// 3, -// "Should query new hints one more time, for the last edit only" -// ); -// let expected_hints = vec!["3".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)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 2, -// "Should update the cache version once more, for the new change" -// ); -// }); -// } - -// #[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, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// 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": 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; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); -// let lsp_request_count = Arc::new(AtomicUsize::new(0)); -// let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); -// let closure_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .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, -// }])) -// } -// }) -// .next() -// .await; -// fn editor_visible_range( -// editor: &ViewHandle, -// cx: &mut gpui::TestAppContext, -// ) -> Range { -// let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); -// 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 -// }) -// } - -// // in large buffers, requests are made for more than visible range of a buffer. -// // invisible parts are queried later, to avoid excessive requests on quick typing. -// // wait the timeout needed to get all requests. -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// let initial_visible_range = editor_visible_range(&editor, cx); -// let lsp_initial_visible_range = lsp::Range::new( -// lsp::Position::new( -// initial_visible_range.start.row, -// initial_visible_range.start.column, -// ), -// lsp::Position::new( -// initial_visible_range.end.row, -// initial_visible_range.end.column, -// ), -// ); -// let expected_initial_query_range_end = -// lsp::Position::new(initial_visible_range.end.row * 2, 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"); - -// 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" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.scroll_screen(&ScrollAmount::Page(1.0), cx); -// editor.scroll_screen(&ScrollAmount::Page(1.0), cx); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// let visible_range_after_scrolls = editor_visible_range(&editor, cx); -// let visible_line_count = -// editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); -// 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 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 -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::center()), cx, |s| { -// s.select_ranges([selection_in_cached_range..selection_in_cached_range]) -// }); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// editor.update(cx, |_, _| { -// let ranges = lsp_request_ranges -// .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); -// }); - -// editor.update(cx, |editor, cx| { -// editor.handle_input("++++more text++++", cx); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let 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"); - -// 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"); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_multiple_excerpts_large_multibuffer( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut 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 language = Arc::new(language); -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), -// "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages().add(Arc::clone(&language)) -// }); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let buffer_1 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "main.rs"), cx) -// }) -// .await -// .unwrap(); -// let buffer_2 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "other.rs"), cx) -// }) -// .await -// .unwrap(); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// buffer_1.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(4, 0)..Point::new(11, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(22, 0)..Point::new(33, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(44, 0)..Point::new(55, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(56, 0)..Point::new(66, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(67, 0)..Point::new(77, 0), -// primary: None, -// }, -// ], -// cx, -// ); -// multibuffer.push_excerpts( -// buffer_2.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 1)..Point::new(2, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(4, 1)..Point::new(11, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(22, 1)..Point::new(33, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(44, 1)..Point::new(55, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(56, 1)..Point::new(66, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(67, 1)..Point::new(77, 1), -// primary: None, -// }, -// ], -// cx, -// ); -// multibuffer -// }); - -// deterministic.run_until_parked(); -// cx.foreground().run_until_parked(); -// let editor = cx -// .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) -// .root(cx); -// let editor_edited = Arc::new(AtomicBool::new(false)); -// let fake_server = fake_servers.next().await.unwrap(); -// let closure_editor_edited = Arc::clone(&editor_edited); -// fake_server -// .handle_request::(move |params, _| { -// let task_editor_edited = Arc::clone(&closure_editor_edited); -// async move { -// let hint_text = if params.text_document.uri -// == lsp::Url::from_file_path("/a/main.rs").unwrap() -// { -// "main hint" -// } else if params.text_document.uri -// == lsp::Url::from_file_path("/a/other.rs").unwrap() -// { -// "other hint" -// } else { -// panic!("unexpected uri: {:?}", params.text_document.uri); -// }; - -// // one hint per excerpt -// let positions = [ -// lsp::Position::new(0, 2), -// lsp::Position::new(4, 2), -// lsp::Position::new(22, 2), -// lsp::Position::new(44, 2), -// lsp::Position::new(56, 2), -// lsp::Position::new(67, 2), -// ]; -// let out_of_range_hint = lsp::InlayHint { -// position: lsp::Position::new( -// params.range.start.line + 99, -// params.range.start.character + 99, -// ), -// label: lsp::InlayHintLabel::String( -// "out of excerpt range, should be ignored".to_string(), -// ), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }; - -// let edited = task_editor_edited.load(Ordering::Acquire); -// Ok(Some( -// std::iter::once(out_of_range_hint) -// .chain(positions.into_iter().enumerate().map(|(i, position)| { -// lsp::InlayHint { -// position, -// label: lsp::InlayHintLabel::String(format!( -// "{hint_text}{} #{i}", -// if edited { "(edited)" } else { "" }, -// )), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// } -// })) -// .collect(), -// )) -// } -// }) -// .next() -// .await; -// cx.foreground().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(), -// ]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) -// }); -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) -// }); -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) -// }); -// }); -// cx.foreground().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 #0".to_string(), -// "other hint #1".to_string(), -// "other hint #2".to_string(), -// ]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); -// assert_eq!(expected_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"); -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) -// }); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// let last_scroll_update_version = editor.update(cx, |editor, cx| { -// let expected_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 #0".to_string(), -// "other hint #1".to_string(), -// "other hint #2".to_string(), -// "other hint #3".to_string(), -// "other hint #4".to_string(), -// "other hint #5".to_string(), -// ]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); -// expected_hints.len() -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) -// }); -// }); -// cx.foreground().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 #0".to_string(), -// "other hint #1".to_string(), -// "other hint #2".to_string(), -// "other hint #3".to_string(), -// "other hint #4".to_string(), -// "other hint #5".to_string(), -// ]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); -// }); - -// editor_edited.store(true, Ordering::Release); -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) -// }); -// editor.handle_input("++++more text++++", cx); -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, 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, -// cached_hint_labels(editor), -// "After multibuffer edit, editor gets scolled back to the last selection; \ -// all hints should be invalidated and requeried for all of its visible excerpts" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - -// let current_cache_version = editor.inlay_hint_cache().version; -// let minimum_expected_version = last_scroll_update_version + expected_hints.len(); -// assert!( -// current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, -// "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_excerpts_removed( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: false, -// show_parameter_hints: false, -// show_other_hints: false, -// }) -// }); - -// 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 language = Arc::new(language); -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), -// "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages().add(Arc::clone(&language)) -// }); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let buffer_1 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "main.rs"), cx) -// }) -// .await -// .unwrap(); -// let buffer_2 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "other.rs"), cx) -// }) -// .await -// .unwrap(); -// let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); -// let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { -// let buffer_1_excerpts = multibuffer.push_excerpts( -// buffer_1.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }], -// cx, -// ); -// let buffer_2_excerpts = multibuffer.push_excerpts( -// buffer_2.clone(), -// [ExcerptRange { -// context: Point::new(0, 1)..Point::new(2, 1), -// primary: None, -// }], -// cx, -// ); -// (buffer_1_excerpts, buffer_2_excerpts) -// }); - -// assert!(!buffer_1_excerpts.is_empty()); -// assert!(!buffer_2_excerpts.is_empty()); - -// deterministic.run_until_parked(); -// cx.foreground().run_until_parked(); -// let editor = cx -// .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) -// .root(cx); -// let editor_edited = Arc::new(AtomicBool::new(false)); -// let fake_server = fake_servers.next().await.unwrap(); -// let closure_editor_edited = Arc::clone(&editor_edited); -// fake_server -// .handle_request::(move |params, _| { -// let task_editor_edited = Arc::clone(&closure_editor_edited); -// async move { -// let hint_text = if params.text_document.uri -// == lsp::Url::from_file_path("/a/main.rs").unwrap() -// { -// "main hint" -// } else if params.text_document.uri -// == lsp::Url::from_file_path("/a/other.rs").unwrap() -// { -// "other hint" -// } else { -// panic!("unexpected uri: {:?}", params.text_document.uri); -// }; - -// let positions = [ -// lsp::Position::new(0, 2), -// lsp::Position::new(4, 2), -// lsp::Position::new(22, 2), -// lsp::Position::new(44, 2), -// lsp::Position::new(56, 2), -// lsp::Position::new(67, 2), -// ]; -// let out_of_range_hint = lsp::InlayHint { -// position: lsp::Position::new( -// params.range.start.line + 99, -// params.range.start.character + 99, -// ), -// label: lsp::InlayHintLabel::String( -// "out of excerpt range, should be ignored".to_string(), -// ), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }; - -// let edited = task_editor_edited.load(Ordering::Acquire); -// Ok(Some( -// std::iter::once(out_of_range_hint) -// .chain(positions.into_iter().enumerate().map(|(i, position)| { -// lsp::InlayHint { -// position, -// label: lsp::InlayHintLabel::String(format!( -// "{hint_text}{} #{i}", -// if edited { "(edited)" } else { "" }, -// )), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// } -// })) -// .collect(), -// )) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// assert_eq!( -// vec!["main hint #0".to_string(), "other hint #0".to_string()], -// cached_hint_labels(editor), -// "Cache should update for both excerpts despite hints display was disabled" -// ); -// assert!( -// 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" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.buffer().update(cx, |multibuffer, cx| { -// multibuffer.remove_excerpts(buffer_2_excerpts, cx) -// }) -// }); -// cx.foreground().run_until_parked(); -// 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" -// ); -// assert!( -// 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" -// ); -// }); - -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// cx.foreground().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), -// "Hint display settings change should not change the cache" -// ); -// assert_eq!( -// expected_hints, -// 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" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut 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": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), -// "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.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let closure_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// 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 query_start = params.range.start; -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; -// Ok(Some(vec![lsp::InlayHint { -// position: query_start, -// 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.foreground().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)]) -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!(expected_hints, cached_hint_labels(editor)); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 1); -// }); -// } - -// #[gpui::test] -// async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: false, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().start_waiting(); -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let closure_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); - -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 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, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "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" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert!( -// cached_hint_labels(editor).is_empty(), -// "Should clear hints after 2nd toggle" -// ); -// assert!(visible_hint_labels(editor, cx).is_empty()); -// assert_eq!(editor.inlay_hint_cache().version, 2); -// }); - -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "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); -// }); - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert!( -// cached_hint_labels(editor).is_empty(), -// "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); -// }); - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["3".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" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 5); -// }); -// } - -// pub(crate) fn init_test(cx: &mut TestAppContext, f: impl 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_language_settings(cx, f); -// } - -// async fn prepare_test_objects( -// cx: &mut TestAppContext, -// ) -> (&'static str, ViewHandle, FakeLanguageServer) { -// 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.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// editor.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); -// }); - -// ("/a/main.rs", editor, fake_server) -// } - -// pub fn cached_hint_labels(editor: &Editor) -> Vec { -// let mut labels = Vec::new(); -// for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { -// let excerpt_hints = excerpt_hints.read(); -// for id in &excerpt_hints.ordered_hints { -// labels.push(excerpt_hints.hints_by_id[id].text()); -// } -// } - -// labels.sort(); -// labels -// } - -// pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { -// let mut hints = editor -// .visible_inlay_hints(cx) -// .into_iter() -// .map(|hint| hint.text.to_string()) -// .collect::>(); -// hints.sort(); -// hints -// } -// } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5c678df317..e4fc5d35c6 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3448,27 +3448,27 @@ impl Workspace { }) } - // todo!() - // #[cfg(any(test, feature = "test-support"))] - // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { - // use node_runtime::FakeNodeRuntime; + #[cfg(any(test, feature = "test-support"))] + pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { + use gpui::Context; + use node_runtime::FakeNodeRuntime; - // let client = project.read(cx).client(); - // let user_store = project.read(cx).user_store(); + let client = project.read(cx).client(); + let user_store = project.read(cx).user_store(); - // let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); - // let app_state = Arc::new(AppState { - // languages: project.read(cx).languages().clone(), - // workspace_store, - // client, - // user_store, - // fs: project.read(cx).fs().clone(), - // build_window_options: |_, _, _| Default::default(), - // initialize_workspace: |_, _, _, _| Task::ready(Ok(())), - // node_runtime: FakeNodeRuntime::new(), - // }); - // Self::new(0, project, app_state, cx) - // } + let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); + let app_state = Arc::new(AppState { + languages: project.read(cx).languages().clone(), + workspace_store, + client, + user_store, + fs: project.read(cx).fs().clone(), + build_window_options: |_, _, _| Default::default(), + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), + node_runtime: FakeNodeRuntime::new(), + }); + Self::new(0, project, app_state, cx) + } // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { // let dock = match position {