Allow highlighting editor rows from multiple sources concurrently (#9153)
This commit is contained in:
parent
f4a86e6fea
commit
41dc5fc412
9 changed files with 590 additions and 45 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -4287,9 +4287,15 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"indoc",
|
||||||
|
"language",
|
||||||
"menu",
|
"menu",
|
||||||
|
"project",
|
||||||
|
"serde_json",
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
|
"tree-sitter-rust",
|
||||||
|
"tree-sitter-typescript",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
@ -6614,12 +6620,18 @@ dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"indoc",
|
||||||
"language",
|
"language",
|
||||||
|
"menu",
|
||||||
"ordered-float 2.10.0",
|
"ordered-float 2.10.0",
|
||||||
"picker",
|
"picker",
|
||||||
|
"project",
|
||||||
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"smol",
|
"smol",
|
||||||
"theme",
|
"theme",
|
||||||
|
"tree-sitter-rust",
|
||||||
|
"tree-sitter-typescript",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
|
|
@ -41,7 +41,7 @@ futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
indoc = "1.0.4"
|
indoc.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
|
|
|
@ -43,7 +43,7 @@ use anyhow::{anyhow, Context as _, Result};
|
||||||
use blink_manager::BlinkManager;
|
use blink_manager::BlinkManager;
|
||||||
use client::{Collaborator, ParticipantIndex};
|
use client::{Collaborator, ParticipantIndex};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
use collections::{hash_map, BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use copilot::Copilot;
|
use copilot::Copilot;
|
||||||
use debounced_delay::DebouncedDelay;
|
use debounced_delay::DebouncedDelay;
|
||||||
|
@ -386,7 +386,8 @@ pub struct Editor {
|
||||||
show_gutter: bool,
|
show_gutter: bool,
|
||||||
show_wrap_guides: Option<bool>,
|
show_wrap_guides: Option<bool>,
|
||||||
placeholder_text: Option<Arc<str>>,
|
placeholder_text: Option<Arc<str>>,
|
||||||
highlighted_rows: Option<Range<u32>>,
|
highlight_order: usize,
|
||||||
|
highlighted_rows: HashMap<TypeId, Vec<(usize, Range<Anchor>, Hsla)>>,
|
||||||
background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
|
background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
|
||||||
nav_history: Option<ItemNavHistory>,
|
nav_history: Option<ItemNavHistory>,
|
||||||
context_menu: RwLock<Option<ContextMenu>>,
|
context_menu: RwLock<Option<ContextMenu>>,
|
||||||
|
@ -1523,7 +1524,8 @@ impl Editor {
|
||||||
show_gutter: mode == EditorMode::Full,
|
show_gutter: mode == EditorMode::Full,
|
||||||
show_wrap_guides: None,
|
show_wrap_guides: None,
|
||||||
placeholder_text: None,
|
placeholder_text: None,
|
||||||
highlighted_rows: None,
|
highlight_order: 0,
|
||||||
|
highlighted_rows: HashMap::default(),
|
||||||
background_highlights: Default::default(),
|
background_highlights: Default::default(),
|
||||||
nav_history: None,
|
nav_history: None,
|
||||||
context_menu: RwLock::new(None),
|
context_menu: RwLock::new(None),
|
||||||
|
@ -8921,12 +8923,93 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
|
/// Adds or removes (on `None` color) a highlight for the rows corresponding to the anchor range given.
|
||||||
self.highlighted_rows = rows;
|
/// On matching anchor range, replaces the old highlight; does not clear the other existing highlights.
|
||||||
|
/// If multiple anchor ranges will produce highlights for the same row, the last range added will be used.
|
||||||
|
pub fn highlight_rows<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
rows: Range<Anchor>,
|
||||||
|
color: Option<Hsla>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||||
|
match self.highlighted_rows.entry(TypeId::of::<T>()) {
|
||||||
|
hash_map::Entry::Occupied(o) => {
|
||||||
|
let row_highlights = o.into_mut();
|
||||||
|
let existing_highlight_index =
|
||||||
|
row_highlights.binary_search_by(|(_, highlight_range, _)| {
|
||||||
|
highlight_range
|
||||||
|
.start
|
||||||
|
.cmp(&rows.start, &multi_buffer_snapshot)
|
||||||
|
.then(highlight_range.end.cmp(&rows.end, &multi_buffer_snapshot))
|
||||||
|
});
|
||||||
|
match color {
|
||||||
|
Some(color) => {
|
||||||
|
let insert_index = match existing_highlight_index {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(i) => i,
|
||||||
|
};
|
||||||
|
row_highlights.insert(
|
||||||
|
insert_index,
|
||||||
|
(post_inc(&mut self.highlight_order), rows, color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if let Ok(i) = existing_highlight_index {
|
||||||
|
row_highlights.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hash_map::Entry::Vacant(v) => {
|
||||||
|
if let Some(color) = color {
|
||||||
|
v.insert(vec![(post_inc(&mut self.highlight_order), rows, color)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlighted_rows(&self) -> Option<Range<u32>> {
|
/// Clear all anchor ranges for a certain highlight context type, so no corresponding rows will be highlighted.
|
||||||
self.highlighted_rows.clone()
|
pub fn clear_row_highlights<T: 'static>(&mut self) {
|
||||||
|
self.highlighted_rows.remove(&TypeId::of::<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For a highlight given context type, gets all anchor ranges that will be used for row highlighting.
|
||||||
|
pub fn highlighted_rows<T: 'static>(
|
||||||
|
&self,
|
||||||
|
) -> Option<impl Iterator<Item = (&Range<Anchor>, &Hsla)>> {
|
||||||
|
Some(
|
||||||
|
self.highlighted_rows
|
||||||
|
.get(&TypeId::of::<T>())?
|
||||||
|
.iter()
|
||||||
|
.map(|(_, range, color)| (range, color)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict.
|
||||||
|
// Rerturns a map of display rows that are highlighted and their corresponding highlight color.
|
||||||
|
pub fn highlighted_display_rows(&mut self, cx: &mut WindowContext) -> BTreeMap<u32, Hsla> {
|
||||||
|
let snapshot = self.snapshot(cx);
|
||||||
|
let mut used_highlight_orders = HashMap::default();
|
||||||
|
self.highlighted_rows
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
|
||||||
|
.fold(
|
||||||
|
BTreeMap::<u32, Hsla>::new(),
|
||||||
|
|mut unique_rows, (highlight_order, anchor_range, hsla)| {
|
||||||
|
let start_row = anchor_range.start.to_display_point(&snapshot).row();
|
||||||
|
let end_row = anchor_range.end.to_display_point(&snapshot).row();
|
||||||
|
for row in start_row..=end_row {
|
||||||
|
let used_index =
|
||||||
|
used_highlight_orders.entry(row).or_insert(*highlight_order);
|
||||||
|
if highlight_order >= used_index {
|
||||||
|
*used_index = *highlight_order;
|
||||||
|
unique_rows.insert(row, *hsla);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unique_rows
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_background<T: 'static>(
|
pub fn highlight_background<T: 'static>(
|
||||||
|
|
|
@ -665,19 +665,53 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(highlighted_rows) = &layout.highlighted_rows {
|
let mut paint_highlight = |highlight_row_start: u32, highlight_row_end: u32, color| {
|
||||||
let origin = point(
|
let origin = point(
|
||||||
bounds.origin.x,
|
bounds.origin.x,
|
||||||
bounds.origin.y
|
bounds.origin.y
|
||||||
+ (layout.position_map.line_height * highlighted_rows.start as f32)
|
+ (layout.position_map.line_height * highlight_row_start as f32)
|
||||||
- scroll_top,
|
- scroll_top,
|
||||||
);
|
);
|
||||||
let size = size(
|
let size = size(
|
||||||
bounds.size.width,
|
bounds.size.width,
|
||||||
layout.position_map.line_height * highlighted_rows.len() as f32,
|
layout.position_map.line_height
|
||||||
|
* (highlight_row_end + 1 - highlight_row_start) as f32,
|
||||||
);
|
);
|
||||||
let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background;
|
cx.paint_quad(fill(Bounds { origin, size }, color));
|
||||||
cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg));
|
};
|
||||||
|
let mut last_row = None;
|
||||||
|
let mut highlight_row_start = 0u32;
|
||||||
|
let mut highlight_row_end = 0u32;
|
||||||
|
for (&row, &color) in &layout.highlighted_rows {
|
||||||
|
let paint = last_row.map_or(false, |(last_row, last_color)| {
|
||||||
|
last_color != color || last_row + 1 < row
|
||||||
|
});
|
||||||
|
|
||||||
|
if paint {
|
||||||
|
let paint_range_is_unfinished = highlight_row_end == 0;
|
||||||
|
if paint_range_is_unfinished {
|
||||||
|
highlight_row_end = row;
|
||||||
|
last_row = None;
|
||||||
|
}
|
||||||
|
paint_highlight(highlight_row_start, highlight_row_end, color);
|
||||||
|
highlight_row_start = 0;
|
||||||
|
highlight_row_end = 0;
|
||||||
|
if !paint_range_is_unfinished {
|
||||||
|
highlight_row_start = row;
|
||||||
|
last_row = Some((row, color));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if last_row.is_none() {
|
||||||
|
highlight_row_start = row;
|
||||||
|
} else {
|
||||||
|
highlight_row_end = row;
|
||||||
|
}
|
||||||
|
last_row = Some((row, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((row, hsla)) = last_row {
|
||||||
|
highlight_row_end = row;
|
||||||
|
paint_highlight(highlight_row_start, highlight_row_end, hsla);
|
||||||
}
|
}
|
||||||
|
|
||||||
let scroll_left =
|
let scroll_left =
|
||||||
|
@ -2064,7 +2098,7 @@ impl EditorElement {
|
||||||
let mut active_rows = BTreeMap::new();
|
let mut active_rows = BTreeMap::new();
|
||||||
let is_singleton = editor.is_singleton(cx);
|
let is_singleton = editor.is_singleton(cx);
|
||||||
|
|
||||||
let highlighted_rows = editor.highlighted_rows();
|
let highlighted_rows = editor.highlighted_display_rows(cx);
|
||||||
let highlighted_ranges = editor.background_highlights_in_range(
|
let highlighted_ranges = editor.background_highlights_in_range(
|
||||||
start_anchor..end_anchor,
|
start_anchor..end_anchor,
|
||||||
&snapshot.display_snapshot,
|
&snapshot.display_snapshot,
|
||||||
|
@ -3198,7 +3232,7 @@ pub struct LayoutState {
|
||||||
visible_anchor_range: Range<Anchor>,
|
visible_anchor_range: Range<Anchor>,
|
||||||
visible_display_row_range: Range<u32>,
|
visible_display_row_range: Range<u32>,
|
||||||
active_rows: BTreeMap<u32, bool>,
|
active_rows: BTreeMap<u32, bool>,
|
||||||
highlighted_rows: Option<Range<u32>>,
|
highlighted_rows: BTreeMap<u32, Hsla>,
|
||||||
line_numbers: Vec<Option<ShapedLine>>,
|
line_numbers: Vec<Option<ShapedLine>>,
|
||||||
display_hunks: Vec<DisplayDiffHunk>,
|
display_hunks: Vec<DisplayDiffHunk>,
|
||||||
blocks: Vec<BlockLayout>,
|
blocks: Vec<BlockLayout>,
|
||||||
|
|
|
@ -81,8 +81,8 @@ impl Editor {
|
||||||
|
|
||||||
let mut target_top;
|
let mut target_top;
|
||||||
let mut target_bottom;
|
let mut target_bottom;
|
||||||
if let Some(highlighted_rows) = &self.highlighted_rows {
|
if let Some(first_highlighted_row) = &self.highlighted_display_rows(cx).first_entry() {
|
||||||
target_top = highlighted_rows.start as f32;
|
target_top = *first_highlighted_row.key() as f32;
|
||||||
target_bottom = target_top + 1.;
|
target_bottom = target_top + 1.;
|
||||||
} else {
|
} else {
|
||||||
let selections = self.selections.all::<Point>(cx);
|
let selections = self.selections.all::<Point>(cx);
|
||||||
|
@ -205,10 +205,7 @@ impl Editor {
|
||||||
let mut target_left;
|
let mut target_left;
|
||||||
let mut target_right;
|
let mut target_right;
|
||||||
|
|
||||||
if self.highlighted_rows.is_some() {
|
if self.highlighted_rows.is_empty() {
|
||||||
target_left = px(0.);
|
|
||||||
target_right = px(0.);
|
|
||||||
} else {
|
|
||||||
target_left = px(f32::INFINITY);
|
target_left = px(f32::INFINITY);
|
||||||
target_right = px(0.);
|
target_right = px(0.);
|
||||||
for selection in selections {
|
for selection in selections {
|
||||||
|
@ -229,6 +226,9 @@ impl Editor {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
target_left = px(0.);
|
||||||
|
target_right = px(0.);
|
||||||
}
|
}
|
||||||
|
|
||||||
target_right = target_right.min(scroll_width);
|
target_right = target_right.min(scroll_width);
|
||||||
|
|
|
@ -24,3 +24,12 @@ workspace.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
indoc.workspace = true
|
||||||
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
menu.workspace = true
|
||||||
|
project = { workspace = true, features = ["test-support"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
tree-sitter-rust.workspace = true
|
||||||
|
tree-sitter-typescript.workspace = true
|
||||||
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod cursor_position;
|
pub mod cursor_position;
|
||||||
|
|
||||||
use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Editor};
|
use editor::{scroll::Autoscroll, Editor};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
|
actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
|
||||||
FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
|
FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
|
||||||
|
@ -34,6 +34,8 @@ impl FocusableView for GoToLine {
|
||||||
}
|
}
|
||||||
impl EventEmitter<DismissEvent> for GoToLine {}
|
impl EventEmitter<DismissEvent> for GoToLine {}
|
||||||
|
|
||||||
|
enum GoToLineRowHighlights {}
|
||||||
|
|
||||||
impl GoToLine {
|
impl GoToLine {
|
||||||
fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||||
let handle = cx.view().downgrade();
|
let handle = cx.view().downgrade();
|
||||||
|
@ -84,7 +86,7 @@ impl GoToLine {
|
||||||
.update(cx, |_, cx| {
|
.update(cx, |_, cx| {
|
||||||
let scroll_position = self.prev_scroll_position.take();
|
let scroll_position = self.prev_scroll_position.take();
|
||||||
self.active_editor.update(cx, |editor, cx| {
|
self.active_editor.update(cx, |editor, cx| {
|
||||||
editor.highlight_rows(None);
|
editor.clear_row_highlights::<GoToLineRowHighlights>();
|
||||||
if let Some(scroll_position) = scroll_position {
|
if let Some(scroll_position) = scroll_position {
|
||||||
editor.set_scroll_position(scroll_position, cx);
|
editor.set_scroll_position(scroll_position, cx);
|
||||||
}
|
}
|
||||||
|
@ -112,9 +114,13 @@ impl GoToLine {
|
||||||
self.active_editor.update(cx, |active_editor, cx| {
|
self.active_editor.update(cx, |active_editor, cx| {
|
||||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||||
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
||||||
let display_point = point.to_display_point(&snapshot);
|
let anchor = snapshot.buffer_snapshot.anchor_before(point);
|
||||||
let row = display_point.row();
|
active_editor.clear_row_highlights::<GoToLineRowHighlights>();
|
||||||
active_editor.highlight_rows(Some(row..row + 1));
|
active_editor.highlight_rows::<GoToLineRowHighlights>(
|
||||||
|
anchor..anchor,
|
||||||
|
Some(cx.theme().colors().editor_highlighted_line_background),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -207,3 +213,140 @@ impl Render for GoToLine {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use gpui::{TestAppContext, VisualTestContext};
|
||||||
|
use indoc::indoc;
|
||||||
|
use project::{FakeFs, Project};
|
||||||
|
use serde_json::json;
|
||||||
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
"a.rs": indoc!{"
|
||||||
|
struct SingleLine; // display line 0
|
||||||
|
// display line 1
|
||||||
|
struct MultiLine { // display line 2
|
||||||
|
field_1: i32, // display line 3
|
||||||
|
field_2: i32, // display line 4
|
||||||
|
} // display line 5
|
||||||
|
// display line 7
|
||||||
|
struct Another { // display line 8
|
||||||
|
field_1: i32, // display line 9
|
||||||
|
field_2: i32, // display line 10
|
||||||
|
field_3: i32, // display line 11
|
||||||
|
field_4: i32, // display line 12
|
||||||
|
} // display line 13
|
||||||
|
"}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||||
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
let worktree_id = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.project().update(cx, |project, cx| {
|
||||||
|
project.worktrees().next().unwrap().read(cx).id()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let _buffer = project
|
||||||
|
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let editor = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "a.rs"), None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let go_to_line_view = open_go_to_line_view(&workspace, cx);
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
Vec::<u32>::new(),
|
||||||
|
"Initially opened go to line modal should not highlight any rows"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.simulate_input("1");
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
vec![0],
|
||||||
|
"Go to line modal should highlight a row, corresponding to the query"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.simulate_input("8");
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
vec![13],
|
||||||
|
"If the query is too large, the last row should be highlighted"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.dispatch_action(menu::Cancel);
|
||||||
|
drop(go_to_line_view);
|
||||||
|
editor.update(cx, |_, _| {});
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
Vec::<u32>::new(),
|
||||||
|
"After cancelling and closing the modal, no rows should be highlighted"
|
||||||
|
);
|
||||||
|
|
||||||
|
let go_to_line_view = open_go_to_line_view(&workspace, cx);
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
Vec::<u32>::new(),
|
||||||
|
"Reopened modal should not highlight any rows"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.simulate_input("5");
|
||||||
|
assert_eq!(highlighted_display_rows(&editor, cx), vec![4]);
|
||||||
|
|
||||||
|
cx.dispatch_action(menu::Confirm);
|
||||||
|
drop(go_to_line_view);
|
||||||
|
editor.update(cx, |_, _| {});
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
Vec::<u32>::new(),
|
||||||
|
"After confirming and closing the modal, no rows should be highlighted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_go_to_line_view(
|
||||||
|
workspace: &View<Workspace>,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) -> View<GoToLine> {
|
||||||
|
cx.dispatch_action(Toggle::default());
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.active_modal::<GoToLine>(cx).unwrap().clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlighted_display_rows(editor: &View<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.highlighted_display_rows(cx).into_keys().collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let state = AppState::test(cx);
|
||||||
|
language::init(cx);
|
||||||
|
crate::init(cx);
|
||||||
|
editor::init(cx);
|
||||||
|
workspace::init_settings(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
|
state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -28,3 +28,12 @@ workspace.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
indoc.workspace = true
|
||||||
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
menu.workspace = true
|
||||||
|
project = { workspace = true, features = ["test-support"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
tree-sitter-rust.workspace = true
|
||||||
|
tree-sitter-typescript.workspace = true
|
||||||
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use editor::{
|
use editor::{scroll::Autoscroll, Anchor, AnchorRangeExt, Editor, EditorMode};
|
||||||
display_map::ToDisplayPoint, scroll::Autoscroll, Anchor, AnchorRangeExt, DisplayPoint, Editor,
|
|
||||||
EditorMode, ToPoint,
|
|
||||||
};
|
|
||||||
use fuzzy::StringMatch;
|
use fuzzy::StringMatch;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
actions, div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||||
|
@ -121,7 +118,7 @@ impl OutlineViewDelegate {
|
||||||
|
|
||||||
fn restore_active_editor(&mut self, cx: &mut WindowContext) {
|
fn restore_active_editor(&mut self, cx: &mut WindowContext) {
|
||||||
self.active_editor.update(cx, |editor, cx| {
|
self.active_editor.update(cx, |editor, cx| {
|
||||||
editor.highlight_rows(None);
|
editor.clear_row_highlights::<OutlineRowHighlights>();
|
||||||
if let Some(scroll_position) = self.prev_scroll_position {
|
if let Some(scroll_position) = self.prev_scroll_position {
|
||||||
editor.set_scroll_position(scroll_position, cx);
|
editor.set_scroll_position(scroll_position, cx);
|
||||||
}
|
}
|
||||||
|
@ -141,19 +138,20 @@ impl OutlineViewDelegate {
|
||||||
let outline_item = &self.outline.items[selected_match.candidate_id];
|
let outline_item = &self.outline.items[selected_match.candidate_id];
|
||||||
|
|
||||||
self.active_editor.update(cx, |active_editor, cx| {
|
self.active_editor.update(cx, |active_editor, cx| {
|
||||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
active_editor.clear_row_highlights::<OutlineRowHighlights>();
|
||||||
let buffer_snapshot = &snapshot.buffer_snapshot;
|
active_editor.highlight_rows::<OutlineRowHighlights>(
|
||||||
let start = outline_item.range.start.to_point(buffer_snapshot);
|
outline_item.range.clone(),
|
||||||
let end = outline_item.range.end.to_point(buffer_snapshot);
|
Some(cx.theme().colors().editor_highlighted_line_background),
|
||||||
let display_rows = start.to_display_point(&snapshot).row()
|
cx,
|
||||||
..end.to_display_point(&snapshot).row() + 1;
|
);
|
||||||
active_editor.highlight_rows(Some(display_rows));
|
|
||||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum OutlineRowHighlights {}
|
||||||
|
|
||||||
impl PickerDelegate for OutlineViewDelegate {
|
impl PickerDelegate for OutlineViewDelegate {
|
||||||
type ListItem = ListItem;
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
@ -240,13 +238,13 @@ impl PickerDelegate for OutlineViewDelegate {
|
||||||
self.prev_scroll_position.take();
|
self.prev_scroll_position.take();
|
||||||
|
|
||||||
self.active_editor.update(cx, |active_editor, cx| {
|
self.active_editor.update(cx, |active_editor, cx| {
|
||||||
if let Some(rows) = active_editor.highlighted_rows() {
|
if let Some(rows) = active_editor
|
||||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
.highlighted_rows::<OutlineRowHighlights>()
|
||||||
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
|
.and_then(|highlights| highlights.into_iter().next().map(|(rows, _)| rows.clone()))
|
||||||
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
{
|
||||||
s.select_ranges([position..position])
|
active_editor
|
||||||
});
|
.change_selections(Some(Autoscroll::center()), cx, |s| s.select_ranges([rows]));
|
||||||
active_editor.highlight_rows(None);
|
active_editor.clear_row_highlights::<OutlineRowHighlights>();
|
||||||
active_editor.focus(cx);
|
active_editor.focus(cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -314,3 +312,260 @@ impl PickerDelegate for OutlineViewDelegate {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use gpui::{TestAppContext, VisualTestContext};
|
||||||
|
use indoc::indoc;
|
||||||
|
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||||
|
use project::{FakeFs, Project};
|
||||||
|
use serde_json::json;
|
||||||
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_outline_view_row_highlights(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
"a.rs": indoc!{"
|
||||||
|
struct SingleLine; // display line 0
|
||||||
|
// display line 1
|
||||||
|
struct MultiLine { // display line 2
|
||||||
|
field_1: i32, // display line 3
|
||||||
|
field_2: i32, // display line 4
|
||||||
|
} // display line 5
|
||||||
|
"}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||||
|
project.read_with(cx, |project, _| project.languages().add(rust_lang()));
|
||||||
|
|
||||||
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
let worktree_id = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.project().update(cx, |project, cx| {
|
||||||
|
project.worktrees().next().unwrap().read(cx).id()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let _buffer = project
|
||||||
|
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let editor = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "a.rs"), None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
let ensure_outline_view_contents =
|
||||||
|
|outline_view: &View<Picker<OutlineViewDelegate>>, cx: &mut VisualTestContext| {
|
||||||
|
assert_eq!(query(&outline_view, cx), "");
|
||||||
|
assert_eq!(
|
||||||
|
outline_names(&outline_view, cx),
|
||||||
|
vec![
|
||||||
|
"struct SingleLine",
|
||||||
|
"struct MultiLine",
|
||||||
|
"field_1",
|
||||||
|
"field_2"
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let outline_view = open_outline_view(&workspace, cx);
|
||||||
|
ensure_outline_view_contents(&outline_view, cx);
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
Vec::<u32>::new(),
|
||||||
|
"Initially opened outline view should have no highlights"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.dispatch_action(menu::SelectNext);
|
||||||
|
ensure_outline_view_contents(&outline_view, cx);
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
vec![2, 3, 4, 5],
|
||||||
|
"Second struct's rows should be highlighted"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.dispatch_action(menu::SelectPrev);
|
||||||
|
ensure_outline_view_contents(&outline_view, cx);
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
vec![0],
|
||||||
|
"First struct's row should be highlighted"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.dispatch_action(menu::Cancel);
|
||||||
|
ensure_outline_view_contents(&outline_view, cx);
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
Vec::<u32>::new(),
|
||||||
|
"No rows should be highlighted after outline view is cancelled and closed"
|
||||||
|
);
|
||||||
|
|
||||||
|
let outline_view = open_outline_view(&workspace, cx);
|
||||||
|
ensure_outline_view_contents(&outline_view, cx);
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
Vec::<u32>::new(),
|
||||||
|
"Reopened outline view should have no highlights"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.dispatch_action(menu::SelectNext);
|
||||||
|
ensure_outline_view_contents(&outline_view, cx);
|
||||||
|
assert_eq!(highlighted_display_rows(&editor, cx), vec![2, 3, 4, 5]);
|
||||||
|
|
||||||
|
cx.dispatch_action(menu::Confirm);
|
||||||
|
ensure_outline_view_contents(&outline_view, cx);
|
||||||
|
assert_eq!(
|
||||||
|
highlighted_display_rows(&editor, cx),
|
||||||
|
Vec::<u32>::new(),
|
||||||
|
"No rows should be highlighted after outline view is confirmed and closed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_outline_view(
|
||||||
|
workspace: &View<Workspace>,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) -> View<Picker<OutlineViewDelegate>> {
|
||||||
|
cx.dispatch_action(Toggle::default());
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.active_modal::<OutlineView>(cx)
|
||||||
|
.unwrap()
|
||||||
|
.read(cx)
|
||||||
|
.picker
|
||||||
|
.clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query(
|
||||||
|
outline_view: &View<Picker<OutlineViewDelegate>>,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) -> String {
|
||||||
|
outline_view.update(cx, |outline_view, cx| outline_view.query(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outline_names(
|
||||||
|
outline_view: &View<Picker<OutlineViewDelegate>>,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) -> Vec<String> {
|
||||||
|
outline_view.update(cx, |outline_view, _| {
|
||||||
|
let items = &outline_view.delegate.outline.items;
|
||||||
|
outline_view
|
||||||
|
.delegate
|
||||||
|
.matches
|
||||||
|
.iter()
|
||||||
|
.map(|hit| items[hit.candidate_id].text.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlighted_display_rows(editor: &View<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.highlighted_display_rows(cx).into_keys().collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let state = AppState::test(cx);
|
||||||
|
language::init(cx);
|
||||||
|
crate::init(cx);
|
||||||
|
editor::init(cx);
|
||||||
|
workspace::init_settings(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
|
state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rust_lang() -> Arc<Language> {
|
||||||
|
Arc::new(
|
||||||
|
Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
matcher: LanguageMatcher {
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_rust::language()),
|
||||||
|
)
|
||||||
|
.with_outline_query(
|
||||||
|
r#"(struct_item
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
"struct" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(enum_item
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
"enum" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(enum_variant
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(impl_item
|
||||||
|
"impl" @context
|
||||||
|
trait: (_)? @name
|
||||||
|
"for"? @context
|
||||||
|
type: (_) @name) @item
|
||||||
|
|
||||||
|
(trait_item
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
"trait" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(function_item
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
(function_modifiers)? @context
|
||||||
|
"fn" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(function_signature_item
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
(function_modifiers)? @context
|
||||||
|
"fn" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(macro_definition
|
||||||
|
. "macro_rules!" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(mod_item
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
"mod" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(type_item
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
"type" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(associated_type
|
||||||
|
"type" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(const_item
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
"const" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(field_declaration
|
||||||
|
(visibility_modifier)? @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue