mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
editor: Make blame and inline blame work for multibuffers (#37366)
Release Notes: - Added blame view and inline blame support for multi buffer editors --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
92283285ae
commit
c1ca7303a8
10 changed files with 429 additions and 325 deletions
|
|
@ -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))),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Arc<str>>,
|
||||
highlight_order: usize,
|
||||
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
|
||||
background_highlights: TreeMap<HighlightKey, BackgroundHighlight>,
|
||||
gutter_highlights: TreeMap<TypeId, GutterHighlight>,
|
||||
background_highlights: HashMap<HighlightKey, BackgroundHighlight>,
|
||||
gutter_highlights: HashMap<TypeId, GutterHighlight>,
|
||||
scrollbar_marker_state: ScrollbarMarkerState,
|
||||
active_indent_guides_state: ActiveIndentGuidesState,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
|
|
@ -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<Pixels>,
|
||||
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::<Point>(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::<GlobalBlameRenderer>().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<Self>,
|
||||
) {
|
||||
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<Anchor>,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
theme: &Theme,
|
||||
) -> Vec<(Range<DisplayPoint>, 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<Anchor>,
|
||||
|
|
@ -19886,84 +19911,6 @@ impl Editor {
|
|||
results
|
||||
}
|
||||
|
||||
pub fn background_highlight_row_ranges<T: 'static>(
|
||||
&self,
|
||||
search_range: Range<Anchor>,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
count: usize,
|
||||
) -> Vec<RangeInclusive<DisplayPoint>> {
|
||||
let mut results = Vec::new();
|
||||
let Some((_, ranges)) = self
|
||||
.background_highlights
|
||||
.get(&HighlightKey::Type(TypeId::of::<T>()))
|
||||
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<Point>, end: Option<Point>| {
|
||||
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<Point> = None;
|
||||
let mut end_row: Option<Point> = 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<Anchor>,
|
||||
|
|
|
|||
|
|
@ -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(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
|
||||
Hsla::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
|
||||
Hsla::green(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
|
||||
Hsla::red(),
|
||||
),
|
||||
(
|
||||
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(),
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ struct SelectionLayout {
|
|||
struct InlineBlameLayout {
|
||||
element: AnyElement,
|
||||
bounds: Bounds<Pixels>,
|
||||
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<GitBlame>,
|
||||
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<Markdown>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
blame: &Entity<GitBlame>,
|
||||
buffer: BufferId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
let renderer = cx.global::<GlobalBlameRenderer>().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<Editor>,
|
||||
workspace: Entity<Workspace>,
|
||||
buffer: BufferId,
|
||||
renderer: Arc<dyn BlameRenderer>,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
|
|
@ -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<Pixels>, BlameEntry)>,
|
||||
pub inline_blame_bounds: Option<(Bounds<Pixels>, BufferId, BlameEntry)>,
|
||||
pub display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
|
||||
pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds<Pixels>)>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Project>,
|
||||
buffer: Entity<Buffer>,
|
||||
struct GitBlameBuffer {
|
||||
entries: SumTree<GitBlameEntry>,
|
||||
commit_details: HashMap<Oid, ParsedCommitMessage>,
|
||||
buffer_snapshot: BufferSnapshot,
|
||||
buffer_edits: text::Subscription,
|
||||
commit_details: HashMap<Oid, ParsedCommitMessage>,
|
||||
}
|
||||
|
||||
pub struct GitBlame {
|
||||
project: Entity<Project>,
|
||||
multi_buffer: WeakEntity<MultiBuffer>,
|
||||
buffers: HashMap<BufferId, GitBlameBuffer>,
|
||||
task: Task<Result<()>>,
|
||||
focused: bool,
|
||||
generated: bool,
|
||||
changed_while_blurred: bool,
|
||||
user_triggered: bool,
|
||||
regenerate_on_edit_task: Task<Result<()>>,
|
||||
|
|
@ -184,44 +189,44 @@ impl gpui::Global for GlobalBlameRenderer {}
|
|||
|
||||
impl GitBlame {
|
||||
pub fn new(
|
||||
buffer: Entity<Buffer>,
|
||||
multi_buffer: Entity<MultiBuffer>,
|
||||
project: Entity<Project>,
|
||||
user_triggered: bool,
|
||||
focused: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> 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<Entity<Repository>> {
|
||||
pub fn repository(&self, cx: &App, id: BufferId) -> Option<Entity<Repository>> {
|
||||
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<ParsedCommitMessage> {
|
||||
self.commit_details.get(&entry.sha).cloned()
|
||||
pub fn details_for_entry(
|
||||
&self,
|
||||
buffer: BufferId,
|
||||
entry: &BlameEntry,
|
||||
) -> Option<ParsedCommitMessage> {
|
||||
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<Item = Option<BlameEntry>> {
|
||||
self.sync(cx);
|
||||
|
||||
let buffer_id = self.buffer_snapshot.remote_id();
|
||||
let mut cursor = self.entries.cursor::<u32>(&());
|
||||
cx: &'a mut App,
|
||||
) -> impl Iterator<Item = Option<(BufferId, BlameEntry)>> + 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::<u32>(&());
|
||||
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;
|
||||
self.sync_all(cx);
|
||||
|
||||
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;
|
||||
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::<u32>(&());
|
||||
let mut cursor = blame_buffer.entries.cursor::<u32>(&());
|
||||
|
||||
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>) {
|
||||
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<Self>) {
|
||||
|
|
@ -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::<Vec<_>>()
|
||||
});
|
||||
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 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;
|
||||
|
||||
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)))
|
||||
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::<Vec<_>>(),
|
||||
expected
|
||||
.into_iter()
|
||||
.map(|it| Some((buffer_id, it?)))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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<_>>(),
|
||||
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<_>>(),
|
||||
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<_>>(),
|
||||
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));
|
||||
|
||||
|
|
|
|||
|
|
@ -735,7 +735,7 @@ impl MultiBuffer {
|
|||
|
||||
pub fn as_singleton(&self) -> Option<Entity<Buffer>> {
|
||||
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<BufferId> {
|
||||
self.buffers.borrow().keys().copied().collect()
|
||||
}
|
||||
|
||||
pub fn buffer(&self, buffer_id: BufferId) -> Option<Entity<Buffer>> {
|
||||
self.buffers
|
||||
.borrow()
|
||||
|
|
|
|||
|
|
@ -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"#
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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::<Self>(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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<K, V>(SumTree<MapEntry<K, V>>)
|
||||
where
|
||||
|
|
|
|||
Loading…
Reference in a new issue