highlight both brackets, only when empty selection, and add test

This commit is contained in:
Keith Simmons 2022-07-05 15:19:05 -07:00
parent 9b36e3d009
commit 1f3dc2f534
6 changed files with 201 additions and 50 deletions

View file

@ -5372,7 +5372,7 @@ impl Editor {
.map(|h| &h.1); .map(|h| &h.1);
let write_highlights = self let write_highlights = self
.background_highlights .background_highlights
.get(&TypeId::of::<DocumentHighlightRead>()) .get(&TypeId::of::<DocumentHighlightWrite>())
.map(|h| &h.1); .map(|h| &h.1);
let left_position = position.bias_left(buffer); let left_position = position.bias_left(buffer);
let right_position = position.bias_right(buffer); let right_position = position.bias_right(buffer);
@ -10281,3 +10281,13 @@ impl<T: Ord + Clone> RangeExt<T> for Range<T> {
self.start.clone()..=self.end.clone() self.start.clone()..=self.end.clone()
} }
} }
trait RangeToAnchorExt {
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
}
impl<T: ToOffset> RangeToAnchorExt for Range<T> {
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor> {
snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end)
}
}

View file

@ -1,6 +1,6 @@
use gpui::ViewContext; use gpui::ViewContext;
use crate::Editor; use crate::{Editor, RangeToAnchorExt};
enum MatchingBracketHighlight {} enum MatchingBracketHighlight {}
@ -8,33 +8,135 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
editor.clear_background_highlights::<MatchingBracketHighlight>(cx); editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
let newest_selection = editor.selections.newest::<usize>(cx); let newest_selection = editor.selections.newest::<usize>(cx);
// Don't highlight brackets if the selection isn't empty
if !newest_selection.is_empty() {
return;
}
let head = newest_selection.head();
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
if let Some((opening_range, closing_range)) = snapshot if let Some((opening_range, closing_range)) = snapshot
.buffer_snapshot .buffer_snapshot
.enclosing_bracket_ranges(newest_selection.range()) .enclosing_bracket_ranges(head..head)
{ {
let head = newest_selection.head(); editor.highlight_background::<MatchingBracketHighlight>(
let range_to_highlight = if opening_range.contains(&head) { vec![
Some(closing_range) opening_range.to_anchors(&snapshot.buffer_snapshot),
} else if closing_range.contains(&head) { closing_range.to_anchors(&snapshot.buffer_snapshot),
Some(opening_range) ],
} else { |theme| theme.editor.document_highlight_read_background,
None cx,
}; )
}
if let Some(range_to_highlight) = range_to_highlight { }
let anchor_range = snapshot
.buffer_snapshot #[cfg(test)]
.anchor_before(range_to_highlight.start) mod tests {
..snapshot use indoc::indoc;
.buffer_snapshot
.anchor_after(range_to_highlight.end); use language::{BracketPair, Language, LanguageConfig};
editor.highlight_background::<MatchingBracketHighlight>( use crate::test::EditorLspTestContext;
vec![anchor_range],
|theme| theme.editor.document_highlight_read_background, use super::*;
cx,
) #[gpui::test]
} async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
let mut cx = EditorLspTestContext::new(
Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
brackets: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: false,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: false,
newline: true,
},
],
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_brackets_query(indoc! {r#"
("{" @open "}" @close)
("(" @open ")" @close)
"#})
.unwrap(),
Default::default(),
cx,
)
.await;
// positioning cursor inside bracket highlights both
cx.set_state_by(
vec!['|'.into()],
indoc! {r#"
pub fn test("Test |argument") {
another_test(1, 2, 3);
}"#},
);
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test[(]"Test argument"[)] {
another_test(1, 2, 3);
}"#});
cx.set_state_by(
vec!['|'.into()],
indoc! {r#"
pub fn test("Test argument") {
another_test(1, |2, 3);
}"#},
);
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test[(]1, 2, 3[)];
}"#});
cx.set_state_by(
vec!['|'.into()],
indoc! {r#"
pub fn test("Test argument") {
another|_test(1, 2, 3);
}"#},
);
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") [{]
another_test(1, 2, 3);
[}]"#});
// positioning outside of brackets removes highlight
cx.set_state_by(
vec!['|'.into()],
indoc! {r#"
pub f|n test("Test argument") {
another_test(1, 2, 3);
}"#},
);
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test(1, 2, 3);
}"#});
// non empty selection dismisses highlight
// positioning outside of brackets removes highlight
cx.set_state_by(
vec![('<', '>').into()],
indoc! {r#"
pub fn test("Te<st arg>ument") {
another_test(1, 2, 3);
}"#},
);
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test(1, 2, 3);
}"#});
} }
} }

View file

@ -2321,7 +2321,7 @@ impl MultiBufferSnapshot {
.enclosing_bracket_ranges(start_in_buffer..end_in_buffer)?; .enclosing_bracket_ranges(start_in_buffer..end_in_buffer)?;
if start_bracket_range.start >= excerpt_buffer_start if start_bracket_range.start >= excerpt_buffer_start
&& end_bracket_range.end < excerpt_buffer_end && end_bracket_range.end <= excerpt_buffer_end
{ {
start_bracket_range.start = start_bracket_range.start =
cursor.start() + (start_bracket_range.start - excerpt_buffer_start); cursor.start() + (start_bracket_range.start - excerpt_buffer_start);

View file

@ -1,4 +1,5 @@
use std::{ use std::{
any::TypeId,
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
sync::Arc, sync::Arc,
}; };
@ -13,7 +14,7 @@ use project::Project;
use settings::Settings; use settings::Settings;
use util::{ use util::{
assert_set_eq, set_eq, assert_set_eq, set_eq,
test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError}, test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError, TextRangeMarker},
}; };
use workspace::{pane, AppState, Workspace, WorkspaceHandle}; use workspace::{pane, AppState, Workspace, WorkspaceHandle};
@ -159,29 +160,30 @@ impl<'a> EditorTestContext<'a> {
// `[` to `}` represents a non empty selection with the head at `}` // `[` to `}` represents a non empty selection with the head at `}`
// `{` to `]` represents a non empty selection with the head at `{` // `{` to `]` represents a non empty selection with the head at `{`
pub fn set_state(&mut self, text: &str) { pub fn set_state(&mut self, text: &str) {
self.set_state_by(
vec![
'|'.into(),
('[', '}').into(),
TextRangeMarker::ReverseRange('{', ']'),
],
text,
);
}
pub fn set_state_by(&mut self, range_markers: Vec<TextRangeMarker>, text: &str) {
self.editor.update(self.cx, |editor, cx| { self.editor.update(self.cx, |editor, cx| {
let (unmarked_text, mut selection_ranges) = marked_text_ranges_by( let (unmarked_text, selection_ranges) = marked_text_ranges_by(&text, range_markers);
&text,
vec!['|'.into(), ('[', '}').into(), ('{', ']').into()],
);
editor.set_text(unmarked_text, cx); editor.set_text(unmarked_text, cx);
let mut selections: Vec<Range<usize>> = let selection_ranges: Vec<Range<usize>> = selection_ranges
selection_ranges.remove(&'|'.into()).unwrap_or_default(); .values()
selections.extend( .into_iter()
selection_ranges .flatten()
.remove(&('{', ']').into()) .cloned()
.unwrap_or_default() .collect();
.into_iter() editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
.map(|range| range.end..range.start), s.select_ranges(selection_ranges)
); })
selections.extend(
selection_ranges
.remove(&('[', '}').into())
.unwrap_or_default(),
);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.select_ranges(selections));
}) })
} }
@ -216,6 +218,26 @@ impl<'a> EditorTestContext<'a> {
) )
} }
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
assert_eq!(unmarked, self.buffer_text());
let asserted_ranges = ranges.remove(&('[', ']').into()).unwrap();
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
editor
.background_highlights
.get(&TypeId::of::<Tag>())
.map(|h| h.1.clone())
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect()
});
assert_set_eq!(asserted_ranges, actual_ranges);
}
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) { pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]); let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
assert_eq!(unmarked, self.buffer_text()); assert_eq!(unmarked, self.buffer_text());

View file

@ -51,10 +51,10 @@ macro_rules! assert_set_eq {
match set_eq!(&left, &right) { match set_eq!(&left, &right) {
Err(SetEqError::LeftMissing(missing)) => { Err(SetEqError::LeftMissing(missing)) => {
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing); panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing);
}, },
Err(SetEqError::RightMissing(missing)) => { Err(SetEqError::RightMissing(missing)) => {
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing); panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing);
}, },
_ => {} _ => {}
} }

View file

@ -28,6 +28,7 @@ pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
pub enum TextRangeMarker { pub enum TextRangeMarker {
Empty(char), Empty(char),
Range(char, char), Range(char, char),
ReverseRange(char, char),
} }
impl TextRangeMarker { impl TextRangeMarker {
@ -35,6 +36,7 @@ impl TextRangeMarker {
match self { match self {
Self::Empty(m) => vec![*m], Self::Empty(m) => vec![*m],
Self::Range(l, r) => vec![*l, *r], Self::Range(l, r) => vec![*l, *r],
Self::ReverseRange(l, r) => vec![*l, *r],
} }
} }
} }
@ -85,6 +87,21 @@ pub fn marked_text_ranges_by(
.collect::<Vec<Range<usize>>>(); .collect::<Vec<Range<usize>>>();
(marker, ranges) (marker, ranges)
} }
TextRangeMarker::ReverseRange(start_marker, end_marker) => {
let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
let ranges = starts
.into_iter()
.zip(ends)
.map(|(start, end)| {
assert!(start >= end, "marked ranges must be disjoint");
end..start
})
.collect::<Vec<Range<usize>>>();
(marker, ranges)
}
}) })
.collect(); .collect();