diff --git a/Cargo.lock b/Cargo.lock index 4a6a42b1c3..49f37fb042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1275,11 +1275,10 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" dependencies = [ - "jobserver", "libc", ] @@ -4395,15 +4394,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - [[package]] name = "journal" version = "0.1.0" diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index ab093a8deb..b2ed144a3f 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -10,6 +10,7 @@ "bindings": { "ctrl->": "zed::IncreaseBufferFontSize", "ctrl-<": "zed::DecreaseBufferFontSize", + "ctrl-shift-j": "editor::JoinLines", "cmd-d": "editor::DuplicateLine", "cmd-backspace": "editor::DeleteLine", "cmd-pagedown": "editor::MovePageDown", @@ -18,7 +19,7 @@ "cmd-alt-enter": "editor::NewlineAbove", "shift-enter": "editor::NewlineBelow", "cmd--": "editor::Fold", - "cmd-=": "editor::UnfoldLines", + "cmd-+": "editor::UnfoldLines", "alt-shift-g": "editor::SplitSelectionIntoLines", "ctrl-g": [ "editor::SelectNext", diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index abba09519b..c7a6c9ee83 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -6,9 +6,12 @@ use gpui::{ WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; -use std::cmp::{self, Reverse}; +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; use theme::ActiveTheme; -use ui::{modal, Label}; +use ui::{v_stack, HighlightedLabel, StyledExt}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -76,8 +79,8 @@ impl Modal for CommandPalette { impl Render for CommandPalette { type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - modal(cx).w_96().child(self.picker.clone()) + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + v_stack().w_96().child(self.picker.clone()) } } @@ -147,6 +150,10 @@ impl CommandPaletteDelegate { impl PickerDelegate for CommandPaletteDelegate { type ListItem = Div>; + fn placeholder_text(&self) -> Arc { + "Execute a command...".into() + } + fn match_count(&self) -> usize { self.matches.len() } @@ -296,25 +303,25 @@ impl PickerDelegate for CommandPaletteDelegate { cx: &mut ViewContext>, ) -> Self::ListItem { let colors = cx.theme().colors(); - let Some(command) = self - .matches - .get(ix) - .and_then(|m| self.commands.get(m.candidate_id)) - else { + let Some(r#match) = self.matches.get(ix) else { + return div(); + }; + let Some(command) = self.commands.get(r#match.candidate_id) else { return div(); }; div() + .px_1() .text_color(colors.text) - .when(selected, |s| { - s.border_l_10().border_color(colors.terminal_ansi_yellow) - }) - .hover(|style| { - style - .bg(colors.element_active) - .text_color(colors.text_accent) - }) - .child(Label::new(command.name.clone())) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) } // fn render_match( diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index d432ae037c..654aa73fee 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::IconButton; +use ui::{IconButton, StyledExt}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, @@ -1555,6 +1555,7 @@ impl CodeActionsMenu { let colors = cx.theme().colors(); div() .px_2() + .text_ui() .text_color(colors.text) .when(selected, |style| { style @@ -1582,7 +1583,7 @@ impl CodeActionsMenu { .collect() }, ) - .bg(cx.theme().colors().element_background) + .elevation_1(cx) .px_2() .py_1() .with_width_from_item( 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..af9febf376 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,2196 @@ 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, WindowHandle}; + 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 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 = + cx.add_window(|cx| Editor::for_buffer(rs_buffer, Some(project.clone()), cx)); + 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 = cx.add_window(|cx| Editor::for_buffer(md_buffer, Some(project), cx)); + 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 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 = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + 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: &WindowHandle, + cx: &mut gpui::TestAppContext, + ) -> Range { + let ranges = editor + .update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)) + .unwrap(); + assert_eq!( + ranges.len(), + 1, + "Single buffer should produce a single excerpt with visible range" + ); + let (_, (excerpt_buffer, _, excerpt_visible_range)) = + ranges.into_iter().next().unwrap(); + excerpt_buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let start = buffer + .anchor_before(excerpt_visible_range.start) + .to_point(&snapshot); + let end = buffer + .anchor_after(excerpt_visible_range.end) + .to_point(&snapshot); + start..end + }) + } + + // 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()) + .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 + }) + .unwrap(); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }); + 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 worktree_id = project.update(cx, |project, cx| { + project.worktrees().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.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(), + // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther + // (or renders less?) note that tests below pass + "main hint #4".to_string(), + "main hint #5".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 worktree_id = project.update(cx, |project, cx| { + project.worktrees().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.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 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 = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + 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| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + 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, WindowHandle, 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 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 = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + + 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/editor2/src/scroll/scroll_amount.rs b/crates/editor2/src/scroll/scroll_amount.rs index 89d188e324..2cb22d1516 100644 --- a/crates/editor2/src/scroll/scroll_amount.rs +++ b/crates/editor2/src/scroll/scroll_amount.rs @@ -11,19 +11,18 @@ pub enum ScrollAmount { impl ScrollAmount { pub fn lines(&self, editor: &mut Editor) -> f32 { - todo!() - // match self { - // Self::Line(count) => *count, - // Self::Page(count) => editor - // .visible_line_count() - // .map(|mut l| { - // // for full pages subtract one to leave an anchor line - // if count.abs() == 1.0 { - // l -= 1.0 - // } - // (l * count).trunc() - // }) - // .unwrap_or(0.), - // } + match self { + Self::Line(count) => *count, + Self::Page(count) => editor + .visible_line_count() + .map(|mut l| { + // for full pages subtract one to leave an anchor line + if count.abs() == 1.0 { + l -= 1.0 + } + (l * count).trunc() + }) + .unwrap_or(0.), + } } } diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index ba387c5e48..f4b8578d17 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -67,14 +67,21 @@ impl Flex { where Tag: 'static, { + // Don't assume that this initialization is what scroll_state really is in other panes: + // `element_state` is shared and there could be init races. let scroll_state = cx.element_state::>( element_id, Rc::new(ScrollState { - scroll_to: Cell::new(scroll_to), - scroll_position: Default::default(), type_tag: TypeTag::new::(), + scroll_to: Default::default(), + scroll_position: Default::default(), }), ); + // Set scroll_to separately, because the default state is already picked as `None` by other panes + // by the time we start setting it here, hence update all others' state too. + scroll_state.update(cx, |this, _| { + this.scroll_to.set(scroll_to); + }); self.scroll_state = Some((scroll_state, cx.handle().id())); self } diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 37978e7ad7..4afcc4fc1a 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -182,7 +182,8 @@ impl Platform for TestPlatform { } fn should_auto_hide_scrollbars(&self) -> bool { - unimplemented!() + // todo() + true } fn write_to_clipboard(&self, _item: crate::ClipboardItem) { diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index f132719655..289ecf7e6b 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -1,10 +1,14 @@ -use std::{rc::Rc, sync::Arc}; +use std::{ + rc::Rc, + sync::{self, Arc}, +}; +use collections::HashMap; use parking_lot::Mutex; use crate::{ - px, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, Scene, Size, - WindowAppearance, WindowBounds, WindowOptions, + px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, + PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, WindowBounds, WindowOptions, }; #[derive(Default)] @@ -30,7 +34,7 @@ impl TestWindow { current_scene: Default::default(), display, - sprite_atlas: Arc::new(TestAtlas), + sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), } } @@ -154,26 +158,71 @@ impl PlatformWindow for TestWindow { self.current_scene.lock().replace(scene); } - fn sprite_atlas(&self) -> std::sync::Arc { + fn sprite_atlas(&self) -> sync::Arc { self.sprite_atlas.clone() } } -pub struct TestAtlas; +pub struct TestAtlasState { + next_id: u32, + tiles: HashMap, +} + +pub struct TestAtlas(Mutex); + +impl TestAtlas { + pub fn new() -> Self { + TestAtlas(Mutex::new(TestAtlasState { + next_id: 0, + tiles: HashMap::default(), + })) + } +} impl PlatformAtlas for TestAtlas { fn get_or_insert_with<'a>( &self, - _key: &crate::AtlasKey, - _build: &mut dyn FnMut() -> anyhow::Result<( + key: &crate::AtlasKey, + build: &mut dyn FnMut() -> anyhow::Result<( Size, std::borrow::Cow<'a, [u8]>, )>, ) -> anyhow::Result { - todo!() + let mut state = self.0.lock(); + if let Some(tile) = state.tiles.get(key) { + return Ok(tile.clone()); + } + + state.next_id += 1; + let texture_id = state.next_id; + state.next_id += 1; + let tile_id = state.next_id; + + drop(state); + let (size, _) = build()?; + let mut state = self.0.lock(); + + state.tiles.insert( + key.clone(), + crate::AtlasTile { + texture_id: AtlasTextureId { + index: texture_id, + kind: crate::AtlasTextureKind::Path, + }, + tile_id: TileId(tile_id), + bounds: crate::Bounds { + origin: Point::zero(), + size, + }, + }, + ); + + Ok(state.tiles[key].clone()) } fn clear(&self) { - todo!() + let mut state = self.0.lock(); + state.tiles = HashMap::default(); + state.next_id = 0; } } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 9d75fcb890..e1979f1b13 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -3,9 +3,8 @@ use gpui::{ div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; -use std::cmp; -use theme::ActiveTheme; -use ui::v_stack; +use std::{cmp, sync::Arc}; +use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; pub struct Picker { pub delegate: D, @@ -21,7 +20,7 @@ pub trait PickerDelegate: Sized + 'static { fn selected_index(&self) -> usize; fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); - // fn placeholder_text(&self) -> Arc; + fn placeholder_text(&self) -> Arc; fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); @@ -37,7 +36,11 @@ pub trait PickerDelegate: Sized + 'static { impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { - let editor = cx.build_view(|cx| Editor::single_line(cx)); + let editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text(delegate.placeholder_text(), cx); + editor + }); cx.subscribe(&editor, Self::on_input_editor_event).detach(); Self { delegate, @@ -142,6 +145,7 @@ impl Render for Picker { div() .context("picker") .size_full() + .elevation_2(cx) .on_action(Self::select_next) .on_action(Self::select_prev) .on_action(Self::select_first) @@ -149,38 +153,42 @@ impl Render for Picker { .on_action(Self::cancel) .on_action(Self::confirm) .on_action(Self::secondary_confirm) - .child( - v_stack().gap_px().child( - v_stack() - .py_0p5() - .px_1() - .child(div().px_2().py_0p5().child(self.editor.clone())), - ), - ) - .child( - div() - .h_px() - .w_full() - .bg(cx.theme().colors().element_background), - ) .child( v_stack() .py_0p5() .px_1() - .grow() - .child( - uniform_list("candidates", self.delegate.match_count(), { - move |this: &mut Self, visible_range, cx| { - let selected_ix = this.delegate.selected_index(); - visible_range - .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) - .collect() - } - }) - .track_scroll(self.scroll_handle.clone()), - ) - .max_h_72() - .overflow_hidden(), + .child(div().px_1().py_0p5().child(self.editor.clone())), ) + .child(Divider::horizontal()) + .when(self.delegate.match_count() > 0, |el| { + el.child( + v_stack() + .p_1() + .grow() + .child( + uniform_list("candidates", self.delegate.match_count(), { + move |this: &mut Self, visible_range, cx| { + let selected_ix = this.delegate.selected_index(); + visible_range + .map(|ix| { + this.delegate.render_match(ix, ix == selected_ix, cx) + }) + .collect() + } + }) + .track_scroll(self.scroll_handle.clone()), + ) + .max_h_72() + .overflow_hidden(), + ) + }) + .when(self.delegate.match_count() == 0, |el| { + el.child( + v_stack() + .p_1() + .grow() + .child(Label::new("No matches").color(LabelColor::Muted)), + ) + }) } } diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 82a010e6b3..067c190575 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -44,6 +44,10 @@ impl PickerDelegate for Delegate { self.candidates.len() } + fn placeholder_text(&self) -> Arc { + "Test".into() + } + fn render_match( &self, ix: usize, diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 706918c080..e7b2d9cf0f 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -3,6 +3,7 @@ mod button; mod checkbox; mod context_menu; mod details; +mod divider; mod elevated_surface; mod facepile; mod icon; @@ -31,6 +32,7 @@ pub use button::*; pub use checkbox::*; pub use context_menu::*; pub use details::*; +pub use divider::*; pub use elevated_surface::*; pub use facepile::*; pub use icon::*; diff --git a/crates/ui2/src/components/divider.rs b/crates/ui2/src/components/divider.rs new file mode 100644 index 0000000000..5ebfc7a4ff --- /dev/null +++ b/crates/ui2/src/components/divider.rs @@ -0,0 +1,46 @@ +use crate::prelude::*; + +enum DividerDirection { + Horizontal, + Vertical, +} + +#[derive(Component)] +pub struct Divider { + direction: DividerDirection, + inset: bool, +} + +impl Divider { + pub fn horizontal() -> Self { + Self { + direction: DividerDirection::Horizontal, + inset: false, + } + } + + pub fn vertical() -> Self { + Self { + direction: DividerDirection::Vertical, + inset: false, + } + } + + pub fn inset(mut self) -> Self { + self.inset = true; + self + } + + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + div() + .map(|this| match self.direction { + DividerDirection::Horizontal => { + this.h_px().w_full().when(self.inset, |this| this.mx_1p5()) + } + DividerDirection::Vertical => { + this.w_px().h_full().when(self.inset, |this| this.my_1p5()) + } + }) + .bg(cx.theme().colors().border_variant) + } +} diff --git a/crates/ui2/src/components/elevated_surface.rs b/crates/ui2/src/components/elevated_surface.rs index 5d0bbe698c..7a6f11978e 100644 --- a/crates/ui2/src/components/elevated_surface.rs +++ b/crates/ui2/src/components/elevated_surface.rs @@ -24,5 +24,5 @@ pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext< } pub fn modal(cx: &mut ViewContext) -> Div { - elevated_surface(ElevationIndex::ModalSurfaces, cx) + elevated_surface(ElevationIndex::ModalSurface, cx) } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 57143e1f0c..5c42975b17 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -401,7 +401,7 @@ impl List { v_stack() .w_full() .py_1() - .children(self.header.map(|header| header)) + .children(self.header) .child(list_content) } } diff --git a/crates/ui2/src/elevation.md b/crates/ui2/src/elevation.md index 08acc6b12e..3960adb599 100644 --- a/crates/ui2/src/elevation.md +++ b/crates/ui2/src/elevation.md @@ -34,9 +34,9 @@ Material Design 3 has a some great visualizations of elevation that may be helpf The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app. -### UI Surface +### Surface -The UI Surface, located above the app background, is the standard level for all elements +The Surface elevation level, located above the app background, is the standard level for all elements Example Elements: Title Bar, Panel, Tab Bar, Editor diff --git a/crates/ui2/src/elevation.rs b/crates/ui2/src/elevation.rs index 0dd51e3314..8a01b9e046 100644 --- a/crates/ui2/src/elevation.rs +++ b/crates/ui2/src/elevation.rs @@ -11,43 +11,53 @@ pub enum Elevation { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ElevationIndex { - AppBackground, - UISurface, + Background, + Surface, ElevatedSurface, Wash, - ModalSurfaces, + ModalSurface, DraggedElement, } impl ElevationIndex { pub fn z_index(self) -> u32 { match self { - ElevationIndex::AppBackground => 0, - ElevationIndex::UISurface => 100, + ElevationIndex::Background => 0, + ElevationIndex::Surface => 100, ElevationIndex::ElevatedSurface => 200, ElevationIndex::Wash => 300, - ElevationIndex::ModalSurfaces => 400, + ElevationIndex::ModalSurface => 400, ElevationIndex::DraggedElement => 900, } } pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> { match self { - ElevationIndex::AppBackground => smallvec![], + ElevationIndex::Surface => smallvec![], - ElevationIndex::UISurface => smallvec![BoxShadow { + ElevationIndex::ElevatedSurface => smallvec![BoxShadow { color: hsla(0., 0., 0., 0.12), offset: point(px(0.), px(1.)), blur_radius: px(3.), spread_radius: px(0.), }], - _ => smallvec![BoxShadow { - color: hsla(0., 0., 0., 0.32), - offset: point(px(1.), px(3.)), - blur_radius: px(12.), - spread_radius: px(0.), - }], + ElevationIndex::ModalSurface => smallvec![ + BoxShadow { + color: hsla(0., 0., 0., 0.12), + offset: point(px(0.), px(1.)), + blur_radius: px(3.), + spread_radius: px(0.), + }, + BoxShadow { + color: hsla(0., 0., 0., 0.16), + offset: point(px(3.), px(1.)), + blur_radius: px(12.), + spread_radius: px(0.), + }, + ], + + _ => smallvec![], } } } diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index 980b31fe5d..0407d98f86 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -1,33 +1,33 @@ -use gpui::{Div, ElementInteractivity, KeyDispatch, Styled}; +use gpui::{Div, ElementInteractivity, KeyDispatch, Styled, UniformList, ViewContext}; +use theme2::ActiveTheme; -use crate::UITextSize; +use crate::{ElevationIndex, UITextSize}; + +fn elevated(this: E, cx: &mut ViewContext, index: ElevationIndex) -> E { + this.bg(cx.theme().colors().elevated_surface_background) + .rounded_lg() + .border() + .border_color(cx.theme().colors().border_variant) + .shadow(index.shadow()) +} /// Extends [`Styled`](gpui::Styled) with Zed specific styling methods. -pub trait StyledExt: Styled { +pub trait StyledExt: Styled + Sized { /// Horizontally stacks elements. /// /// Sets `flex()`, `flex_row()`, `items_center()` - fn h_flex(self) -> Self - where - Self: Sized, - { + fn h_flex(self) -> Self { self.flex().flex_row().items_center() } /// Vertically stacks elements. /// /// Sets `flex()`, `flex_col()` - fn v_flex(self) -> Self - where - Self: Sized, - { + fn v_flex(self) -> Self { self.flex().flex_col() } - fn text_ui_size(self, size: UITextSize) -> Self - where - Self: Sized, - { + fn text_ui_size(self, size: UITextSize) -> Self { let size = size.rems(); self.text_size(size) @@ -40,10 +40,7 @@ pub trait StyledExt: Styled { /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// /// Use [`text_ui_sm`] for regular-sized text. - fn text_ui(self) -> Self - where - Self: Sized, - { + fn text_ui(self) -> Self { let size = UITextSize::default().rems(); self.text_size(size) @@ -56,14 +53,44 @@ pub trait StyledExt: Styled { /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// /// Use [`text_ui`] for regular-sized text. - fn text_ui_sm(self) -> Self - where - Self: Sized, - { + fn text_ui_sm(self) -> Self { let size = UITextSize::Small.rems(); self.text_size(size) } + + /// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements + /// + /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` + /// + /// Example Elements: Title Bar, Panel, Tab Bar, Editor + fn elevation_1(self, cx: &mut ViewContext) -> Self { + elevated(self, cx, ElevationIndex::Surface) + } + + /// Non-Modal Elevated Surfaces appear above the [`Surface`](ui2::ElevationIndex::Surface) layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc. + /// + /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` + /// + /// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels + fn elevation_2(self, cx: &mut ViewContext) -> Self { + elevated(self, cx, ElevationIndex::ElevatedSurface) + } + + // There is no elevation 3, as the third elevation level is reserved for wash layers. See [`Elevation`](ui2::Elevation). + + /// Modal Surfaces are used for elements that should appear above all other UI elements and are located above the wash layer. This is the maximum elevation at which UI elements can be rendered in their default state. + /// + /// Elements rendered at this layer should have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal. + /// + /// If the element does not have this behavior, it should be rendered at the [`Elevated Surface`](ui2::ElevationIndex::ElevatedSurface) layer. + /// + /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` + /// + /// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs + fn elevation_4(self, cx: &mut ViewContext) -> Self { + elevated(self, cx, ElevationIndex::ModalSurface) + } } impl StyledExt for Div @@ -72,3 +99,5 @@ where F: KeyDispatch, { } + +impl StyledExt for UniformList {} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 21c72fd385..14a7685a9b 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 {