From 5d2750e0d4e5e9206146cef07c564beb92168cb7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 14:39:37 +0300 Subject: [PATCH 01/12] Hide inlay cache fields --- crates/collab/src/tests/integration_tests.rs | 42 +++++++++++--------- crates/editor/src/inlay_hint_cache.rs | 23 ++++++++--- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index ce7fd8a094..657457d592 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7953,7 +7953,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Host editor update the cache version after every cache/view change", ); }); @@ -7976,7 +7977,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest editor update the cache version after every cache/view change" ); }); @@ -7996,7 +7998,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Host should get hints from the 1st edit and 1st LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_b.update(cx_b, |editor, _| { assert_eq!( @@ -8010,7 +8012,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_a.update(cx_a, |editor, cx| { @@ -8035,7 +8037,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( 4th query was made by guest (but not applied) due to cache invalidation logic" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_b.update(cx_b, |editor, _| { assert_eq!( @@ -8051,7 +8053,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get hints from 3rd edit, 6th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); fake_language_server @@ -8077,7 +8079,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Host should accepted all edits and bump its cache version every time" ); }); @@ -8098,7 +8101,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, + inlay_cache.version(), edits_made, "Guest should accepted all edits and bump its cache version every time" ); @@ -8264,7 +8267,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 0, + inlay_cache.version(), + 0, "Host should not increment its cache version due to no changes", ); }); @@ -8279,7 +8283,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest editor update the cache version after every cache/view change" ); }); @@ -8296,7 +8301,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 0, + inlay_cache.version(), + 0, "Host should not increment its cache version due to no changes", ); }); @@ -8311,7 +8317,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest should accepted all edits and bump its cache version every time" ); }); @@ -8343,13 +8350,10 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP fn extract_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 (_, inlay) in excerpt_hints.hints.iter() { - match &inlay.label { - project::InlayHintLabel::String(s) => labels.push(s.to_string()), - _ => unreachable!(), - } + for hint in editor.inlay_hint_cache().hints() { + match hint.label { + project::InlayHintLabel::String(s) => labels.push(s), + _ => unreachable!(), } } labels diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 2d75b4d2ce..44fbbf163d 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -20,10 +20,10 @@ use language::language_settings::InlayHintSettings; use util::post_inc; pub struct InlayHintCache { - pub hints: HashMap>>, - pub allowed_hint_kinds: HashSet>, - pub version: usize, - pub enabled: bool, + hints: HashMap>>, + allowed_hint_kinds: HashSet>, + version: usize, + enabled: bool, update_tasks: HashMap, } @@ -32,7 +32,7 @@ pub struct CachedExcerptHints { version: usize, buffer_version: Global, buffer_id: u64, - pub hints: Vec<(InlayId, InlayHint)>, + hints: Vec<(InlayId, InlayHint)>, } #[derive(Debug, Clone, Copy)] @@ -368,6 +368,19 @@ impl InlayHintCache { self.update_tasks.clear(); self.hints.clear(); } + + pub fn hints(&self) -> Vec { + let mut hints = Vec::new(); + for excerpt_hints in self.hints.values() { + let excerpt_hints = excerpt_hints.read(); + hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned()); + } + hints + } + + pub fn version(&self) -> usize { + self.version + } } fn spawn_new_update_tasks( From 708409e06d381269a21dedbd02fa86b30c5d4047 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 14:51:38 +0300 Subject: [PATCH 02/12] Query hints on every scroll --- crates/editor/src/inlay_hint_cache.rs | 4 +++- crates/editor/src/scroll.rs | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 44fbbf163d..1dbef165fa 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -24,6 +24,7 @@ pub struct InlayHintCache { allowed_hint_kinds: HashSet>, version: usize, enabled: bool, + // TODO kb track them by excerpt range update_tasks: HashMap, } @@ -100,6 +101,7 @@ impl InvalidationStrategy { } impl ExcerptQuery { + // TODO kb query only visible + one visible below and above fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges { let visible_range = self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end; @@ -168,7 +170,6 @@ impl InlayHintCache { ); if new_splice.is_some() { self.version += 1; - self.update_tasks.clear(); self.allowed_hint_kinds = new_allowed_hint_kinds; } ControlFlow::Break(new_splice) @@ -464,6 +465,7 @@ fn spawn_new_update_tasks( cx, ) }; + // TODO kb need to add to update tasks + ensure RefreshRequested cleans other ranges match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { hash_map::Entry::Occupied(mut o) => { let update_task = o.get_mut(); diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index d595337428..1f3adaf477 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -13,7 +13,7 @@ use gpui::{ }; use language::{Bias, Point}; use util::ResultExt; -use workspace::{item::Item, WorkspaceId}; +use workspace::WorkspaceId; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, @@ -333,9 +333,7 @@ impl Editor { cx, ); - if !self.is_singleton(cx) { - self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); - } + self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { From 0e2a1fc14996c5831c1060fe7cf3ddb3670a6626 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 18:09:09 +0300 Subject: [PATCH 03/12] Query inlay hints for parts of the file --- crates/editor/src/inlay_hint_cache.rs | 396 ++++++++++---------------- 1 file changed, 145 insertions(+), 251 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1dbef165fa..30f02c17f5 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -9,7 +9,7 @@ use crate::{ }; use anyhow::Context; use clock::Global; -use gpui::{ModelHandle, Task, ViewContext}; +use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; use parking_lot::RwLock; @@ -24,8 +24,13 @@ pub struct InlayHintCache { allowed_hint_kinds: HashSet>, version: usize, enabled: bool, - // TODO kb track them by excerpt range - update_tasks: HashMap, + update_tasks: HashMap, +} + +#[derive(Debug)] +struct TasksForRanges { + tasks: Vec>, + ranges: Vec>, } #[derive(Debug)] @@ -49,18 +54,6 @@ pub struct InlaySplice { pub to_insert: Vec, } -struct UpdateTask { - invalidate: InvalidationStrategy, - cache_version: usize, - task: RunningTask, - pending_refresh: Option, -} - -struct RunningTask { - _task: Task<()>, - is_running_rx: smol::channel::Receiver<()>, -} - #[derive(Debug)] struct ExcerptHintsUpdate { excerpt_id: ExcerptId, @@ -73,24 +66,10 @@ struct ExcerptHintsUpdate { struct ExcerptQuery { buffer_id: u64, excerpt_id: ExcerptId, - dimensions: ExcerptDimensions, cache_version: usize, invalidate: InvalidationStrategy, } -#[derive(Debug, Clone, Copy)] -struct ExcerptDimensions { - excerpt_range_start: language::Anchor, - excerpt_range_end: language::Anchor, - excerpt_visible_range_start: language::Anchor, - excerpt_visible_range_end: language::Anchor, -} - -struct HintFetchRanges { - visible_range: Range, - other_ranges: Vec>, -} - impl InvalidationStrategy { fn should_invalidate(&self) -> bool { matches!( @@ -100,37 +79,43 @@ impl InvalidationStrategy { } } -impl ExcerptQuery { - // TODO kb query only visible + one visible below and above - fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges { - let visible_range = - self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end; - let mut other_ranges = Vec::new(); - if self - .dimensions - .excerpt_range_start - .cmp(&visible_range.start, buffer) - .is_lt() - { - let mut end = visible_range.start; - end.offset -= 1; - other_ranges.push(self.dimensions.excerpt_range_start..end); - } - if self - .dimensions - .excerpt_range_end - .cmp(&visible_range.end, buffer) - .is_gt() - { - let mut start = visible_range.end; - start.offset += 1; - other_ranges.push(start..self.dimensions.excerpt_range_end); +impl TasksForRanges { + fn new(ranges: Vec>, task: Task<()>) -> Self { + Self { + tasks: vec![task], + ranges, } + } - HintFetchRanges { - visible_range, - other_ranges: other_ranges.into_iter().map(|range| range).collect(), - } + fn update_cached_tasks( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_range: Range, + invalidate: InvalidationStrategy, + spawn_task: impl FnOnce(Vec>) -> Task<()>, + ) { + let ranges_to_query = match invalidate { + InvalidationStrategy::None => { + // let mut ranges_to_query = Vec::new(); + + // todo!("TODO kb also remove task ranges on invalidation"); + // if ranges_to_query.is_empty() { + // return; + // } + // ranges_to_query + vec![query_range] + } + InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { + self.tasks.clear(); + self.ranges.clear(); + vec![query_range] + } + }; + + self.ranges.extend(ranges_to_query.clone()); + self.ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); + self.tasks.push(spawn_task(ranges_to_query)); } } @@ -198,7 +183,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, - mut excerpts_to_query: HashMap, Global, Range)>, + excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, ) -> Option { @@ -206,11 +191,10 @@ impl InlayHintCache { return None; } - let update_tasks = &mut self.update_tasks; let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { let mut changed = false; - update_tasks.retain(|task_excerpt_id, _| { + self.update_tasks.retain(|task_excerpt_id, _| { let retain = excerpts_to_query.contains_key(task_excerpt_id); changed |= !retain; retain @@ -232,17 +216,6 @@ impl InlayHintCache { } let cache_version = self.version; - excerpts_to_query.retain(|visible_excerpt_id, _| { - match update_tasks.entry(*visible_excerpt_id) { - hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) { - cmp::Ordering::Less => true, - cmp::Ordering::Equal => invalidate.should_invalidate(), - cmp::Ordering::Greater => false, - }, - hash_map::Entry::Vacant(_) => true, - } - }); - cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { @@ -392,13 +365,14 @@ fn spawn_new_update_tasks( cx: &mut ViewContext<'_, '_, Editor>, ) { let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); - for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in + 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 = buffer_handle.read(cx); + let buffer = excerpt_buffer.read(cx); + let buffer_id = buffer.remote_id(); let buffer_snapshot = buffer.snapshot(); if buffer_snapshot .version() @@ -416,203 +390,120 @@ fn spawn_new_update_tasks( { continue; } - if !new_task_buffer_version.changed_since(&cached_buffer_version) - && !matches!(invalidate, InvalidationStrategy::RefreshRequested) - { - continue; - } }; - let buffer_id = buffer.remote_id(); - let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start); - let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end); - - let (multi_buffer_snapshot, full_excerpt_range) = + let (multi_buffer_snapshot, Some(query_range)) = editor.buffer.update(cx, |multi_buffer, cx| { - let multi_buffer_snapshot = multi_buffer.snapshot(cx); ( - multi_buffer_snapshot, - multi_buffer - .excerpts_for_buffer(&buffer_handle, cx) - .into_iter() - .find(|(id, _)| id == &excerpt_id) - .map(|(_, range)| range.context), + multi_buffer.snapshot(cx), + determine_query_range( + 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, + }; - if let Some(full_excerpt_range) = full_excerpt_range { - let query = ExcerptQuery { - buffer_id, - excerpt_id, - dimensions: ExcerptDimensions { - excerpt_range_start: full_excerpt_range.start, - excerpt_range_end: full_excerpt_range.end, - excerpt_visible_range_start, - excerpt_visible_range_end, - }, - cache_version: update_cache_version, - invalidate, - }; - - let new_update_task = |is_refresh_after_regular_task| { - new_update_task( - query, - multi_buffer_snapshot, - buffer_snapshot, - Arc::clone(&visible_hints), - cached_excerpt_hints, - is_refresh_after_regular_task, - cx, - ) - }; - // TODO kb need to add to update tasks + ensure RefreshRequested cleans other ranges - match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { - hash_map::Entry::Occupied(mut o) => { - let update_task = o.get_mut(); - match (update_task.invalidate, invalidate) { - (_, InvalidationStrategy::None) => {} - ( - InvalidationStrategy::BufferEdited, - InvalidationStrategy::RefreshRequested, - ) if !update_task.task.is_running_rx.is_closed() => { - update_task.pending_refresh = Some(query); - } - _ => { - o.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } - } - } - hash_map::Entry::Vacant(v) => { - v.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } + let new_update_task = |fetch_ranges| { + new_update_task( + query, + fetch_ranges, + multi_buffer_snapshot, + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints, + 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_range, + invalidate, + new_update_task, + ); + } + hash_map::Entry::Vacant(v) => { + v.insert(TasksForRanges::new( + vec![query_range.clone()], + new_update_task(vec![query_range]), + )); } } } } +fn determine_query_range( + multi_buffer: &mut MultiBuffer, + excerpt_id: ExcerptId, + excerpt_buffer: &ModelHandle, + excerpt_visible_range: Range, + cx: &mut ModelContext<'_, MultiBuffer>, +) -> Option> { + let full_excerpt_range = multi_buffer + .excerpts_for_buffer(excerpt_buffer, cx) + .into_iter() + .find(|(id, _)| id == &excerpt_id) + .map(|(_, range)| range.context)?; + + let buffer = excerpt_buffer.read(cx); + let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; + let start = buffer.anchor_before( + excerpt_visible_range + .start + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range.start.offset), + ); + let end = buffer.anchor_after( + excerpt_visible_range + .end + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range.end.offset) + .min(buffer.len()), + ); + Some(start..end) +} + fn new_update_task( query: ExcerptQuery, + hints_fetch_ranges: Vec>, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, cached_excerpt_hints: Option>>, - is_refresh_after_regular_task: bool, cx: &mut ViewContext<'_, '_, Editor>, -) -> RunningTask { - let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot); - let (is_running_tx, is_running_rx) = smol::channel::bounded(1); - let _task = cx.spawn(|editor, mut cx| async move { - let _is_running_tx = is_running_tx; - let create_update_task = |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, - range, - cx.clone(), - ) - }; - - if is_refresh_after_regular_task { - let visible_range_has_updates = - match create_update_task(hints_fetch_ranges.visible_range).await { - Ok(updated) => updated, - Err(e) => { - error!("inlay hint visible range update task failed: {e:#}"); - return; - } - }; - - if visible_range_has_updates { - let other_update_results = futures::future::join_all( - hints_fetch_ranges - .other_ranges - .into_iter() - .map(create_update_task), +) -> Task<()> { + cx.spawn(|editor, cx| async move { + let task_update_results = + futures::future::join_all(hints_fetch_ranges.into_iter().map(|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, + range, + cx.clone(), ) - .await; - - for result in other_update_results { - if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); - } - } - } - } else { - let task_update_results = futures::future::join_all( - std::iter::once(hints_fetch_ranges.visible_range) - .chain(hints_fetch_ranges.other_ranges.into_iter()) - .map(create_update_task), - ) + })) .await; - for result in task_update_results { - if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); - } + for result in task_update_results { + if let Err(e) = result { + error!("inlay hint update task failed: {e:#}"); } } - - editor - .update(&mut cx, |editor, cx| { - let pending_refresh_query = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) - .and_then(|task| task.pending_refresh.take()); - - if let Some(pending_refresh_query) = pending_refresh_query { - let refresh_multi_buffer = editor.buffer().read(cx); - let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx); - let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx)); - let refresh_cached_excerpt_hints = editor - .inlay_hint_cache - .hints - .get(&pending_refresh_query.excerpt_id) - .map(Arc::clone); - if let Some(buffer) = - refresh_multi_buffer.buffer(pending_refresh_query.buffer_id) - { - editor.inlay_hint_cache.update_tasks.insert( - pending_refresh_query.excerpt_id, - UpdateTask { - invalidate: InvalidationStrategy::RefreshRequested, - cache_version: editor.inlay_hint_cache.version, - task: new_update_task( - pending_refresh_query, - refresh_multi_buffer_snapshot, - buffer.read(cx).snapshot(), - refresh_visible_hints, - refresh_cached_excerpt_hints, - true, - cx, - ), - pending_refresh: None, - }, - ); - } - } - }) - .ok(); - }); - - RunningTask { - _task, - is_running_rx, - } + }) } async fn fetch_and_update_hints( @@ -2202,7 +2093,8 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - "main hint #4".to_string(), + // TODO kb find the range needed + // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2227,7 +2119,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - "main hint #4".to_string(), + // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2255,7 +2147,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - "main hint #4".to_string(), + // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2284,6 +2176,8 @@ mod tests { "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), "main hint(edited) #3".to_string(), + // TODO kb why? + "main hint(edited) #3".to_string(), "main hint(edited) #4".to_string(), "main hint(edited) #5".to_string(), "other hint(edited) #0".to_string(), From 56f89739f888c9122f54de48d701a52ced806f91 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 17:36:34 +0300 Subject: [PATCH 04/12] Do not add duplicate hints to the cache --- crates/editor/src/inlay_hint_cache.rs | 106 +++++++++++++------------- crates/editor/src/scroll.rs | 1 + 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 30f02c17f5..e792064ec7 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -193,29 +193,21 @@ impl InlayHintCache { let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { - let mut changed = false; - self.update_tasks.retain(|task_excerpt_id, _| { - let retain = excerpts_to_query.contains_key(task_excerpt_id); - changed |= !retain; - retain - }); + self.update_tasks + .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); self.hints.retain(|cached_excerpt, cached_hints| { let retain = excerpts_to_query.contains_key(cached_excerpt); - changed |= !retain; if !retain { invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id)); } retain }); - if changed { - self.version += 1; - } } if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { return None; } - let cache_version = self.version; + let cache_version = self.version + 1; cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { @@ -475,7 +467,7 @@ fn determine_query_range( fn new_update_task( query: ExcerptQuery, - hints_fetch_ranges: Vec>, + hint_fetch_ranges: Vec>, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, @@ -484,7 +476,7 @@ fn new_update_task( ) -> Task<()> { cx.spawn(|editor, cx| async move { let task_update_results = - futures::future::join_all(hints_fetch_ranges.into_iter().map(|range| { + futures::future::join_all(hint_fetch_ranges.into_iter().map(|range| { fetch_and_update_hints( editor.clone(), multi_buffer_snapshot.clone(), @@ -515,7 +507,7 @@ async fn fetch_and_update_hints( query: ExcerptQuery, fetch_range: Range, mut cx: gpui::AsyncAppContext, -) -> anyhow::Result { +) -> anyhow::Result<()> { let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { editor @@ -531,8 +523,7 @@ async fn fetch_and_update_hints( }) .ok() .flatten(); - let mut update_happened = false; - let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) }; + let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(()) }; let new_hints = inlay_hints_fetch_task .await .context("inlay hint fetch task")?; @@ -555,10 +546,6 @@ async fn fetch_and_update_hints( editor .update(&mut cx, |editor, cx| { if let Some(new_update) = new_update { - update_happened = !new_update.add_to_cache.is_empty() - || !new_update.remove_from_cache.is_empty() - || !new_update.remove_from_visible.is_empty(); - let cached_excerpt_hints = editor .inlay_hint_cache .hints @@ -578,43 +565,51 @@ async fn fetch_and_update_hints( cached_excerpt_hints.version = query.cache_version; } } + + let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); cached_excerpt_hints .hints .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); - cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); - editor.inlay_hint_cache.version += 1; - 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 new_hint_position = multi_buffer_snapshot - .anchor_in_excerpt(query.excerpt_id, new_hint.position); - let new_inlay_id = post_inc(&mut editor.next_inlay_id); - if editor - .inlay_hint_cache - .allowed_hint_kinds - .contains(&new_hint.kind) - { - splice.to_insert.push(Inlay::hint( - new_inlay_id, - new_hint_position, - &new_hint, - )); + let cached_hints = &mut cached_excerpt_hints.hints; + let insert_position = match cached_hints.binary_search_by(|probe| { + probe.1.position.cmp(&new_hint.position, &buffer_snapshot) + }) { + Ok(i) => { + if cached_hints[i].1.text() == new_hint.text() { + None + } else { + Some(i) + } + } + 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, + )); + } + cached_hints + .insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); + cached_inlays_changed = true; } - - cached_excerpt_hints - .hints - .push((InlayId::Hint(new_inlay_id), new_hint)); } - - cached_excerpt_hints - .hints - .sort_by(|(_, hint_a), (_, hint_b)| { - hint_a.position.cmp(&hint_b.position, &buffer_snapshot) - }); + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); drop(cached_excerpt_hints); if query.invalidate.should_invalidate() { @@ -633,6 +628,7 @@ async fn fetch_and_update_hints( .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); } } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); editor .inlay_hint_cache .hints @@ -643,14 +639,18 @@ async fn fetch_and_update_hints( to_remove, to_insert, } = splice; - if !to_remove.is_empty() || !to_insert.is_empty() { + 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) } } }) .ok(); - Ok(update_happened) + Ok(()) } fn calculate_hint_updates( @@ -2093,7 +2093,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // TODO kb find the range needed + // TODO kb find the range needed. Is it due to the hint not fitting any excerpt subranges? // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), @@ -2176,8 +2176,6 @@ mod tests { "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), "main hint(edited) #3".to_string(), - // TODO kb why? - "main hint(edited) #3".to_string(), "main hint(edited) #4".to_string(), "main hint(edited) #5".to_string(), "other hint(edited) #0".to_string(), @@ -2192,8 +2190,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, - last_scroll_update_version + expected_layers.len() + 1, - "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal" + last_scroll_update_version + expected_layers.len(), + "Due to every excerpt having one hint, cache should update per new excerpt received" ); }); } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 1f3adaf477..9a6748fa7c 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -333,6 +333,7 @@ impl Editor { cx, ); + // TODO kb too many events + too many LSP requests even due to deduplication, why? self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } From 449c009639d07d760a33d8caac8dc80e05dac9c5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 23:06:57 +0300 Subject: [PATCH 05/12] Properly generate ranges to query --- crates/editor/src/inlay_hint_cache.rs | 71 ++++++++++++++++++++++----- crates/editor/src/scroll.rs | 1 - 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index e792064ec7..a3edb65128 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -96,14 +96,55 @@ impl TasksForRanges { ) { let ranges_to_query = match invalidate { InvalidationStrategy::None => { - // let mut ranges_to_query = Vec::new(); + let mut ranges_to_query = Vec::new(); + let mut last_cache_range_stop = None::; + for cached_range in self + .ranges + .iter() + .skip_while(|cached_range| { + cached_range + .end + .cmp(&query_range.start, buffer_snapshot) + .is_lt() + }) + .take_while(|cached_range| { + cached_range + .start + .cmp(&query_range.end, buffer_snapshot) + .is_le() + }) + { + match last_cache_range_stop { + Some(last_cache_range_stop) => { + if last_cache_range_stop.offset.saturating_add(1) + < cached_range.start.offset + { + ranges_to_query.push(last_cache_range_stop..cached_range.start); + } + } + None => { + if query_range + .start + .cmp(&cached_range.start, buffer_snapshot) + .is_lt() + { + ranges_to_query.push(query_range.start..cached_range.start); + } + } + } + last_cache_range_stop = Some(cached_range.end); + } - // todo!("TODO kb also remove task ranges on invalidation"); - // if ranges_to_query.is_empty() { - // return; - // } - // ranges_to_query - vec![query_range] + match last_cache_range_stop { + Some(last_cache_range_stop) => { + if last_cache_range_stop.offset.saturating_add(1) < query_range.end.offset { + ranges_to_query.push(last_cache_range_stop..query_range.end); + } + } + None => ranges_to_query.push(query_range), + } + + ranges_to_query } InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { self.tasks.clear(); @@ -112,10 +153,12 @@ impl TasksForRanges { } }; - self.ranges.extend(ranges_to_query.clone()); - self.ranges - .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); - self.tasks.push(spawn_task(ranges_to_query)); + if !ranges_to_query.is_empty() { + self.ranges.extend(ranges_to_query.clone()); + self.ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); + self.tasks.push(spawn_task(ranges_to_query)); + } } } @@ -462,7 +505,11 @@ fn determine_query_range( .min(full_excerpt_range.end.offset) .min(buffer.len()), ); - Some(start..end) + if start.cmp(&end, buffer).is_eq() { + None + } else { + Some(start..end) + } } fn new_update_task( diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 9a6748fa7c..1f3adaf477 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -333,7 +333,6 @@ impl Editor { cx, ); - // TODO kb too many events + too many LSP requests even due to deduplication, why? self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } From 87e6651ecb2acbe8f949e786528bcb751ff41def Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 11:24:49 +0300 Subject: [PATCH 06/12] Fix hint tests, add a char boundary bug test --- crates/editor/src/editor.rs | 2 +- crates/editor/src/inlay_hint_cache.rs | 534 ++++++++++++++++++-------- 2 files changed, 377 insertions(+), 159 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d8b77ea95..f65f19cfec 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2723,7 +2723,7 @@ impl Editor { .collect() } - fn excerpt_visible_offsets( + pub fn excerpt_visible_offsets( &self, restrict_to_languages: Option<&HashSet>>, cx: &mut ViewContext<'_, '_, Editor>, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index a3edb65128..b06a720090 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -30,7 +30,7 @@ pub struct InlayHintCache { #[derive(Debug)] struct TasksForRanges { tasks: Vec>, - ranges: Vec>, + sorted_ranges: Vec>, } #[derive(Debug)] @@ -80,10 +80,10 @@ impl InvalidationStrategy { } impl TasksForRanges { - fn new(ranges: Vec>, task: Task<()>) -> Self { + fn new(sorted_ranges: Vec>, task: Task<()>) -> Self { Self { tasks: vec![task], - ranges, + sorted_ranges, } } @@ -99,8 +99,8 @@ impl TasksForRanges { let mut ranges_to_query = Vec::new(); let mut last_cache_range_stop = None::; for cached_range in self - .ranges - .iter() + .sorted_ranges + .iter_mut() .skip_while(|cached_range| { cached_range .end @@ -148,14 +148,14 @@ impl TasksForRanges { } InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { self.tasks.clear(); - self.ranges.clear(); + self.sorted_ranges.clear(); vec![query_range] } }; if !ranges_to_query.is_empty() { - self.ranges.extend(ranges_to_query.clone()); - self.ranges + self.sorted_ranges.extend(ranges_to_query.clone()); + self.sorted_ranges .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); self.tasks.push(spawn_task(ranges_to_query)); } @@ -458,6 +458,7 @@ fn spawn_new_update_tasks( cx, ) }; + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { hash_map::Entry::Occupied(mut o) => { o.get_mut().update_cached_tasks( @@ -570,10 +571,10 @@ async fn fetch_and_update_hints( }) .ok() .flatten(); - let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(()) }; - let new_hints = inlay_hints_fetch_task - .await - .context("inlay hint fetch task")?; + let new_hints = match inlay_hints_fetch_task { + Some(task) => task.await.context("inlay hint fetch task")?, + None => return Ok(()), + }; let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); let new_update = cx @@ -589,114 +590,20 @@ async fn fetch_and_update_hints( ) }) .await; - - editor - .update(&mut cx, |editor, cx| { - if let Some(new_update) = new_update { - 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, - hints: Vec::new(), - })) - }); - 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 - .hints - .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 cached_hints = &mut cached_excerpt_hints.hints; - let insert_position = match cached_hints.binary_search_by(|probe| { - probe.1.position.cmp(&new_hint.position, &buffer_snapshot) - }) { - Ok(i) => { - if cached_hints[i].1.text() == new_hint.text() { - None - } else { - Some(i) - } - } - 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, - )); - } - cached_hints - .insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); - cached_inlays_changed = true; - } - } - cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); - drop(cached_excerpt_hints); - - if query.invalidate.should_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.hints.iter().map(|(id, _)| id)); - } - } - 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) - } - } - }) - .ok(); - + if let Some(new_update) = new_update { + editor + .update(&mut cx, |editor, cx| { + apply_hint_update( + editor, + new_update, + query, + buffer_snapshot, + multi_buffer_snapshot, + cx, + ); + }) + .ok(); + } Ok(()) } @@ -808,6 +715,113 @@ fn contains_position( && range.end.cmp(&position, buffer_snapshot).is_ge() } +fn apply_hint_update( + editor: &mut Editor, + new_update: ExcerptHintsUpdate, + query: ExcerptQuery, + buffer_snapshot: BufferSnapshot, + multi_buffer_snapshot: MultiBufferSnapshot, + cx: &mut ViewContext<'_, '_, Editor>, +) { + 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, + hints: Vec::new(), + })) + }); + 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 + .hints + .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 cached_hints = &mut cached_excerpt_hints.hints; + let insert_position = match cached_hints + .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot)) + { + Ok(i) => { + if cached_hints[i].1.text() == new_hint.text() { + None + } else { + Some(i) + } + } + 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)); + } + cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); + cached_inlays_changed = true; + } + } + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); + drop(cached_excerpt_hints); + + if query.invalidate.should_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.hints.iter().map(|(id, _)| id)); + } + } + 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)] mod tests { use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -819,6 +833,7 @@ mod tests { }; use futures::StreamExt; use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; + use itertools::Itertools; use language::{ language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, }; @@ -826,7 +841,7 @@ mod tests { use parking_lot::Mutex; use project::{FakeFs, Project}; use settings::SettingsStore; - use text::Point; + use text::{Point, ToPoint}; use workspace::Workspace; use crate::editor_tests::update_test_language_settings; @@ -1832,7 +1847,7 @@ mod tests { task_lsp_request_ranges.lock().push(params.range); let query_start = params.range.start; - let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + 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()), @@ -1847,18 +1862,44 @@ mod tests { }) .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 + }) + } + + let initial_visible_range = editor_visible_range(&editor, cx); + let expected_initial_query_range_end = + lsp::Position::new(initial_visible_range.end.row * 2, 1); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - ranges.sort_by_key(|range| range.start); - assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); - assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); - assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line"); - assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent"); + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + assert_eq!(ranges.len(), 1, + "When scroll is at the edge of a big document, double of its visible part range should be queried for hints in one single big request, but got: {ranges:?}"); + let query_range = &ranges[0]; + assert_eq!(query_range.start, lsp::Position::new(0, 0), "Should query initially from the beginning of the document"); + assert_eq!(query_range.end, expected_initial_query_range_end, "Should query initially for double lines of the visible part of the document"); - assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2, - "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); - let expected_layers = vec!["1".to_string(), "2".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 1); + let expected_layers = vec!["1".to_string()]; assert_eq!( expected_layers, cached_hint_labels(editor), @@ -1866,37 +1907,108 @@ mod tests { ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!( - editor.inlay_hint_cache().version, 2, - "Both LSP queries should've bumped the cache version" + editor.inlay_hint_cache().version, 1, + "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); - editor.change_selections(None, cx, |s| s.select_ranges([600..600])); - editor.handle_input("++++more text++++", cx); }); + let visible_range_after_scrolls = editor_visible_range(&editor, cx); + cx.foreground().run_until_parked(); + 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", + ); + let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; + assert_eq!( + second_scroll.end, + lsp::Position::new( + visible_range_after_scrolls.end.row + + expected_increment, + 0 + ), + "Second scroll should query one more screen down after the end of the visible range" + ); + + assert_eq!( + lsp_request_count.load(Ordering::Acquire), + 3, + "Should query for hints after every scroll" + ); + let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Should have hints from the new LSP response after the edit" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 3, + "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 -= expected_increment; + selection_in_cached_range + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }); + 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), 3); + }); + + editor.update(cx, |editor, cx| { + editor.handle_input("++++more text++++", cx); + }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - ranges.sort_by_key(|range| range.start); - assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints"); - assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); - assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end"); - assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning"); - assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning"); - assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line"); - assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent"); + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; + assert_eq!(ranges.len(), 1, + "On edit, should scroll to selection and query a range around it. Instead, got query ranges {ranges:?}"); + let query_range = &ranges[0]; + assert_eq!(query_range.start, lsp::Position::new(selection_in_cached_range.row - expected_increment, 0)); + assert_eq!(query_range.end, lsp::Position::new(selection_in_cached_range.row + expected_increment, 0)); - assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5, - "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints"); - let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "Should query for hints after the scroll and again after the edit"); + let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor), - "Should have hints from the new LSP response after edit"); + "Should have hints from the new LSP response after the edit"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added"); + assert_eq!(editor.inlay_hint_cache().version, 3, "Should update the cache for every LSP response with hints added"); }); } @@ -2130,7 +2242,7 @@ mod tests { s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) }); editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) }); }); cx.foreground().run_until_parked(); @@ -2140,8 +2252,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // TODO kb find the range needed. Is it due to the hint not fitting any excerpt subranges? - // "main hint #4".to_string(), + "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2166,7 +2277,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // "main hint #4".to_string(), + "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2194,7 +2305,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // "main hint #4".to_string(), + "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2442,8 +2553,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 3, - "Excerpt removal should trigger cache update" + 2, + "Excerpt removal should trigger a cache update" ); }); @@ -2470,12 +2581,119 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 4, - "Settings change should trigger cache update" + 3, + "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_layers = vec!["1".to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + } + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { cx.foreground().forbid_parking(); From 558367dc8babf93484db741ad1235014501d58f0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 16:19:44 +0300 Subject: [PATCH 07/12] Optimize query ranges tracking --- crates/editor/src/inlay_hint_cache.rs | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b06a720090..d014e71488 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -97,7 +97,7 @@ impl TasksForRanges { let ranges_to_query = match invalidate { InvalidationStrategy::None => { let mut ranges_to_query = Vec::new(); - let mut last_cache_range_stop = None::; + let mut latest_cached_range = None::<&mut Range>; for cached_range in self .sorted_ranges .iter_mut() @@ -114,12 +114,13 @@ impl TasksForRanges { .is_le() }) { - match last_cache_range_stop { - Some(last_cache_range_stop) => { - if last_cache_range_stop.offset.saturating_add(1) + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset { - ranges_to_query.push(last_cache_range_stop..cached_range.start); + ranges_to_query.push(latest_cached_range.end..cached_range.start); + cached_range.start = latest_cached_range.end; } } None => { @@ -129,19 +130,28 @@ impl TasksForRanges { .is_lt() { ranges_to_query.push(query_range.start..cached_range.start); + cached_range.start = query_range.start; } } } - last_cache_range_stop = Some(cached_range.end); + latest_cached_range = Some(cached_range); } - match last_cache_range_stop { - Some(last_cache_range_stop) => { - if last_cache_range_stop.offset.saturating_add(1) < query_range.end.offset { - ranges_to_query.push(last_cache_range_stop..query_range.end); + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset + { + ranges_to_query.push(latest_cached_range.end..query_range.end); + latest_cached_range.end = query_range.end; } } - None => ranges_to_query.push(query_range), + None => { + ranges_to_query.push(query_range.clone()); + self.sorted_ranges.push(query_range); + self.sorted_ranges.sort_by(|range_a, range_b| { + range_a.start.cmp(&range_b.start, buffer_snapshot) + }); + } } ranges_to_query @@ -154,9 +164,6 @@ impl TasksForRanges { }; if !ranges_to_query.is_empty() { - self.sorted_ranges.extend(ranges_to_query.clone()); - self.sorted_ranges - .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); self.tasks.push(spawn_task(ranges_to_query)); } } From 336fbb3392ece0db7d78e778257b6e652bf81dab Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 18:37:50 +0300 Subject: [PATCH 08/12] Clip offsets in inlay hint queries --- crates/editor/src/inlay_hint_cache.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d014e71488..24aa84ee88 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -17,6 +17,7 @@ use project::InlayHint; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use sum_tree::Bias; use util::post_inc; pub struct InlayHintCache { @@ -500,19 +501,17 @@ fn determine_query_range( let buffer = excerpt_buffer.read(cx); let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; - let start = buffer.anchor_before( - excerpt_visible_range - .start - .saturating_sub(excerpt_visible_len) - .max(full_excerpt_range.start.offset), - ); - let end = buffer.anchor_after( - excerpt_visible_range - .end - .saturating_add(excerpt_visible_len) - .min(full_excerpt_range.end.offset) - .min(buffer.len()), - ); + let start_offset = excerpt_visible_range + .start + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range.start.offset); + let start = buffer.anchor_before(buffer.clip_offset(start_offset, Bias::Left)); + let end_offset = excerpt_visible_range + .end + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range.end.offset) + .min(buffer.len()); + let end = buffer.anchor_after(buffer.clip_offset(end_offset, Bias::Right)); if start.cmp(&end, buffer).is_eq() { None } else { From 4b3273182ac90b4d2df9c5e7cd13b03665a1dfe6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 19:20:20 +0300 Subject: [PATCH 09/12] Do not filter out hints to be removed --- crates/editor/src/inlay_hint_cache.rs | 29 +++------------------------ 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 24aa84ee88..d6b45629a4 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -659,19 +659,6 @@ fn calculate_hint_updates( visible_hints .iter() .filter(|hint| hint.position.excerpt_id == query.excerpt_id) - .filter(|hint| { - contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot) - }) - .filter(|hint| { - fetch_range - .start - .cmp(&hint.position.text_anchor, buffer_snapshot) - .is_le() - && fetch_range - .end - .cmp(&hint.position.text_anchor, buffer_snapshot) - .is_ge() - }) .map(|inlay_hint| inlay_hint.id) .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), ); @@ -685,16 +672,6 @@ fn calculate_hint_updates( .filter(|(cached_inlay_id, _)| { !excerpt_hints_to_persist.contains_key(cached_inlay_id) }) - .filter(|(_, cached_hint)| { - fetch_range - .start - .cmp(&cached_hint.position, buffer_snapshot) - .is_le() - && fetch_range - .end - .cmp(&cached_hint.position, buffer_snapshot) - .is_ge() - }) .map(|(cached_inlay_id, _)| *cached_inlay_id), ); } @@ -2009,12 +1986,12 @@ mod tests { assert_eq!(query_range.start, lsp::Position::new(selection_in_cached_range.row - expected_increment, 0)); assert_eq!(query_range.end, lsp::Position::new(selection_in_cached_range.row + expected_increment, 0)); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "Should query for hints after the scroll and again after the edit"); - let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4, "Should query for hints once after the edit"); + let expected_layers = vec!["4".to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor), "Should have hints from the new LSP response after the edit"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 3, "Should update the cache for every LSP response with hints added"); + assert_eq!(editor.inlay_hint_cache().version, 4, "Should update the cache for every LSP response with hints added"); }); } From e0d011e35475b84bb2af1457bb0c6fdc841f6d52 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 20:12:35 +0300 Subject: [PATCH 10/12] Better assert multibuffer edit test results --- crates/editor/src/inlay_hint_cache.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d6b45629a4..8be72aec46 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1901,6 +1901,8 @@ mod tests { }); let visible_range_after_scrolls = editor_visible_range(&editor, cx); + let visible_line_count = + editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); cx.foreground().run_until_parked(); let selection_in_cached_range = editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges @@ -1923,12 +1925,11 @@ mod tests { first_scroll.start, expected_initial_query_range_end, "First scroll should start the query right after the end of the original scroll", ); - let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; assert_eq!( second_scroll.end, lsp::Position::new( visible_range_after_scrolls.end.row - + expected_increment, + + visible_line_count.ceil() as u32, 0 ), "Second scroll should query one more screen down after the end of the visible range" @@ -1953,12 +1954,12 @@ mod tests { ); let mut selection_in_cached_range = visible_range_after_scrolls.end; - selection_in_cached_range.row -= expected_increment; + 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::Next), cx, |s| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { s.select_ranges([selection_in_cached_range..selection_in_cached_range]) }); }); @@ -1979,12 +1980,17 @@ mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; assert_eq!(ranges.len(), 1, "On edit, should scroll to selection and query a range around it. Instead, got query ranges {ranges:?}"); let query_range = &ranges[0]; - assert_eq!(query_range.start, lsp::Position::new(selection_in_cached_range.row - expected_increment, 0)); - assert_eq!(query_range.end, lsp::Position::new(selection_in_cached_range.row + expected_increment, 0)); + assert!(query_range.start.line < selection_in_cached_range.row, + "Hints should be queried with the selected range after the query range start"); + assert!(query_range.end.line > selection_in_cached_range.row, + "Hints should be queried with the selected range before the query range end"); + assert!(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!(query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen after"); assert_eq!(lsp_request_count.load(Ordering::Acquire), 4, "Should query for hints once after the edit"); let expected_layers = vec!["4".to_string()]; From 27bf01c3a82efe050d43358c6c35e7f1e038595f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 22:42:22 +0300 Subject: [PATCH 11/12] Strip off inlay hints data that should be resolved --- crates/lsp/src/lsp.rs | 4 ++- crates/project/src/lsp_command.rs | 56 ++++++------------------------- 2 files changed, 14 insertions(+), 46 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 78c858a90c..e0ae64d806 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,7 +434,9 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: None, + resolve_support: Some(InlayHintResolveClientCapabilities { + properties: vec!["textEdits".to_string(), "tooltip".to_string()], + }), dynamic_registration: Some(false), }), ..Default::default() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 08261b64f1..a8692257d8 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1954,7 +1954,7 @@ impl LspCommand for InlayHints { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - cx: &mut AppContext, + _: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response @@ -1963,51 +1963,17 @@ impl LspCommand for InlayHints { position: Some(language::proto::serialize_anchor(&response_hint.position)), padding_left: response_hint.padding_left, padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() - }) - } - }), - }), kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }, - ) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } + // Do not pass extra data such as tooltips to clients: host can put tooltip data from the cache during resolution. + tooltip: None, + // Similarly, do not pass label parts to clients: host can return a detailed list during resolution. + label: Some(proto::InlayHintLabel { + label: Some(proto::inlay_hint_label::Label::Value( + match response_hint.label { + InlayHintLabel::String(s) => s, + InlayHintLabel::LabelParts(_) => response_hint.text(), + }, + )), }), }) .collect(), From e5eed29c72c0029ad1d635f34b76e9edf1213ea3 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 15 Aug 2023 03:06:43 -0700 Subject: [PATCH 12/12] Add components example Re-arrange generics on mouse event handler Add TypeTag struct for dynamically tagged components --- .../src/activity_indicator.rs | 2 +- crates/ai/src/assistant.rs | 18 +- crates/auto_update/src/update_notification.rs | 4 +- crates/breadcrumbs/src/breadcrumbs.rs | 2 +- crates/collab_ui/src/collab_titlebar_item.rs | 30 +- crates/collab_ui/src/contact_list.rs | 20 +- crates/collab_ui/src/contacts_popover.rs | 2 +- .../src/incoming_call_notification.rs | 4 +- crates/collab_ui/src/notifications.rs | 4 +- .../src/project_shared_notification.rs | 4 +- .../collab_ui/src/sharing_status_indicator.rs | 2 +- crates/context_menu/src/context_menu.rs | 4 +- crates/copilot/src/sign_in.rs | 2 +- crates/copilot_button/src/copilot_button.rs | 2 +- crates/diagnostics/src/items.rs | 4 +- crates/drag_and_drop/src/drag_and_drop.rs | 6 +- crates/editor/src/editor.rs | 10 +- crates/editor/src/element.rs | 2 +- crates/editor/src/hover_popover.rs | 4 +- crates/feedback/src/deploy_feedback_button.rs | 2 +- crates/feedback/src/feedback_info_text.rs | 2 +- crates/feedback/src/submit_feedback_button.rs | 2 +- crates/gpui/examples/components.rs | 335 ++++++++++++++++++ crates/gpui/src/app.rs | 38 +- crates/gpui/src/elements.rs | 4 +- crates/gpui/src/elements/container.rs | 9 + .../gpui/src/elements/mouse_event_handler.rs | 63 +++- crates/gpui/src/elements/tooltip.rs | 2 +- crates/gpui/src/fonts.rs | 7 + crates/gpui/src/platform.rs | 11 + crates/gpui/src/scene/mouse_region.rs | 46 +-- crates/gpui/src/views/select.rs | 4 +- .../src/active_buffer_language.rs | 2 +- crates/language_tools/src/lsp_log.rs | 8 +- crates/language_tools/src/syntax_tree_view.rs | 8 +- crates/picker/src/picker.rs | 2 +- crates/project_panel/src/project_panel.rs | 6 +- crates/search/src/buffer_search.rs | 8 +- crates/search/src/project_search.rs | 8 +- crates/theme/src/ui.rs | 14 +- crates/vcs_menu/src/lib.rs | 2 +- crates/workspace/src/dock.rs | 3 +- crates/workspace/src/notifications.rs | 4 +- crates/workspace/src/pane.rs | 10 +- .../src/pane/dragged_item_receiver.rs | 4 +- crates/workspace/src/pane_group.rs | 2 +- crates/workspace/src/shared_screen.rs | 2 +- crates/workspace/src/toolbar.rs | 2 +- crates/workspace/src/workspace.rs | 4 +- 49 files changed, 585 insertions(+), 155 deletions(-) create mode 100644 crates/gpui/examples/components.rs diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 8b46d7cfc5..6d1db5ada5 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -318,7 +318,7 @@ impl View for ActivityIndicator { on_click, } = self.content_to_render(cx); - let mut element = MouseEventHandler::::new(0, cx, |state, cx| { + let mut element = MouseEventHandler::new::(0, cx, |state, cx| { let theme = &theme::current(cx).workspace.status_bar.lsp_status; let style = if state.hovered() && on_click.is_some() { theme.hovered.as_ref().unwrap_or(&theme.default) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 0a18266d2a..bb4e9f6db4 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -348,7 +348,7 @@ impl AssistantPanel { enum History {} let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.hamburger_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -380,7 +380,7 @@ impl AssistantPanel { fn render_split_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.split_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -404,7 +404,7 @@ impl AssistantPanel { fn render_assist_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.assist_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -422,7 +422,7 @@ impl AssistantPanel { fn render_quote_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.quote_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -450,7 +450,7 @@ impl AssistantPanel { fn render_plus_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.plus_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -480,7 +480,7 @@ impl AssistantPanel { &theme.assistant.zoom_in_button }; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -506,7 +506,7 @@ impl AssistantPanel { ) -> impl Element { let conversation = &self.saved_conversations[index]; let path = conversation.path.clone(); - MouseEventHandler::::new(index, cx, move |state, cx| { + MouseEventHandler::new::(index, cx, move |state, cx| { let style = &theme::current(cx).assistant.saved_conversation; Flex::row() .with_child( @@ -1818,7 +1818,7 @@ impl ConversationEditor { let theme = theme::current(cx); let style = &theme.assistant; let message_id = message.id; - let sender = MouseEventHandler::::new( + let sender = MouseEventHandler::new::( message_id.0, cx, |state, _| match message.role { @@ -2044,7 +2044,7 @@ impl ConversationEditor { ) -> impl Element { enum Model {} - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { let style = style.model.style_for(state); Label::new(self.conversation.read(cx).model.clone(), style.text.clone()) .contained() diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index cd2e53905d..8397fa0745 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -31,7 +31,7 @@ impl View for UpdateNotification { let app_name = cx.global::().display_name(); - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { Flex::column() .with_child( Flex::row() @@ -48,7 +48,7 @@ impl View for UpdateNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 433dbed29b..615e238648 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -82,7 +82,7 @@ impl View for Breadcrumbs { .into_any(); } - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); crumbs.with_style(style.container) }) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index fefb1c608f..bda11796e0 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -226,7 +226,7 @@ impl CollabTitlebarItem { let mut ret = Flex::row().with_child( Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |mouse_state, cx| { + MouseEventHandler::new::(0, cx, |mouse_state, cx| { let style = project_style .in_state(self.project_popover.is_some()) .style_for(mouse_state); @@ -266,7 +266,7 @@ impl CollabTitlebarItem { .with_child( Stack::new() .with_child( - MouseEventHandler::::new( + MouseEventHandler::new::( 0, cx, |mouse_state, cx| { @@ -398,7 +398,7 @@ impl CollabTitlebarItem { self.branch_popover.as_ref().map(|child| { let theme = theme::current(cx).clone(); let child = ChildView::new(child, cx); - let child = MouseEventHandler::::new(0, cx, |_, _| { + let child = MouseEventHandler::new::(0, cx, |_, _| { child .flex(1., true) .contained() @@ -433,7 +433,7 @@ impl CollabTitlebarItem { self.project_popover.as_ref().map(|child| { let theme = theme::current(cx).clone(); let child = ChildView::new(child, cx); - let child = MouseEventHandler::::new(0, cx, |_, _| { + let child = MouseEventHandler::new::(0, cx, |_, _| { child .flex(1., true) .contained() @@ -560,7 +560,7 @@ impl CollabTitlebarItem { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .toggle_contacts_button .in_state(self.contacts_popover.is_some()) @@ -610,7 +610,7 @@ impl CollabTitlebarItem { let active = room.read(cx).is_screen_sharing(); let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .screen_share_button .in_state(active) @@ -659,7 +659,7 @@ impl CollabTitlebarItem { } let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .toggle_microphone_button .in_state(is_muted) @@ -712,7 +712,7 @@ impl CollabTitlebarItem { } let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .toggle_speakers_button .in_state(is_deafened) @@ -747,7 +747,7 @@ impl CollabTitlebarItem { let tooltip = "Leave call"; let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar.leave_call_button.style_for(state); Svg::new(icon) .with_color(style.color) @@ -801,7 +801,7 @@ impl CollabTitlebarItem { Some( Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { //TODO: Ensure this button has consistent width for both text variations let style = titlebar.share_button.inactive_state().style_for(state); Label::new(label, style.text.clone()) @@ -847,7 +847,7 @@ impl CollabTitlebarItem { let avatar_style = &user_menu_button_style.avatar; Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = user_menu_button_style .user_menu .inactive_state() @@ -907,7 +907,7 @@ impl CollabTitlebarItem { fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar.sign_in_button.inactive_state().style_for(state); Label::new("Sign In", style.text.clone()) .contained() @@ -1142,7 +1142,7 @@ impl CollabTitlebarItem { if let Some(replica_id) = replica_id { enum ToggleFollow {} - content = MouseEventHandler::::new( + content = MouseEventHandler::new::( replica_id.into(), cx, move |_, _| content, @@ -1173,7 +1173,7 @@ impl CollabTitlebarItem { enum JoinProject {} let user_id = user.id; - content = MouseEventHandler::::new( + content = MouseEventHandler::new::( peer_id.as_u64() as usize, cx, move |_, _| content, @@ -1261,7 +1261,7 @@ impl CollabTitlebarItem { .into_any(), ), client::Status::UpgradeRequired => Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Label::new( "Please update Zed to collaborate", theme.titlebar.outdated_warning.text.clone(), diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index b8024e2bfd..83f3bd97b2 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -810,7 +810,7 @@ impl ContactList { worktree_root_names.join(", ") }; - MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -904,7 +904,7 @@ impl ContactList { let baseline_offset = row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - MouseEventHandler::::new( + MouseEventHandler::new::( peer_id.as_u64() as usize, cx, |mouse_state, _| { @@ -1006,7 +1006,7 @@ impl ContactList { }; let leave_call = if section == Section::ActiveCall { Some( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.leave_call.style_for(state); Label::new("Leave Call", style.text.clone()) .contained() @@ -1024,7 +1024,7 @@ impl ContactList { }; let icon_size = theme.section_icon_size; - MouseEventHandler::::new(section as usize, cx, |_, _| { + MouseEventHandler::new::(section as usize, cx, |_, _| { Flex::row() .with_child( Svg::new(if is_collapsed { @@ -1075,7 +1075,7 @@ impl ContactList { let github_login = contact.user.github_login.clone(); let initial_project = project.clone(); let mut event_handler = - MouseEventHandler::::new(contact.user.id as usize, cx, |_, cx| { + MouseEventHandler::new::(contact.user.id as usize, cx, |_, cx| { Flex::row() .with_children(contact.user.avatar.clone().map(|avatar| { let status_badge = if contact.online { @@ -1114,7 +1114,7 @@ impl ContactList { .flex(1., true), ) .with_child( - MouseEventHandler::::new( + MouseEventHandler::new::( contact.user.id as usize, cx, |mouse_state, _| { @@ -1208,7 +1208,7 @@ impl ContactList { if is_incoming { row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_button } else { @@ -1231,7 +1231,7 @@ impl ContactList { ); row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_button } else { @@ -1254,7 +1254,7 @@ impl ContactList { ); } else { row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_button } else { @@ -1333,7 +1333,7 @@ impl View for ContactList { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { render_icon_button( &theme.contact_list.add_contact_button, "icons/user_plus_16.svg", diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 1d6d1c84c7..39ab9c621c 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -113,7 +113,7 @@ impl View for ContactsPopover { Child::ContactFinder(child) => ChildView::new(child, cx), }; - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Flex::column() .with_child(child.flex(1., true)) .contained() diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 6f86a74300..410adbf862 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -173,7 +173,7 @@ impl IncomingCallNotification { let theme = theme::current(cx); Flex::column() .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.incoming_call_notification; Label::new("Accept", theme.accept_button.text.clone()) .aligned() @@ -187,7 +187,7 @@ impl IncomingCallNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.incoming_call_notification; Label::new("Decline", theme.decline_button.text.clone()) .aligned() diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index cbd072fe89..9258ad3ab1 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -52,7 +52,7 @@ where .flex(1., true), ) .with_child( - MouseEventHandler::::new(user.id as usize, cx, |state, _| { + MouseEventHandler::new::(user.id as usize, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) @@ -92,7 +92,7 @@ where Flex::row() .with_children(buttons.into_iter().enumerate().map( |(ix, (message, handler))| { - MouseEventHandler::::new(ix, cx, |state, _| { + MouseEventHandler::new::(ix, cx, |state, _| { let button = theme.button.style_for(state); Label::new(message, button.text.clone()) .contained() diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 63922f2b65..500599db59 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -170,7 +170,7 @@ impl ProjectSharedNotification { let theme = theme::current(cx); Flex::column() .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.project_shared_notification; Label::new("Open", theme.open_button.text.clone()) .aligned() @@ -182,7 +182,7 @@ impl ProjectSharedNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.project_shared_notification; Label::new("Dismiss", theme.dismiss_button.text.clone()) .aligned() diff --git a/crates/collab_ui/src/sharing_status_indicator.rs b/crates/collab_ui/src/sharing_status_indicator.rs index a39ffc457a..9fcd15aa18 100644 --- a/crates/collab_ui/src/sharing_status_indicator.rs +++ b/crates/collab_ui/src/sharing_status_indicator.rs @@ -47,7 +47,7 @@ impl View for SharingStatusIndicator { Appearance::Dark | Appearance::VibrantDark => Color::white(), }; - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Svg::new("icons/disable_screen_sharing_12.svg") .with_color(color) .constrained() diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index a5534b6262..89df86beef 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -439,14 +439,14 @@ impl ContextMenu { let style = theme::current(cx).context_menu.clone(); - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() .with_children(self.items.iter().enumerate().map(|(ix, item)| { match item { ContextMenuItem::Item { label, action } => { let action = action.clone(); let view_id = self.parent_view_id; - MouseEventHandler::::new(ix, cx, |state, _| { + MouseEventHandler::new::(ix, cx, |state, _| { let style = style.item.in_state(self.selected_index == Some(ix)); let style = style.style_for(state); let keystroke = match &action { diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index d03a2d393b..ac3b81f0c6 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -113,7 +113,7 @@ impl CopilotCodeVerification { let device_code_style = &style.auth.prompting.device_code; - MouseEventHandler::::new(0, cx, |state, _cx| { + MouseEventHandler::new::(0, cx, |state, _cx| { Flex::row() .with_child( Label::new(data.user_code.clone(), device_code_style.text.clone()) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index eae1746a01..f73f854927 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -62,7 +62,7 @@ impl View for CopilotButton { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, { + MouseEventHandler::new::(0, cx, { let theme = theme.clone(); let status = status.clone(); move |state, _cx| { diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 0ae55e99d9..d1a32c72f1 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -94,7 +94,7 @@ impl View for DiagnosticIndicator { let tooltip_style = theme::current(cx).tooltip.clone(); let in_progress = !self.in_progress_checks.is_empty(); let mut element = Flex::row().with_child( - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { let theme = theme::current(cx); let style = theme .workspace @@ -195,7 +195,7 @@ impl View for DiagnosticIndicator { } else if let Some(diagnostic) = &self.current_diagnostic { let message_style = style.diagnostic_message.clone(); element.add_child( - MouseEventHandler::::new(1, cx, |state, _| { + MouseEventHandler::new::(1, cx, |state, _| { Label::new( diagnostic.message.split('\n').next().unwrap().to_string(), message_style.style_for(state).text.clone(), diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index ddfed0c858..59b0bc89e2 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -202,7 +202,7 @@ impl DragAndDrop { let position = (position - region_offset).round(); Some( Overlay::new( - MouseEventHandler::::new( + MouseEventHandler::new::( 0, cx, |_, cx| render(payload, cx), @@ -235,7 +235,7 @@ impl DragAndDrop { } State::Canceled => Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Empty::new().constrained().with_width(0.).with_height(0.) }) .on_up(MouseButton::Left, |_, _, cx| { @@ -301,7 +301,7 @@ pub trait Draggable { Self: Sized; } -impl Draggable for MouseEventHandler { +impl Draggable for MouseEventHandler { fn as_draggable( self, payload: P, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d8b77ea95..d9bccdfea3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -867,7 +867,7 @@ impl CompletionsMenu { let completion = &completions[mat.candidate_id]; let item_ix = start_ix + ix; items.push( - MouseEventHandler::::new( + MouseEventHandler::new::( mat.candidate_id, cx, |state, _| { @@ -1044,7 +1044,7 @@ impl CodeActionsMenu { for (ix, action) in actions[range].iter().enumerate() { let item_ix = start_ix + ix; items.push( - MouseEventHandler::::new(item_ix, cx, |state, _| { + MouseEventHandler::new::(item_ix, cx, |state, _| { let item_style = if item_ix == selected_item { style.autocomplete.selected_item } else if state.hovered() { @@ -3547,7 +3547,7 @@ impl Editor { if self.available_code_actions.is_some() { enum CodeActions {} Some( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { Svg::new("icons/bolt_8.svg").with_color( style .code_actions @@ -3594,7 +3594,7 @@ impl Editor { fold_data .map(|(fold_status, buffer_row, active)| { (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { - MouseEventHandler::::new( + MouseEventHandler::new::( ix as usize, cx, |mouse_state, _| { @@ -8663,7 +8663,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); let anchor_x = cx.anchor_x; enum BlockContextToolip {} - MouseEventHandler::::new(cx.block_id, cx, |_, _| { + MouseEventHandler::new::(cx.block_id, cx, |_, _| { Flex::column() .with_children(highlighted_lines.iter().map(|(line, highlights)| { Label::new( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 13856cc8ef..0e9938cf47 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1637,7 +1637,7 @@ impl EditorElement { let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); enum JumpIcon {} - MouseEventHandler::::new((*id).into(), cx, |state, _| { + MouseEventHandler::new::((*id).into(), cx, |state, _| { let style = style.jump_icon.style_for(state); Svg::new("icons/arrow_up_right_8.svg") .with_color(style.color) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index afc13f983d..e4509a765c 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -565,7 +565,7 @@ impl InfoPopover { ) }); - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let mut region_id = 0; let view_id = cx.view_id(); @@ -654,7 +654,7 @@ impl DiagnosticPopover { let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { text.with_soft_wrap(true) .contained() .with_style(container_style) diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index d197f57fa5..ad2a40b60c 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -35,7 +35,7 @@ impl View for DeployFeedbackButton { let theme = theme::current(cx).clone(); Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = &theme .workspace .status_bar diff --git a/crates/feedback/src/feedback_info_text.rs b/crates/feedback/src/feedback_info_text.rs index 6c55b7a713..91ff22e904 100644 --- a/crates/feedback/src/feedback_info_text.rs +++ b/crates/feedback/src/feedback_info_text.rs @@ -41,7 +41,7 @@ impl View for FeedbackInfoText { .aligned(), ) .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let contained_text = if state.hovered() { &theme.feedback.link_text_hover } else { diff --git a/crates/feedback/src/submit_feedback_button.rs b/crates/feedback/src/submit_feedback_button.rs index 2133296e25..df59cf143f 100644 --- a/crates/feedback/src/submit_feedback_button.rs +++ b/crates/feedback/src/submit_feedback_button.rs @@ -52,7 +52,7 @@ impl View for SubmitFeedbackButton { .map_or(true, |i| i.read(cx).allow_submission); enum SubmitFeedbackButton {} - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let text; let style = if allow_submission { text = "Submit as Markdown"; diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs new file mode 100644 index 0000000000..50ace5eee0 --- /dev/null +++ b/crates/gpui/examples/components.rs @@ -0,0 +1,335 @@ +use button_component::Button; + +use component::AdaptComponent; +use gpui::{ + color::Color, + elements::{ContainerStyle, Flex, Label, ParentElement}, + fonts::{self, TextStyle}, + platform::WindowOptions, + AnyElement, App, Element, Entity, View, ViewContext, +}; +use log::LevelFilter; +use pathfinder_geometry::vector::vec2f; +use simplelog::SimpleLogger; +use theme::Toggleable; +use toggleable_button::ToggleableButton; + +fn main() { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + + App::new(()).unwrap().run(|cx| { + cx.platform().activate(true); + cx.add_window(WindowOptions::with_bounds(vec2f(300., 200.)), |_| { + TestView { + count: 0, + is_doubling: false, + } + }); + }); +} + +pub struct TestView { + count: usize, + is_doubling: bool, +} + +impl TestView { + fn increase_count(&mut self) { + if self.is_doubling { + self.count *= 2; + } else { + self.count += 1; + } + } +} + +impl Entity for TestView { + type Event = (); +} + +type ButtonStyle = ContainerStyle; + +impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement { + fonts::with_font_cache(cx.font_cache.to_owned(), || { + Flex::column() + .with_child(Label::new( + format!("Count: {}", self.count), + TextStyle::for_color(Color::red()), + )) + .with_child( + Button::new(move |_, v: &mut Self, cx| { + v.increase_count(); + cx.notify(); + }) + .with_text( + "Hello from a counting BUTTON", + TextStyle::for_color(Color::blue()), + ) + .with_style(ButtonStyle::fill(Color::yellow())) + .into_element(), + ) + .with_child( + ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { + v.is_doubling = !v.is_doubling; + cx.notify(); + }) + .with_text("Double the count?", TextStyle::for_color(Color::black())) + .with_style(Toggleable { + inactive: ButtonStyle::fill(Color::red()), + active: ButtonStyle::fill(Color::green()), + }) + .into_element(), + ) + .expanded() + .contained() + .with_background_color(Color::white()) + .into_any() + }) + } +} + +mod theme { + pub struct Toggleable { + pub inactive: T, + pub active: T, + } + + impl Toggleable { + pub fn style_for(&self, active: bool) -> &T { + if active { + &self.active + } else { + &self.inactive + } + } + } +} + +// Component creation: +mod toggleable_button { + use gpui::{ + elements::{ContainerStyle, LabelStyle}, + scene::MouseClick, + EventContext, View, + }; + + use crate::{button_component::Button, component::Component, theme::Toggleable}; + + pub struct ToggleableButton { + active: bool, + style: Option>, + button: Button, + } + + impl ToggleableButton { + pub fn new(active: bool, on_click: F) -> Self + where + F: Fn(MouseClick, &mut V, &mut EventContext) + 'static, + { + Self { + active, + button: Button::new(on_click), + style: None, + } + } + + pub fn with_text(self, text: &str, style: impl Into) -> ToggleableButton { + ToggleableButton { + active: self.active, + style: self.style, + button: self.button.with_text(text, style), + } + } + + pub fn with_style(self, style: Toggleable) -> ToggleableButton { + ToggleableButton { + active: self.active, + style: Some(style), + button: self.button, + } + } + } + + impl Component for ToggleableButton { + type View = V; + + fn render( + self, + v: &mut Self::View, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + let button = if let Some(style) = self.style { + self.button.with_style(*style.style_for(self.active)) + } else { + self.button + }; + button.render(v, cx) + } + } +} + +mod button_component { + + use gpui::{ + elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler}, + platform::MouseButton, + scene::MouseClick, + AnyElement, Element, EventContext, TypeTag, View, ViewContext, + }; + + use crate::component::Component; + + type ClickHandler = Box)>; + + pub struct Button { + click_handler: ClickHandler, + tag: TypeTag, + contents: Option>, + style: Option, + } + + impl Button { + pub fn new) + 'static>(handler: F) -> Self { + Self { + click_handler: Box::new(handler), + tag: TypeTag::new::(), + style: None, + contents: None, + } + } + + pub fn with_text(mut self, text: &str, style: impl Into) -> Self { + self.contents = Some(Label::new(text.to_string(), style).into_any()); + self + } + + pub fn _with_contents>(mut self, contents: E) -> Self { + self.contents = Some(contents.into_any()); + self + } + + pub fn with_style(mut self, style: ContainerStyle) -> Self { + self.style = Some(style); + self + } + } + + impl Component for Button { + type View = V; + + fn render(self, _: &mut Self::View, cx: &mut ViewContext) -> AnyElement { + let click_handler = self.click_handler; + + let result = MouseEventHandler::new_dynamic(self.tag, 0, cx, |_, _| { + self.contents + .unwrap_or_else(|| gpui::elements::Empty::new().into_any()) + }) + .on_click(MouseButton::Left, move |click, v, cx| { + click_handler(click, v, cx); + }) + .contained(); + + let result = if let Some(style) = self.style { + result.with_style(style) + } else { + result + }; + + result.into_any() + } + } +} + +mod component { + + use gpui::{AnyElement, Element, View, ViewContext}; + use pathfinder_geometry::vector::Vector2F; + + // Public API: + pub trait Component { + type View: View; + + fn render( + self, + v: &mut Self::View, + cx: &mut ViewContext, + ) -> AnyElement; + } + + pub struct ComponentAdapter { + component: Option, + } + + impl ComponentAdapter { + pub fn new(e: E) -> Self { + Self { component: Some(e) } + } + } + + pub trait AdaptComponent: Sized { + fn into_element(self) -> ComponentAdapter { + ComponentAdapter::new(self) + } + } + + impl AdaptComponent for C {} + + impl Element for ComponentAdapter { + type LayoutState = AnyElement; + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + view: &mut C::View, + cx: &mut gpui::LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let component = self.component.take().unwrap(); + let mut element = component.render(view, cx.view_context()); + let constraint = element.layout(constraint, view, cx); + (constraint, element) + } + + fn paint( + &mut self, + scene: &mut gpui::SceneBuilder, + bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + view: &mut C::View, + cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + layout.paint(scene, bounds.origin(), visible_bounds, view, cx) + } + + fn rect_for_text_range( + &self, + _: std::ops::Range, + _: gpui::geometry::rect::RectF, + _: gpui::geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &C::View, + _: &ViewContext, + ) -> Option { + todo!() + } + + fn debug( + &self, + _: gpui::geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &C::View, + _: &ViewContext, + ) -> serde_json::Value { + todo!() + } + } +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2a9d9f4768..8e6d43a45d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3280,7 +3280,11 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } pub fn mouse_state(&self, region_id: usize) -> MouseState { - let region_id = MouseRegionId::new::(self.view_id, region_id); + self.mouse_state_dynamic(TypeTag::new::(), region_id) + } + + pub fn mouse_state_dynamic(&self, tag: TypeTag, region_id: usize) -> MouseState { + let region_id = MouseRegionId::new(tag, self.view_id, region_id); MouseState { hovered: self.window.hovered_region_ids.contains(®ion_id), clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region { @@ -3321,6 +3325,36 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct TypeTag { + tag: TypeId, + #[cfg(debug_assertions)] + tag_type_name: &'static str, +} + +impl TypeTag { + pub fn new() -> Self { + Self { + tag: TypeId::of::(), + #[cfg(debug_assertions)] + tag_type_name: std::any::type_name::(), + } + } + + pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self { + Self { + tag, + #[cfg(debug_assertions)] + tag_type_name: type_name, + } + } + + #[cfg(debug_assertions)] + pub(crate) fn type_name(&self) -> &'static str { + self.tag_type_name + } +} + impl BorrowAppContext for ViewContext<'_, '_, V> { fn read_with T>(&self, f: F) -> T { BorrowAppContext::read_with(&*self.window_context, f) @@ -5171,7 +5205,7 @@ mod tests { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { enum Handler {} let mouse_down_count = self.mouse_down_count.clone(); - MouseEventHandler::::new(0, cx, |_, _| Empty::new()) + MouseEventHandler::new::(0, cx, |_, _| Empty::new()) .on_down(MouseButton::Left, move |_, _, _| { mouse_down_count.fetch_add(1, SeqCst); }) diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 56a712802b..16c750ea8e 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -193,11 +193,11 @@ pub trait Element: 'static { Resizable::new(self.into_any(), side, size, on_resize) } - fn mouse(self, region_id: usize) -> MouseEventHandler + fn mouse(self, region_id: usize) -> MouseEventHandler where Self: Sized, { - MouseEventHandler::for_child(self.into_any(), region_id) + MouseEventHandler::for_child::(self.into_any(), region_id) } } diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 698100ab29..bb1366b4e7 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -38,6 +38,15 @@ pub struct ContainerStyle { pub cursor: Option, } +impl ContainerStyle { + pub fn fill(color: Color) -> Self { + Self { + background_color: Some(color), + ..Default::default() + } + } +} + pub struct Container { child: AnyElement, style: ContainerStyle, diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 6005277f73..2ed0f1720f 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -11,12 +11,12 @@ use crate::{ MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, }, AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext, - SceneBuilder, SizeConstraint, View, ViewContext, + SceneBuilder, SizeConstraint, TypeTag, View, ViewContext, }; use serde_json::json; -use std::{marker::PhantomData, ops::Range}; +use std::ops::Range; -pub struct MouseEventHandler { +pub struct MouseEventHandler { child: AnyElement, region_id: usize, cursor_style: Option, @@ -26,13 +26,13 @@ pub struct MouseEventHandler { notify_on_click: bool, above: bool, padding: Padding, - _tag: PhantomData, + tag: TypeTag, } /// Element which provides a render_child callback with a MouseState and paints a mouse /// region under (or above) it for easy mouse event handling. -impl MouseEventHandler { - pub fn for_child(child: impl Element, region_id: usize) -> Self { +impl MouseEventHandler { + pub fn for_child(child: impl Element, region_id: usize) -> Self { Self { child: child.into_any(), region_id, @@ -43,16 +43,19 @@ impl MouseEventHandler { hoverable: false, above: false, padding: Default::default(), - _tag: PhantomData, + tag: TypeTag::new::(), } } - pub fn new(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self + pub fn new( + region_id: usize, + cx: &mut ViewContext, + render_child: impl FnOnce(&mut MouseState, &mut ViewContext) -> E, + ) -> Self where E: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> E, { - let mut mouse_state = cx.mouse_state::(region_id); + let mut mouse_state = cx.mouse_state_dynamic(TypeTag::new::(), region_id); let child = render_child(&mut mouse_state, cx).into_any(); let notify_on_hover = mouse_state.accessed_hovered(); let notify_on_click = mouse_state.accessed_clicked(); @@ -66,19 +69,46 @@ impl MouseEventHandler { hoverable: true, above: false, padding: Default::default(), - _tag: PhantomData, + tag: TypeTag::new::(), + } + } + + pub fn new_dynamic( + tag: TypeTag, + region_id: usize, + cx: &mut ViewContext, + render_child: impl FnOnce(&mut MouseState, &mut ViewContext) -> AnyElement, + ) -> Self { + let mut mouse_state = cx.mouse_state_dynamic(tag, region_id); + let child = render_child(&mut mouse_state, cx); + let notify_on_hover = mouse_state.accessed_hovered(); + let notify_on_click = mouse_state.accessed_clicked(); + Self { + child, + region_id, + cursor_style: None, + handlers: Default::default(), + notify_on_hover, + notify_on_click, + hoverable: true, + above: false, + padding: Default::default(), + tag, } } /// Modifies the MouseEventHandler to render the MouseRegion above the child element. Useful /// for drag and drop handling and similar events which should be captured before the child /// gets the opportunity - pub fn above(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self + pub fn above( + region_id: usize, + cx: &mut ViewContext, + render_child: impl FnOnce(&mut MouseState, &mut ViewContext) -> D, + ) -> Self where D: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> D, { - let mut handler = Self::new(region_id, cx, render_child); + let mut handler = Self::new::(region_id, cx, render_child); handler.above = true; handler } @@ -223,7 +253,8 @@ impl MouseEventHandler { }); } scene.push_mouse_region( - MouseRegion::from_handlers::( + MouseRegion::from_handlers( + self.tag, cx.view_id(), self.region_id, hit_bounds, @@ -236,7 +267,7 @@ impl MouseEventHandler { } } -impl Element for MouseEventHandler { +impl Element for MouseEventHandler { type LayoutState = (); type PaintState = (); diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 14f3809e67..0ba0110303 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -95,7 +95,7 @@ impl Tooltip { } else { None }; - let child = MouseEventHandler::, _>::new(id, cx, |_, _| child) + let child = MouseEventHandler::new::, _>(id, cx, |_, _| child) .on_hover(move |e, _, cx| { let position = e.position; if e.started { diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index b003042866..8858e1a316 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -72,6 +72,13 @@ pub struct TextStyle { } impl TextStyle { + pub fn for_color(color: Color) -> Self { + Self { + color, + ..Default::default() + } + } + pub fn refine(self, refinement: TextStyleRefinement) -> TextStyle { TextStyle { color: refinement.color.unwrap_or(self.color), diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 1d93a45fc7..9f6e303cb7 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -24,6 +24,7 @@ use crate::{ use anyhow::{anyhow, bail, Result}; use async_task::Runnable; pub use event::*; +use pathfinder_geometry::vector::vec2f; use postage::oneshot; use schemars::JsonSchema; use serde::Deserialize; @@ -180,6 +181,16 @@ pub struct WindowOptions<'a> { pub screen: Option>, } +impl<'a> WindowOptions<'a> { + pub fn with_bounds(bounds: Vector2F) -> Self { + Self { + bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), bounds)), + center: true, + ..Default::default() + } + } +} + #[derive(Debug)] pub struct TitlebarOptions<'a> { pub title: Option<&'a str>, diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index cf39ac782f..1632b494a3 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -1,13 +1,10 @@ -use crate::{platform::MouseButton, window::WindowContext, EventContext, View, ViewContext}; +use crate::{ + platform::MouseButton, window::WindowContext, EventContext, TypeTag, View, ViewContext, +}; use collections::HashMap; use pathfinder_geometry::rect::RectF; use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - fmt::Debug, - mem::Discriminant, - rc::Rc, -}; +use std::{any::Any, fmt::Debug, mem::Discriminant, rc::Rc}; use super::{ mouse_event::{ @@ -33,14 +30,27 @@ impl MouseRegion { /// should pass a different (consistent) region_id. If you have one big region that covers your /// whole component, just pass the view_id again. pub fn new(view_id: usize, region_id: usize, bounds: RectF) -> Self { - Self::from_handlers::(view_id, region_id, bounds, Default::default()) + Self::from_handlers( + TypeTag::new::(), + view_id, + region_id, + bounds, + Default::default(), + ) } pub fn handle_all(view_id: usize, region_id: usize, bounds: RectF) -> Self { - Self::from_handlers::(view_id, region_id, bounds, HandlerSet::capture_all()) + Self::from_handlers( + TypeTag::new::(), + view_id, + region_id, + bounds, + HandlerSet::capture_all(), + ) } - pub fn from_handlers( + pub fn from_handlers( + tag: TypeTag, view_id: usize, region_id: usize, bounds: RectF, @@ -49,10 +59,8 @@ impl MouseRegion { Self { id: MouseRegionId { view_id, - tag: TypeId::of::(), + tag, region_id, - #[cfg(debug_assertions)] - tag_type_name: std::any::type_name::(), }, bounds, handlers, @@ -180,20 +188,16 @@ impl MouseRegion { #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] pub struct MouseRegionId { view_id: usize, - tag: TypeId, + tag: TypeTag, region_id: usize, - #[cfg(debug_assertions)] - tag_type_name: &'static str, } impl MouseRegionId { - pub(crate) fn new(view_id: usize, region_id: usize) -> Self { + pub(crate) fn new(tag: TypeTag, view_id: usize, region_id: usize) -> Self { MouseRegionId { view_id, region_id, - tag: TypeId::of::(), - #[cfg(debug_assertions)] - tag_type_name: std::any::type_name::(), + tag, } } @@ -203,7 +207,7 @@ impl MouseRegionId { #[cfg(debug_assertions)] pub fn tag_type_name(&self) -> &'static str { - self.tag_type_name + self.tag.type_name() } } diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index f3be9de3ec..f76fab738e 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -106,7 +106,7 @@ impl View for Select { Default::default() }; let mut result = Flex::column().with_child( - MouseEventHandler::::new(self.handle.id(), cx, |mouse_state, cx| { + MouseEventHandler::new::(self.handle.id(), cx, |mouse_state, cx| { (self.render_item)( self.selected_item_ix, ItemType::Header, @@ -130,7 +130,7 @@ impl View for Select { let selected_item_ix = this.selected_item_ix; range.end = range.end.min(this.item_count); items.extend(range.map(|ix| { - MouseEventHandler::::new(ix, cx, |mouse_state, cx| { + MouseEventHandler::new::(ix, cx, |mouse_state, cx| { (this.render_item)( ix, if ix == selected_item_ix { diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index b97417580f..5ffcb13fba 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -53,7 +53,7 @@ impl View for ActiveBufferLanguage { "Unknown".to_string() }; - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { let theme = &theme::current(cx).workspace.status_bar; let style = theme.active_language.style_for(state); Label::new(active_language_text, style.text.clone()) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 0dc594a13f..cc2bf37d4a 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -573,7 +573,7 @@ impl View for LspLogToolbarItemView { .with_children(if self.menu_open { Some( Overlay::new( - MouseEventHandler::::new(0, cx, move |_, cx| { + MouseEventHandler::new::(0, cx, move |_, cx| { Flex::column() .with_children(menu_rows.into_iter().map(|row| { Self::render_language_server_menu_item( @@ -672,7 +672,7 @@ impl LspLogToolbarItemView { cx: &mut ViewContext, ) -> impl Element { enum ToggleMenu {} - MouseEventHandler::::new(0, cx, move |state, cx| { + MouseEventHandler::new::(0, cx, move |state, cx| { let label: Cow = current_server .and_then(|row| { let worktree = row.worktree.read(cx); @@ -728,7 +728,7 @@ impl LspLogToolbarItemView { .with_height(theme.toolbar_dropdown_menu.row_height) }) .with_child( - MouseEventHandler::::new(id.0, cx, move |state, _| { + MouseEventHandler::new::(id.0, cx, move |state, _| { let style = theme .toolbar_dropdown_menu .item @@ -746,7 +746,7 @@ impl LspLogToolbarItemView { }), ) .with_child( - MouseEventHandler::::new(id.0, cx, move |state, cx| { + MouseEventHandler::new::(id.0, cx, move |state, cx| { let style = theme .toolbar_dropdown_menu .item diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 3e6727bbf4..60788d034e 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -389,7 +389,7 @@ impl View for SyntaxTreeView { { let layer = layer.clone(); let theme = editor_theme.clone(); - return MouseEventHandler::::new(0, cx, move |state, cx| { + return MouseEventHandler::new::(0, cx, move |state, cx| { let list_hovered = state.hovered(); UniformList::new( self.list_state.clone(), @@ -505,7 +505,7 @@ impl SyntaxTreeToolbarItemView { .with_child(Self::render_header(&theme, &active_layer, cx)) .with_children(self.menu_open.then(|| { Overlay::new( - MouseEventHandler::::new(0, cx, move |_, cx| { + MouseEventHandler::new::(0, cx, move |_, cx| { Flex::column() .with_children(active_buffer.syntax_layers().enumerate().map( |(ix, layer)| { @@ -564,7 +564,7 @@ impl SyntaxTreeToolbarItemView { cx: &mut ViewContext, ) -> impl Element { enum ToggleMenu {} - MouseEventHandler::::new(0, cx, move |state, _| { + MouseEventHandler::new::(0, cx, move |state, _| { let style = theme.toolbar_dropdown_menu.header.style_for(state); Flex::row() .with_child( @@ -596,7 +596,7 @@ impl SyntaxTreeToolbarItemView { cx: &mut ViewContext, ) -> impl Element { enum ActivateLayer {} - MouseEventHandler::::new(layer_ix, cx, move |state, _| { + MouseEventHandler::new::(layer_ix, cx, move |state, _| { let is_selected = layer.node() == active_layer.node(); let style = theme .toolbar_dropdown_menu diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 6efa33e961..a3b8672f9f 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -112,7 +112,7 @@ impl View for Picker { let selected_ix = this.delegate.selected_index(); range.end = cmp::min(range.end, this.delegate.match_count()); items.extend(range.map(move |ix| { - MouseEventHandler::::new(ix, cx, |state, cx| { + MouseEventHandler::new::(ix, cx, |state, cx| { this.delegate.render_match(ix, state, ix == selected_ix, cx) }) // Capture mouse events diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index f7582f1764..4acc539263 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1407,7 +1407,7 @@ impl ProjectPanel { let show_editor = details.is_editing && !details.is_processing; - MouseEventHandler::::new(entry_id.to_usize(), cx, |state, cx| { + MouseEventHandler::new::(entry_id.to_usize(), cx, |state, cx| { let mut style = entry_style .in_state(details.is_selected) .style_for(state) @@ -1519,7 +1519,7 @@ impl View for ProjectPanel { if has_worktree { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { UniformList::new( self.list.clone(), self.visible_entries @@ -1563,7 +1563,7 @@ impl View for ProjectPanel { } else { Flex::column() .with_child( - MouseEventHandler::::new(2, cx, { + MouseEventHandler::new::(2, cx, { let button_style = theme.open_project_button.clone(); let context_menu_item_style = theme::current(cx).context_menu.item.clone(); move |state, cx| { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c3b4f5caa6..36c9d3becd 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -416,7 +416,7 @@ impl BufferSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = self.search_options.contains(option); Some( - MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { + MouseEventHandler::new::(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -463,7 +463,7 @@ impl BufferSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); enum NavButton {} - MouseEventHandler::::new(direction as usize, cx, |state, cx| { + MouseEventHandler::new::(direction as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme.search.option_button.inactive_state().style_for(state); Label::new(icon, style.text.clone()) @@ -497,7 +497,7 @@ impl BufferSearchBar { let action_type_id = 0_usize; enum ActionButton {} - MouseEventHandler::::new(action_type_id, cx, |state, cx| { + MouseEventHandler::new::(action_type_id, cx, |state, cx| { let theme = theme::current(cx); let style = theme.search.action_button.style_for(state); Label::new(icon, style.text.clone()) @@ -527,7 +527,7 @@ impl BufferSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); enum CloseButton {} - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 018ab9cb11..8bc8d7da88 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -328,7 +328,7 @@ impl View for ProjectSearchView { editor.set_placeholder_text(new_placeholder_text, cx); }); - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Label::new(text, theme.search.results_status.clone()) .aligned() .contained() @@ -1103,7 +1103,7 @@ impl ProjectSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); enum NavButton {} - MouseEventHandler::::new(direction as usize, cx, |state, cx| { + MouseEventHandler::new::(direction as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme.search.option_button.inactive_state().style_for(state); Label::new(icon, style.text.clone()) @@ -1134,7 +1134,7 @@ impl ProjectSearchBar { ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = self.is_option_enabled(option, cx); - MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { + MouseEventHandler::new::(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -1170,7 +1170,7 @@ impl ProjectSearchBar { let region_id = 3; - MouseEventHandler::::new(region_id, cx, |state, cx| { + MouseEventHandler::new::(region_id, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index a16c3cb21e..81663ed6ca 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -34,7 +34,7 @@ pub fn checkbox( id: usize, cx: &mut ViewContext, change: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, V: View, @@ -43,7 +43,7 @@ where let label = Label::new(label, style.label.text.clone()) .contained() .with_style(style.label.container); - checkbox_with_label(label, style, checked, id, cx, change) + checkbox_with_label::(label, style, checked, id, cx, change) } pub fn checkbox_with_label( @@ -53,14 +53,14 @@ pub fn checkbox_with_label( id: usize, cx: &mut ViewContext, change: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, D: Element, V: View, F: 'static + Fn(&mut V, bool, &mut EventContext), { - MouseEventHandler::new(id, cx, |state, _| { + MouseEventHandler::new::(id, cx, |state, _| { let indicator = if checked { svg(&style.icon) } else { @@ -143,14 +143,14 @@ pub fn cta_button( style: &ButtonStyle, cx: &mut ViewContext, f: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, L: Into>, V: View, F: Fn(MouseClick, &mut V, &mut EventContext) + 'static, { - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); Label::new(label, style.text.to_owned()) .aligned() @@ -205,7 +205,7 @@ where )) .with_child( // FIXME: Get a better tag type - MouseEventHandler::::new(999999, cx, |state, _cx| { + MouseEventHandler::new::(999999, cx, |state, _cx| { let style = style.close_icon.style_for(state); icon(style) }) diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 384b622469..9009c4e3d3 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -295,7 +295,7 @@ impl PickerDelegate for BranchListDelegate { let style = theme.picker.footer.clone(); enum BranchCreateButton {} Some( - Flex::row().with_child(MouseEventHandler::::new(0, cx, |state, _| { + Flex::row().with_child(MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); Label::new("Create branch", style.label.clone()) .contained() diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index e33c0a5391..641eae081e 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -497,9 +497,8 @@ impl View for PanelButtons { }; Stack::new() .with_child( - MouseEventHandler::::new(panel_ix, cx, |state, cx| { + MouseEventHandler::new::(panel_ix, cx, |state, cx| { let style = button_style.in_state(is_active); - let style = style.style_for(state); Flex::row() .with_child( diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 09cfb4d5d8..55b44e9673 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -290,7 +290,7 @@ pub mod simple_message_notification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) @@ -319,7 +319,7 @@ pub mod simple_message_notification { .with_children({ click_message .map(|click_message| { - MouseEventHandler::::new( + MouseEventHandler::new::( 0, cx, |state, _| { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 06f13cd52d..4aca96374a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1211,7 +1211,7 @@ impl Pane { enum Tab {} let mouse_event_handler = - MouseEventHandler::::new(ix, cx, |_, cx| { + MouseEventHandler::new::(ix, cx, |_, cx| { Self::render_tab( &item, pane.clone(), @@ -1420,7 +1420,7 @@ impl Pane { let item_id = item.id(); enum TabCloseButton {} let icon = Svg::new("icons/x_mark_8.svg"); - MouseEventHandler::::new(item_id, cx, |mouse_state, _| { + MouseEventHandler::new::(item_id, cx, |mouse_state, _| { if mouse_state.hovered() { icon.with_color(tab_style.icon_close_active) } else { @@ -1485,7 +1485,7 @@ impl Pane { ) -> AnyElement { enum TabBarButton {} - let mut button = MouseEventHandler::::new(index, cx, |mouse_state, cx| { + let mut button = MouseEventHandler::new::(index, cx, |mouse_state, cx| { let theme = &settings::get::(cx).theme.workspace.tab_bar; let style = theme.pane_button.in_state(is_active).style_for(mouse_state); Svg::new(icon) @@ -1547,7 +1547,7 @@ impl View for Pane { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { enum MouseNavigationHandler {} - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let active_item_index = self.active_item_index; if let Some(active_item) = self.active_item() { @@ -1559,7 +1559,7 @@ impl View for Pane { enum TabBarEventHandler {} stack.add_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Empty::new() .contained() .with_style(theme.workspace.tab_bar.container) diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 165537a1af..1cbf3e4f50 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -19,7 +19,7 @@ pub fn dragged_item_receiver( split_margin: Option, cx: &mut ViewContext, render_child: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, D: Element, @@ -39,7 +39,7 @@ where None }; - let mut handler = MouseEventHandler::::above(region_id, cx, |state, cx| { + let mut handler = MouseEventHandler::above::(region_id, cx, |state, cx| { // Observing hovered will cause a render when the mouse enters regardless // of if mouse position was accessed before let drag_position = if state.hovered() { drag_position } else { None }; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index dfda5092ca..7528fb7468 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -212,7 +212,7 @@ impl Member { let leader_user_id = leader.user.id; let app_state = Arc::downgrade(app_state); Some( - MouseEventHandler::::new( + MouseEventHandler::new::( pane.id(), cx, |_, _| { diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 977e167f60..aea03df5e0 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -72,7 +72,7 @@ impl View for SharedScreen { enum Focus {} let frame = self.frame.clone(); - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { Canvas::new(move |scene, bounds, _, _, _| { if let Some(frame) = frame.clone() { let size = constrain_size_preserving_aspect_ratio( diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 945ac7b0f5..0516f3a145 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -223,7 +223,7 @@ fn nav_button action_name: &'static str, cx: &mut ViewContext, ) -> AnyElement { - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = if enabled { style.style_for(state) } else { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ab4f7286dc..a449c58de3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2560,7 +2560,7 @@ impl Workspace { }; enum TitleBar {} - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { Stack::new() .with_children( self.titlebar_item @@ -2649,7 +2649,7 @@ impl Workspace { if self.project.read(cx).is_read_only() { enum DisconnectedOverlay {} Some( - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let theme = &theme::current(cx); Label::new( "Your connection to the remote project has been lost.",