diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index bfea497e9b57d806af1f13bb3af7e88521d03816..a3f63c527693a19bb7ac1cd87c104cee3d5cfa6e 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -3425,16 +3425,16 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA assert_eq!( entries, vec![ - Some(blame_entry("1b1b1b", 0..1)), - Some(blame_entry("0d0d0d", 1..2)), - Some(blame_entry("3a3a3a", 2..3)), - Some(blame_entry("4c4c4c", 3..4)), + Some((buffer_id_b, blame_entry("1b1b1b", 0..1))), + Some((buffer_id_b, blame_entry("0d0d0d", 1..2))), + Some((buffer_id_b, blame_entry("3a3a3a", 2..3))), + Some((buffer_id_b, blame_entry("4c4c4c", 3..4))), ] ); blame.update(cx, |blame, _| { - for (idx, entry) in entries.iter().flatten().enumerate() { - let details = blame.details_for_entry(entry).unwrap(); + for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() { + let details = blame.details_for_entry(*buffer, entry).unwrap(); assert_eq!(details.message, format!("message for idx-{}", idx)); assert_eq!( details.permalink.unwrap().to_string(), @@ -3474,9 +3474,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA entries, vec![ None, - Some(blame_entry("0d0d0d", 1..2)), - Some(blame_entry("3a3a3a", 2..3)), - Some(blame_entry("4c4c4c", 3..4)), + Some((buffer_id_b, blame_entry("0d0d0d", 1..2))), + Some((buffer_id_b, blame_entry("3a3a3a", 2..3))), + Some((buffer_id_b, blame_entry("4c4c4c", 3..4))), ] ); }); @@ -3511,8 +3511,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA vec![ None, None, - Some(blame_entry("3a3a3a", 2..3)), - Some(blame_entry("4c4c4c", 3..4)), + Some((buffer_id_b, blame_entry("3a3a3a", 2..3))), + Some((buffer_id_b, blame_entry("4c4c4c", 3..4))), ] ); }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 00fe6637bf6c5ff8d8caa3c9c223fc55ffaee3cc..a8e2b001f330423e2108a22eb6f5113c3aa3e78b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -190,7 +190,6 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use sum_tree::TreeMap; use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; use text::{BufferId, FromAnchor, OffsetUtf16, Rope}; use theme::{ @@ -227,7 +226,7 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024; pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000); #[doc(hidden)] pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); -const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); +pub const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5); pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5); @@ -1060,8 +1059,8 @@ pub struct Editor { placeholder_text: Option>, highlight_order: usize, highlighted_rows: HashMap>, - background_highlights: TreeMap, - gutter_highlights: TreeMap, + background_highlights: HashMap, + gutter_highlights: HashMap, scrollbar_marker_state: ScrollbarMarkerState, active_indent_guides_state: ActiveIndentGuidesState, nav_history: Option, @@ -2112,8 +2111,8 @@ impl Editor { placeholder_text: None, highlight_order: 0, highlighted_rows: HashMap::default(), - background_highlights: TreeMap::default(), - gutter_highlights: TreeMap::default(), + background_highlights: HashMap::default(), + gutter_highlights: HashMap::default(), scrollbar_marker_state: ScrollbarMarkerState::default(), active_indent_guides_state: ActiveIndentGuidesState::default(), nav_history: None, @@ -6630,7 +6629,7 @@ impl Editor { buffer_row: Some(point.row), ..Default::default() }; - let Some(blame_entry) = blame + let Some((buffer, blame_entry)) = blame .update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next()) .flatten() else { @@ -6640,12 +6639,19 @@ impl Editor { let anchor = self.selections.newest_anchor().head(); let position = self.to_pixel_point(anchor, &snapshot, window); if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) { - self.show_blame_popover(&blame_entry, position + last_bounds.origin, true, cx); + self.show_blame_popover( + buffer, + &blame_entry, + position + last_bounds.origin, + true, + cx, + ); }; } fn show_blame_popover( &mut self, + buffer: BufferId, blame_entry: &BlameEntry, position: gpui::Point, ignore_timeout: bool, @@ -6669,7 +6675,7 @@ impl Editor { return; }; let blame = blame.read(cx); - let details = blame.details_for_entry(&blame_entry); + let details = blame.details_for_entry(buffer, &blame_entry); let markdown = cx.new(|cx| { Markdown::new( details @@ -19071,7 +19077,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); let cursor = self.selections.newest::(cx).head(); let (buffer, point, _) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)?; - let blame_entry = blame + let (_, blame_entry) = blame .update(cx, |blame, cx| { blame .blame_for_rows( @@ -19086,7 +19092,7 @@ impl Editor { }) .flatten()?; let renderer = cx.global::().0.clone(); - let repo = blame.read(cx).repository(cx)?; + let repo = blame.read(cx).repository(cx, buffer.remote_id())?; let workspace = self.workspace()?.downgrade(); renderer.open_blame_commit(blame_entry, repo, workspace, window, cx); None @@ -19122,18 +19128,17 @@ impl Editor { cx: &mut Context, ) { if let Some(project) = self.project() { - let Some(buffer) = self.buffer().read(cx).as_singleton() else { - return; - }; - - if buffer.read(cx).file().is_none() { + if let Some(buffer) = self.buffer().read(cx).as_singleton() + && buffer.read(cx).file().is_none() + { return; } let focused = self.focus_handle(cx).contains_focused(window, cx); let project = project.clone(); - let blame = cx.new(|cx| GitBlame::new(buffer, project, user_triggered, focused, cx)); + let blame = cx + .new(|cx| GitBlame::new(self.buffer.clone(), project, user_triggered, focused, cx)); self.blame_subscription = Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify())); self.blame = Some(blame); @@ -19783,7 +19788,24 @@ impl Editor { let buffer = &snapshot.buffer_snapshot; let start = buffer.anchor_before(0); let end = buffer.anchor_after(buffer.len()); - self.background_highlights_in_range(start..end, &snapshot, cx.theme()) + self.sorted_background_highlights_in_range(start..end, &snapshot, cx.theme()) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn sorted_background_highlights_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + theme: &Theme, + ) -> Vec<(Range, Hsla)> { + let mut res = self.background_highlights_in_range(search_range, display_snapshot, theme); + res.sort_by(|a, b| { + a.0.start + .cmp(&b.0.start) + .then_with(|| a.0.end.cmp(&b.0.end)) + .then_with(|| a.1.cmp(&b.1)) + }); + res } #[cfg(feature = "test-support")] @@ -19848,6 +19870,9 @@ impl Editor { .is_some_and(|(_, highlights)| !highlights.is_empty()) } + /// Returns all background highlights for a given range. + /// + /// The order of highlights is not deterministic, do sort the ranges if needed for the logic. pub fn background_highlights_in_range( &self, search_range: Range, @@ -19886,84 +19911,6 @@ impl Editor { results } - pub fn background_highlight_row_ranges( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - count: usize, - ) -> Vec> { - let mut results = Vec::new(); - let Some((_, ranges)) = self - .background_highlights - .get(&HighlightKey::Type(TypeId::of::())) - else { - return vec![]; - }; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&search_range.start, &display_snapshot.buffer_snapshot); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - let mut push_region = |start: Option, end: Option| { - if let (Some(start_display), Some(end_display)) = (start, end) { - results.push( - start_display.to_display_point(display_snapshot) - ..=end_display.to_display_point(display_snapshot), - ); - } - }; - let mut start_row: Option = None; - let mut end_row: Option = None; - if ranges.len() > count { - return Vec::new(); - } - for range in &ranges[start_ix..] { - if range - .start - .cmp(&search_range.end, &display_snapshot.buffer_snapshot) - .is_ge() - { - break; - } - let end = range.end.to_point(&display_snapshot.buffer_snapshot); - if let Some(current_row) = &end_row - && end.row == current_row.row - { - continue; - } - let start = range.start.to_point(&display_snapshot.buffer_snapshot); - if start_row.is_none() { - assert_eq!(end_row, None); - start_row = Some(start); - end_row = Some(end); - continue; - } - if let Some(current_end) = end_row.as_mut() { - if start.row > current_end.row + 1 { - push_region(start_row, end_row); - start_row = Some(start); - end_row = Some(end); - } else { - // Merge two hunks. - *current_end = end; - } - } else { - unreachable!(); - } - } - // We might still have a hunk that was not rendered (if there was a search hit on the last line) - push_region(start_row, end_row); - results - } - pub fn gutter_highlights_in_range( &self, search_range: Range, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ddfd32be8d11f421ff9cd41aa49996bd27dd06fb..1893839ea67995d316dca64418cc05b13586ac4b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -15453,37 +15453,34 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { ); let snapshot = editor.snapshot(window, cx); - let mut highlighted_ranges = editor.background_highlights_in_range( + let highlighted_ranges = editor.sorted_background_highlights_in_range( anchor_range(Point::new(3, 4)..Point::new(7, 4)), &snapshot, cx.theme(), ); - // Enforce a consistent ordering based on color without relying on the ordering of the - // highlight's `TypeId` which is non-executor. - highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); assert_eq!( highlighted_ranges, &[ ( - DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4), - Hsla::red(), + DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5), + Hsla::green(), ), ( - DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5), + DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4), Hsla::red(), ), ( - DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5), + DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6), Hsla::green(), ), ( - DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6), - Hsla::green(), + DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5), + Hsla::red(), ), ] ); assert_eq!( - editor.background_highlights_in_range( + editor.sorted_background_highlights_in_range( anchor_range(Point::new(5, 6)..Point::new(6, 4)), &snapshot, cx.theme(), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 500cce7e0a63bf0a3c985fdd6b507af389775792..fd5e544725696c63af2458447c66813f12f4cba3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -117,6 +117,7 @@ struct SelectionLayout { struct InlineBlameLayout { element: AnyElement, bounds: Bounds, + buffer_id: BufferId, entry: BlameEntry, } @@ -1157,7 +1158,7 @@ impl EditorElement { cx.notify(); } - if let Some((bounds, blame_entry)) = &position_map.inline_blame_bounds { + if let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds { let mouse_over_inline_blame = bounds.contains(&event.position); let mouse_over_popover = editor .inline_blame_popover @@ -1170,7 +1171,7 @@ impl EditorElement { .is_some_and(|state| state.keyboard_grace); if mouse_over_inline_blame || mouse_over_popover { - editor.show_blame_popover(blame_entry, event.position, false, cx); + editor.show_blame_popover(*buffer_id, blame_entry, event.position, false, cx); } else if !keyboard_grace { editor.hide_blame_popover(cx); } @@ -2454,7 +2455,7 @@ impl EditorElement { padding * em_width }; - let entry = blame + let (buffer_id, entry) = blame .update(cx, |blame, cx| { blame.blame_for_rows(&[*row_info], cx).next() }) @@ -2489,13 +2490,22 @@ impl EditorElement { let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); let bounds = Bounds::new(absolute_offset, size); - self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx); + self.layout_blame_entry_popover( + entry.clone(), + blame, + line_height, + text_hitbox, + row_info.buffer_id?, + window, + cx, + ); element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx); Some(InlineBlameLayout { element, bounds, + buffer_id, entry, }) } @@ -2506,6 +2516,7 @@ impl EditorElement { blame: Entity, line_height: Pixels, text_hitbox: &Hitbox, + buffer: BufferId, window: &mut Window, cx: &mut App, ) { @@ -2530,6 +2541,7 @@ impl EditorElement { popover_state.markdown, workspace, &blame, + buffer, window, cx, ) @@ -2604,14 +2616,16 @@ impl EditorElement { .into_iter() .enumerate() .flat_map(|(ix, blame_entry)| { + let (buffer_id, blame_entry) = blame_entry?; let mut element = render_blame_entry( ix, &blame, - blame_entry?, + blame_entry, &self.style, &mut last_used_color, self.editor.clone(), workspace.clone(), + buffer_id, blame_renderer.clone(), cx, )?; @@ -7401,12 +7415,13 @@ fn render_blame_entry_popover( markdown: Entity, workspace: WeakEntity, blame: &Entity, + buffer: BufferId, window: &mut Window, cx: &mut App, ) -> Option { let renderer = cx.global::().0.clone(); let blame = blame.read(cx); - let repository = blame.repository(cx)?; + let repository = blame.repository(cx, buffer)?; renderer.render_blame_entry_popover( blame_entry, scroll_handle, @@ -7427,6 +7442,7 @@ fn render_blame_entry( last_used_color: &mut Option<(PlayerColor, Oid)>, editor: Entity, workspace: Entity, + buffer: BufferId, renderer: Arc, cx: &mut App, ) -> Option { @@ -7447,8 +7463,8 @@ fn render_blame_entry( last_used_color.replace((sha_color, blame_entry.sha)); let blame = blame.read(cx); - let details = blame.details_for_entry(&blame_entry); - let repository = blame.repository(cx)?; + let details = blame.details_for_entry(buffer, &blame_entry); + let repository = blame.repository(cx, buffer)?; renderer.render_blame_entry( &style.text, blame_entry, @@ -8755,7 +8771,7 @@ impl Element for EditorElement { return None; } let blame = editor.blame.as_ref()?; - let blame_entry = blame + let (_, blame_entry) = blame .update(cx, |blame, cx| { let row_infos = snapshot.row_infos(snapshot.longest_row()).next()?; @@ -9305,7 +9321,7 @@ impl Element for EditorElement { text_hitbox: text_hitbox.clone(), inline_blame_bounds: inline_blame_layout .as_ref() - .map(|layout| (layout.bounds, layout.entry.clone())), + .map(|layout| (layout.bounds, layout.buffer_id, layout.entry.clone())), display_hunks: display_hunks.clone(), diff_hunk_control_bounds, }); @@ -9969,7 +9985,7 @@ pub(crate) struct PositionMap { pub snapshot: EditorSnapshot, pub text_hitbox: Hitbox, pub gutter_hitbox: Hitbox, - pub inline_blame_bounds: Option<(Bounds, BlameEntry)>, + pub inline_blame_bounds: Option<(Bounds, BufferId, BlameEntry)>, pub display_hunks: Vec<(DisplayDiffHunk, Option)>, pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds)>, } diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index b11617ccec22f08737ba40ab257e19a81eddfd89..27a9b8870383b7f1136e31028bacedc8744e0650 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -10,16 +10,18 @@ use gpui::{ AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task, TextStyle, WeakEntity, Window, }; -use language::{Bias, Buffer, BufferSnapshot, Edit}; +use itertools::Itertools; +use language::{Bias, BufferSnapshot, Edit}; use markdown::Markdown; -use multi_buffer::RowInfo; +use multi_buffer::{MultiBuffer, RowInfo}; use project::{ - Project, ProjectItem, + Project, ProjectItem as _, git_store::{GitStoreEvent, Repository, RepositoryEvent}, }; use smallvec::SmallVec; use std::{sync::Arc, time::Duration}; use sum_tree::SumTree; +use text::BufferId; use workspace::Workspace; #[derive(Clone, Debug, Default)] @@ -63,16 +65,19 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 { } } -pub struct GitBlame { - project: Entity, - buffer: Entity, +struct GitBlameBuffer { entries: SumTree, - commit_details: HashMap, buffer_snapshot: BufferSnapshot, buffer_edits: text::Subscription, + commit_details: HashMap, +} + +pub struct GitBlame { + project: Entity, + multi_buffer: WeakEntity, + buffers: HashMap, task: Task>, focused: bool, - generated: bool, changed_while_blurred: bool, user_triggered: bool, regenerate_on_edit_task: Task>, @@ -184,44 +189,44 @@ impl gpui::Global for GlobalBlameRenderer {} impl GitBlame { pub fn new( - buffer: Entity, + multi_buffer: Entity, project: Entity, user_triggered: bool, focused: bool, cx: &mut Context, ) -> Self { - let entries = SumTree::from_item( - GitBlameEntry { - rows: buffer.read(cx).max_point().row + 1, - blame: None, + let multi_buffer_subscription = cx.subscribe( + &multi_buffer, + |git_blame, multi_buffer, event, cx| match event { + multi_buffer::Event::DirtyChanged => { + if !multi_buffer.read(cx).is_dirty(cx) { + git_blame.generate(cx); + } + } + multi_buffer::Event::ExcerptsAdded { .. } + | multi_buffer::Event::ExcerptsEdited { .. } => git_blame.regenerate_on_edit(cx), + _ => {} }, - &(), ); - let buffer_subscriptions = cx.subscribe(&buffer, |this, buffer, event, cx| match event { - language::BufferEvent::DirtyChanged => { - if !buffer.read(cx).is_dirty() { - this.generate(cx); - } - } - language::BufferEvent::Edited => { - this.regenerate_on_edit(cx); - } - _ => {} - }); - let project_subscription = cx.subscribe(&project, { - let buffer = buffer.clone(); + let multi_buffer = multi_buffer.downgrade(); - move |this, _, event, cx| { + move |git_blame, _, event, cx| { if let project::Event::WorktreeUpdatedEntries(_, updated) = event { - let project_entry_id = buffer.read(cx).entry_id(cx); + let Some(multi_buffer) = multi_buffer.upgrade() else { + return; + }; + let project_entry_id = multi_buffer + .read(cx) + .as_singleton() + .and_then(|it| it.read(cx).entry_id(cx)); if updated .iter() .any(|(_, entry_id, _)| project_entry_id == Some(*entry_id)) { log::debug!("Updated buffers. Regenerating blame data...",); - this.generate(cx); + git_blame.generate(cx); } } } @@ -239,24 +244,17 @@ impl GitBlame { _ => {} }); - let buffer_snapshot = buffer.read(cx).snapshot(); - let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); - let mut this = Self { project, - buffer, - buffer_snapshot, - entries, - buffer_edits, + multi_buffer: multi_buffer.downgrade(), + buffers: HashMap::default(), user_triggered, focused, changed_while_blurred: false, - commit_details: HashMap::default(), task: Task::ready(Ok(())), - generated: false, regenerate_on_edit_task: Task::ready(Ok(())), _regenerate_subscriptions: vec![ - buffer_subscriptions, + multi_buffer_subscription, project_subscription, git_store_subscription, ], @@ -265,56 +263,63 @@ impl GitBlame { this } - pub fn repository(&self, cx: &App) -> Option> { + pub fn repository(&self, cx: &App, id: BufferId) -> Option> { self.project .read(cx) .git_store() .read(cx) - .repository_and_path_for_buffer_id(self.buffer.read(cx).remote_id(), cx) + .repository_and_path_for_buffer_id(id, cx) .map(|(repo, _)| repo) } pub fn has_generated_entries(&self) -> bool { - self.generated + !self.buffers.is_empty() } - pub fn details_for_entry(&self, entry: &BlameEntry) -> Option { - self.commit_details.get(&entry.sha).cloned() + pub fn details_for_entry( + &self, + buffer: BufferId, + entry: &BlameEntry, + ) -> Option { + self.buffers + .get(&buffer)? + .commit_details + .get(&entry.sha) + .cloned() } pub fn blame_for_rows<'a>( &'a mut self, rows: &'a [RowInfo], - cx: &App, - ) -> impl 'a + Iterator> { - self.sync(cx); - - let buffer_id = self.buffer_snapshot.remote_id(); - let mut cursor = self.entries.cursor::(&()); + cx: &'a mut App, + ) -> impl Iterator> + use<'a> { rows.iter().map(move |info| { - let row = info - .buffer_row - .filter(|_| info.buffer_id == Some(buffer_id))?; - cursor.seek_forward(&row, Bias::Right); - cursor.item()?.blame.clone() + let buffer_id = info.buffer_id?; + self.sync(cx, buffer_id); + + let buffer_row = info.buffer_row?; + let mut cursor = self.buffers.get(&buffer_id)?.entries.cursor::(&()); + cursor.seek_forward(&buffer_row, Bias::Right); + Some((buffer_id, cursor.item()?.blame.clone()?)) }) } - pub fn max_author_length(&mut self, cx: &App) -> usize { - self.sync(cx); - + pub fn max_author_length(&mut self, cx: &mut App) -> usize { let mut max_author_length = 0; - - for entry in self.entries.iter() { - let author_len = entry - .blame - .as_ref() - .and_then(|entry| entry.author.as_ref()) - .map(|author| author.len()); - if let Some(author_len) = author_len - && author_len > max_author_length - { - max_author_length = author_len; + self.sync_all(cx); + + for buffer in self.buffers.values() { + for entry in buffer.entries.iter() { + let author_len = entry + .blame + .as_ref() + .and_then(|entry| entry.author.as_ref()) + .map(|author| author.len()); + if let Some(author_len) = author_len + && author_len > max_author_length + { + max_author_length = author_len; + } } } @@ -336,22 +341,48 @@ impl GitBlame { } } - fn sync(&mut self, cx: &App) { - let edits = self.buffer_edits.consume(); - let new_snapshot = self.buffer.read(cx).snapshot(); + fn sync_all(&mut self, cx: &mut App) { + let Some(multi_buffer) = self.multi_buffer.upgrade() else { + return; + }; + multi_buffer + .read(cx) + .excerpt_buffer_ids() + .into_iter() + .for_each(|id| self.sync(cx, id)); + } + + fn sync(&mut self, cx: &mut App, buffer_id: BufferId) { + let Some(blame_buffer) = self.buffers.get_mut(&buffer_id) else { + return; + }; + let Some(buffer) = self + .multi_buffer + .upgrade() + .and_then(|multi_buffer| multi_buffer.read(cx).buffer(buffer_id)) + else { + return; + }; + let edits = blame_buffer.buffer_edits.consume(); + let new_snapshot = buffer.read(cx).snapshot(); let mut row_edits = edits .into_iter() .map(|edit| { - let old_point_range = self.buffer_snapshot.offset_to_point(edit.old.start) - ..self.buffer_snapshot.offset_to_point(edit.old.end); + let old_point_range = blame_buffer.buffer_snapshot.offset_to_point(edit.old.start) + ..blame_buffer.buffer_snapshot.offset_to_point(edit.old.end); let new_point_range = new_snapshot.offset_to_point(edit.new.start) ..new_snapshot.offset_to_point(edit.new.end); if old_point_range.start.column - == self.buffer_snapshot.line_len(old_point_range.start.row) + == blame_buffer + .buffer_snapshot + .line_len(old_point_range.start.row) && (new_snapshot.chars_at(edit.new.start).next() == Some('\n') - || self.buffer_snapshot.line_len(old_point_range.end.row) == 0) + || blame_buffer + .buffer_snapshot + .line_len(old_point_range.end.row) + == 0) { Edit { old: old_point_range.start.row + 1..old_point_range.end.row + 1, @@ -375,7 +406,7 @@ impl GitBlame { .peekable(); let mut new_entries = SumTree::default(); - let mut cursor = self.entries.cursor::(&()); + let mut cursor = blame_buffer.entries.cursor::(&()); while let Some(mut edit) = row_edits.next() { while let Some(next_edit) = row_edits.peek() { @@ -433,17 +464,28 @@ impl GitBlame { new_entries.append(cursor.suffix(), &()); drop(cursor); - self.buffer_snapshot = new_snapshot; - self.entries = new_entries; + blame_buffer.buffer_snapshot = new_snapshot; + blame_buffer.entries = new_entries; } #[cfg(test)] fn check_invariants(&mut self, cx: &mut Context) { - self.sync(cx); - assert_eq!( - self.entries.summary().rows, - self.buffer.read(cx).max_point().row + 1 - ); + self.sync_all(cx); + for (&id, buffer) in &self.buffers { + assert_eq!( + buffer.entries.summary().rows, + self.multi_buffer + .upgrade() + .unwrap() + .read(cx) + .buffer(id) + .unwrap() + .read(cx) + .max_point() + .row + + 1 + ); + } } fn generate(&mut self, cx: &mut Context) { @@ -451,62 +493,105 @@ impl GitBlame { self.changed_while_blurred = true; return; } - let buffer_edits = self.buffer.update(cx, |buffer, _| buffer.subscribe()); - let snapshot = self.buffer.read(cx).snapshot(); let blame = self.project.update(cx, |project, cx| { - project.blame_buffer(&self.buffer, None, cx) + let Some(multi_buffer) = self.multi_buffer.upgrade() else { + return Vec::new(); + }; + multi_buffer + .read(cx) + .all_buffer_ids() + .into_iter() + .filter_map(|id| { + let buffer = multi_buffer.read(cx).buffer(id)?; + let snapshot = buffer.read(cx).snapshot(); + let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); + + let blame_buffer = project.blame_buffer(&buffer, None, cx); + Some((id, snapshot, buffer_edits, blame_buffer)) + }) + .collect::>() }); let provider_registry = GitHostingProviderRegistry::default_global(cx); self.task = cx.spawn(async move |this, cx| { - let result = cx + let (result, errors) = cx .background_spawn({ - let snapshot = snapshot.clone(); async move { - let Some(Blame { - entries, - messages, - remote_url, - }) = blame.await? - else { - return Ok(None); - }; - - let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row); - let commit_details = - parse_commit_messages(messages, remote_url, provider_registry).await; - - anyhow::Ok(Some((entries, commit_details))) + let mut res = vec![]; + let mut errors = vec![]; + for (id, snapshot, buffer_edits, blame) in blame { + match blame.await { + Ok(Some(Blame { + entries, + messages, + remote_url, + })) => { + let entries = build_blame_entry_sum_tree( + entries, + snapshot.max_point().row, + ); + let commit_details = parse_commit_messages( + messages, + remote_url, + provider_registry.clone(), + ) + .await; + + res.push(( + id, + snapshot, + buffer_edits, + Some(entries), + commit_details, + )); + } + Ok(None) => { + res.push((id, snapshot, buffer_edits, None, Default::default())) + } + Err(e) => errors.push(e), + } + } + (res, errors) } }) .await; - this.update(cx, |this, cx| match result { - Ok(None) => { - // Nothing to do, e.g. no repository found + this.update(cx, |this, cx| { + this.buffers.clear(); + for (id, snapshot, buffer_edits, entries, commit_details) in result { + let Some(entries) = entries else { + continue; + }; + this.buffers.insert( + id, + GitBlameBuffer { + buffer_edits, + buffer_snapshot: snapshot, + entries, + commit_details, + }, + ); } - Ok(Some((entries, commit_details))) => { - this.buffer_edits = buffer_edits; - this.buffer_snapshot = snapshot; - this.entries = entries; - this.commit_details = commit_details; - this.generated = true; - cx.notify(); + cx.notify(); + if !errors.is_empty() { + this.project.update(cx, |_, cx| { + if this.user_triggered { + log::error!("failed to get git blame data: {errors:?}"); + let notification = errors + .into_iter() + .format_with(",", |e, f| f(&format_args!("{:#}", e))) + .to_string(); + cx.emit(project::Event::Toast { + notification_id: "git-blame".into(), + message: notification, + }); + } else { + // If we weren't triggered by a user, we just log errors in the background, instead of sending + // notifications. + log::debug!("failed to get git blame data: {errors:?}"); + } + }) } - Err(error) => this.project.update(cx, |_, cx| { - if this.user_triggered { - log::error!("failed to get git blame data: {error:?}"); - let notification = format!("{:#}", error).trim().to_string(); - cx.emit(project::Event::Toast { - notification_id: "git-blame".into(), - message: notification, - }); - } else { - // If we weren't triggered by a user, we just log errors in the background, instead of sending - // notifications. - log::debug!("failed to get git blame data: {error:?}"); - } - }), }) }); } @@ -520,7 +605,7 @@ impl GitBlame { this.update(cx, |this, cx| { this.generate(cx); }) - }) + }); } } @@ -659,6 +744,9 @@ mod tests { ) .collect::>(), expected + .into_iter() + .map(|it| Some((buffer_id, it?))) + .collect::>() ); } @@ -705,6 +793,7 @@ mod tests { }) .await .unwrap(); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let blame = cx.new(|cx| GitBlame::new(buffer.clone(), project.clone(), true, true, cx)); @@ -785,6 +874,7 @@ mod tests { .await .unwrap(); let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx)); @@ -806,14 +896,14 @@ mod tests { ) .collect::>(), vec![ - Some(blame_entry("1b1b1b", 0..1)), - Some(blame_entry("0d0d0d", 1..2)), - Some(blame_entry("3a3a3a", 2..3)), + Some((buffer_id, blame_entry("1b1b1b", 0..1))), + Some((buffer_id, blame_entry("0d0d0d", 1..2))), + Some((buffer_id, blame_entry("3a3a3a", 2..3))), None, None, - Some(blame_entry("3a3a3a", 5..6)), - Some(blame_entry("0d0d0d", 6..7)), - Some(blame_entry("3a3a3a", 7..8)), + Some((buffer_id, blame_entry("3a3a3a", 5..6))), + Some((buffer_id, blame_entry("0d0d0d", 6..7))), + Some((buffer_id, blame_entry("3a3a3a", 7..8))), ] ); // Subset of lines @@ -831,8 +921,8 @@ mod tests { ) .collect::>(), vec![ - Some(blame_entry("0d0d0d", 1..2)), - Some(blame_entry("3a3a3a", 2..3)), + Some((buffer_id, blame_entry("0d0d0d", 1..2))), + Some((buffer_id, blame_entry("3a3a3a", 2..3))), None ] ); @@ -852,7 +942,7 @@ mod tests { cx ) .collect::>(), - vec![Some(blame_entry("0d0d0d", 1..2)), None, None] + vec![Some((buffer_id, blame_entry("0d0d0d", 1..2))), None, None] ); }); } @@ -895,6 +985,7 @@ mod tests { .await .unwrap(); let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx)); @@ -1061,8 +1152,9 @@ mod tests { }) .await .unwrap(); + let mbuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx)); + let git_blame = cx.new(|cx| GitBlame::new(mbuffer.clone(), project, false, true, cx)); cx.executor().run_until_parked(); git_blame.update(cx, |blame, cx| blame.check_invariants(cx)); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e27cbf868a67eec98c0538a4410f4b7ff11b7d0c..874e58d2a354628958ef160fdae2836b563d3c7b 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -735,7 +735,7 @@ impl MultiBuffer { pub fn as_singleton(&self) -> Option> { if self.singleton { - return Some( + Some( self.buffers .borrow() .values() @@ -743,7 +743,7 @@ impl MultiBuffer { .unwrap() .buffer .clone(), - ); + ) } else { None } @@ -2552,6 +2552,10 @@ impl MultiBuffer { .collect() } + pub fn all_buffer_ids(&self) -> Vec { + self.buffers.borrow().keys().copied().collect() + } + pub fn buffer(&self, buffer_id: BufferId) -> Option> { self.buffers .borrow() diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 1521d012955050c932d2d2f797a25ab8f3c99d48..a8d5046f90f3136ef4c913ea0e87fd55f77dcd6a 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5402,8 +5402,9 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); - populate_with_test_ra_project(&fs, "/rust-analyzer").await; - let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await; + let root = path!("/rust-analyzer"); + populate_with_test_ra_project(&fs, root).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| { project.languages().add(Arc::new(rust_lang())) }); @@ -5448,15 +5449,16 @@ mod tests { }); }); - let all_matches = r#"/rust-analyzer/ + let all_matches = format!( + r#"{root}/ crates/ ide/src/ inlay_hints/ fn_lifetime_fn.rs - search: match config.param_names_for_lifetime_elision_hints { - search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - search: Some(it) if config.param_names_for_lifetime_elision_hints => { - search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + search: match config.param_names_for_lifetime_elision_hints {{ + search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{ + search: Some(it) if config.param_names_for_lifetime_elision_hints => {{ + search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }}, inlay_hints.rs search: pub param_names_for_lifetime_elision_hints: bool, search: param_names_for_lifetime_elision_hints: self @@ -5467,7 +5469,9 @@ mod tests { analysis_stats.rs search: param_names_for_lifetime_elision_hints: true, config.rs - search: param_names_for_lifetime_elision_hints: self"#; + search: param_names_for_lifetime_elision_hints: self"# + ); + let select_first_in_all_matches = |line_to_select: &str| { assert!(all_matches.contains(line_to_select)); all_matches.replacen( @@ -5524,7 +5528,7 @@ mod tests { cx, ), format!( - r#"/rust-analyzer/ + r#"{root}/ crates/ ide/src/ inlay_hints/ @@ -5594,7 +5598,7 @@ mod tests { cx, ), format!( - r#"/rust-analyzer/ + r#"{root}/ crates/ ide/src/{SELECTED_MARKER} rust-analyzer/src/ @@ -5631,8 +5635,9 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); - populate_with_test_ra_project(&fs, "/rust-analyzer").await; - let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await; + let root = path!("/rust-analyzer"); + populate_with_test_ra_project(&fs, root).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| { project.languages().add(Arc::new(rust_lang())) }); @@ -5676,15 +5681,16 @@ mod tests { ); }); }); - let all_matches = r#"/rust-analyzer/ + let all_matches = format!( + r#"{root}/ crates/ ide/src/ inlay_hints/ fn_lifetime_fn.rs - search: match config.param_names_for_lifetime_elision_hints { - search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - search: Some(it) if config.param_names_for_lifetime_elision_hints => { - search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + search: match config.param_names_for_lifetime_elision_hints {{ + search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{ + search: Some(it) if config.param_names_for_lifetime_elision_hints => {{ + search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }}, inlay_hints.rs search: pub param_names_for_lifetime_elision_hints: bool, search: param_names_for_lifetime_elision_hints: self @@ -5695,7 +5701,8 @@ mod tests { analysis_stats.rs search: param_names_for_lifetime_elision_hints: true, config.rs - search: param_names_for_lifetime_elision_hints: self"#; + search: param_names_for_lifetime_elision_hints: self"# + ); cx.executor() .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); @@ -5768,8 +5775,9 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); - populate_with_test_ra_project(&fs, path!("/rust-analyzer")).await; - let project = Project::test(fs.clone(), [path!("/rust-analyzer").as_ref()], cx).await; + let root = path!("/rust-analyzer"); + populate_with_test_ra_project(&fs, root).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| { project.languages().add(Arc::new(rust_lang())) }); @@ -5813,9 +5821,8 @@ mod tests { ); }); }); - let root_path = format!("{}/", path!("/rust-analyzer")); let all_matches = format!( - r#"{root_path} + r#"{root}/ crates/ ide/src/ inlay_hints/ @@ -5977,7 +5984,7 @@ mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ "one": { "a.txt": "aaa aaa" @@ -5989,7 +5996,7 @@ mod tests { }), ) .await; - let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await; + let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await; let workspace = add_outline_panel(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let outline_panel = outline_panel(&workspace, cx); @@ -6000,7 +6007,7 @@ mod tests { let items = workspace .update(cx, |workspace, window, cx| { workspace.open_paths( - vec![PathBuf::from("/root/two")], + vec![PathBuf::from(path!("/root/two"))], OpenOptions { visible: Some(OpenVisible::OnlyDirectories), ..Default::default() @@ -6064,13 +6071,17 @@ mod tests { outline_panel.selected_entry(), cx, ), - r#"/root/one/ + format!( + r#"{}/ a.txt search: aaa aaa <==== selected search: aaa aaa -/root/two/ +{}/ b.txt - search: a aaa"# + search: a aaa"#, + path!("/root/one"), + path!("/root/two"), + ), ); }); @@ -6090,11 +6101,15 @@ mod tests { outline_panel.selected_entry(), cx, ), - r#"/root/one/ + format!( + r#"{}/ a.txt <==== selected -/root/two/ +{}/ b.txt - search: a aaa"# + search: a aaa"#, + path!("/root/one"), + path!("/root/two"), + ), ); }); @@ -6114,9 +6129,13 @@ mod tests { outline_panel.selected_entry(), cx, ), - r#"/root/one/ + format!( + r#"{}/ a.txt -/root/two/ <==== selected"# +{}/ <==== selected"#, + path!("/root/one"), + path!("/root/two"), + ), ); }); @@ -6135,11 +6154,15 @@ mod tests { outline_panel.selected_entry(), cx, ), - r#"/root/one/ + format!( + r#"{}/ a.txt -/root/two/ <==== selected +{}/ <==== selected b.txt - search: a aaa"# + search: a aaa"#, + path!("/root/one"), + path!("/root/two"), + ) ); }); } @@ -6165,7 +6188,7 @@ struct OutlineEntryExcerpt { }), ) .await; - let project = Project::test(fs.clone(), [root.as_ref()], cx).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| { project.languages().add(Arc::new( rust_lang() @@ -6508,7 +6531,7 @@ outline: struct OutlineEntryExcerpt async fn test_frontend_repo_structure(cx: &mut TestAppContext) { init_test(cx); - let root = "/frontend-project"; + let root = path!("/frontend-project"); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( root, @@ -6545,7 +6568,7 @@ outline: struct OutlineEntryExcerpt }), ) .await; - let project = Project::test(fs.clone(), [root.as_ref()], cx).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; let workspace = add_outline_panel(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let outline_panel = outline_panel(&workspace, cx); @@ -6599,10 +6622,11 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } <==== selected + search: {{ "something": "static" }} <==== selected src/ app/(site)/ (about)/jobs/[slug]/ @@ -6614,6 +6638,7 @@ outline: struct OutlineEntryExcerpt components/ ErrorBoundary.tsx search: static"# + ) ); }); @@ -6636,15 +6661,17 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } + search: {{ "something": "static" }} src/ app/(site)/ <==== selected components/ ErrorBoundary.tsx search: static"# + ) ); }); @@ -6664,15 +6691,17 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } + search: {{ "something": "static" }} src/ app/(site)/ components/ ErrorBoundary.tsx search: static <==== selected"# + ) ); }); @@ -6696,14 +6725,16 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } + search: {{ "something": "static" }} src/ app/(site)/ components/ ErrorBoundary.tsx <==== selected"# + ) ); }); @@ -6727,15 +6758,17 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } + search: {{ "something": "static" }} src/ app/(site)/ components/ ErrorBoundary.tsx <==== selected search: static"# + ) ); }); } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 73f5da086c78b3a3e597be3d32bdcb7a15649117..6e06c2dd95fa9bf8d3b1a0670fb119a0ef552de1 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -2566,10 +2566,7 @@ impl LocalLspStore { }; let Ok(file_url) = lsp::Uri::from_file_path(old_path.as_path()) else { - debug_panic!( - "`{}` is not parseable as an URI", - old_path.to_string_lossy() - ); + debug_panic!("{old_path:?} is not parseable as an URI"); return; }; self.unregister_buffer_from_language_servers(buffer, &file_url, cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2668d270d7f008d49d6d067ba01d951d44a43a00..4a2dbf31fc96b43db34bd9977fafb09cc5ad60d1 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1384,6 +1384,9 @@ impl ProjectSearchView { let match_ranges = self.entity.read(cx).match_ranges.clone(); if match_ranges.is_empty() { self.active_match_index = None; + self.results_editor.update(cx, |editor, cx| { + editor.clear_background_highlights::(cx); + }); } else { self.active_match_index = Some(0); self.update_match_index(cx); @@ -2338,7 +2341,7 @@ pub fn perform_project_search( #[cfg(test)] pub mod tests { - use std::{ops::Deref as _, sync::Arc}; + use std::{ops::Deref as _, sync::Arc, time::Duration}; use super::*; use editor::{DisplayPoint, display_map::DisplayRow}; @@ -2381,6 +2384,7 @@ pub mod tests { "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" ); let match_background_color = cx.theme().colors().search_match_background; + let selection_background_color = cx.theme().colors().editor_document_highlight_bracket_background; assert_eq!( search_view .results_editor @@ -2390,14 +2394,23 @@ pub mod tests { DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35), match_background_color ), + ( + DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40), + selection_background_color + ), ( DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40), match_background_color ), + ( + DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9), + selection_background_color + ), ( DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9), match_background_color - ) + ), + ] ); assert_eq!(search_view.active_match_index, Some(0)); @@ -4156,6 +4169,10 @@ pub mod tests { search_view.search(cx); }) .unwrap(); + // Ensure editor highlights appear after the search is done + cx.executor().advance_clock( + editor::SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(100), + ); cx.background_executor.run_until_parked(); } } diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 54e8ae8343f4778e04a37a7ebd3dbe2b6da587cd..fc93d40ae5021165cefdf9130a65af221806ee6d 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -2,6 +2,7 @@ use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; +/// A cheaply-clonable ordered map based on a [SumTree](crate::SumTree). #[derive(Clone, PartialEq, Eq)] pub struct TreeMap(SumTree>) where