@@ -2352,6 +2352,61 @@ fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoin
None
}
+const BRACKET_PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
+
+fn get_bracket_pair(ch: char) -> Option<(char, char, bool)> {
+ for (open, close) in BRACKET_PAIRS {
+ if ch == open {
+ return Some((open, close, true));
+ }
+ if ch == close {
+ return Some((open, close, false));
+ }
+ }
+ None
+}
+
+fn find_matching_bracket_text_based(
+ map: &DisplaySnapshot,
+ offset: MultiBufferOffset,
+ line_range: Range<MultiBufferOffset>,
+) -> Option<MultiBufferOffset> {
+ let bracket_info = map
+ .buffer_chars_at(offset)
+ .take_while(|(_, char_offset)| *char_offset < line_range.end)
+ .find_map(|(ch, char_offset)| get_bracket_pair(ch).map(|info| (info, char_offset)));
+
+ let (open, close, is_opening) = bracket_info?.0;
+ let bracket_offset = bracket_info?.1;
+
+ let mut depth = 0i32;
+ if is_opening {
+ for (ch, char_offset) in map.buffer_chars_at(bracket_offset) {
+ if ch == open {
+ depth += 1;
+ } else if ch == close {
+ depth -= 1;
+ if depth == 0 {
+ return Some(char_offset);
+ }
+ }
+ }
+ } else {
+ for (ch, char_offset) in map.reverse_buffer_chars_at(bracket_offset + close.len_utf8()) {
+ if ch == close {
+ depth += 1;
+ } else if ch == open {
+ depth -= 1;
+ if depth == 0 {
+ return Some(char_offset);
+ }
+ }
+ }
+ }
+
+ None
+}
+
fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
if !map.is_singleton() {
return display_point;
@@ -2398,10 +2453,10 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
let line_range = map.prev_line_boundary(point).0..line_end;
let visible_line_range =
line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
+ let line_range = line_range.start.to_offset(&map.buffer_snapshot())
+ ..line_range.end.to_offset(&map.buffer_snapshot());
let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range);
if let Some(ranges) = ranges {
- let line_range = line_range.start.to_offset(&map.buffer_snapshot())
- ..line_range.end.to_offset(&map.buffer_snapshot());
let mut closest_pair_destination = None;
let mut closest_distance = usize::MAX;
@@ -2447,9 +2502,15 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
closest_pair_destination
.map(|destination| destination.to_display_point(map))
- .unwrap_or(display_point)
+ .unwrap_or_else(|| {
+ find_matching_bracket_text_based(map, offset, line_range.clone())
+ .map(|o| o.to_display_point(map))
+ .unwrap_or(display_point)
+ })
} else {
- display_point
+ find_matching_bracket_text_based(map, offset, line_range)
+ .map(|o| o.to_display_point(map))
+ .unwrap_or(display_point)
}
}
@@ -1866,6 +1866,24 @@ mod test {
.assert_matches();
}
+ #[gpui::test]
+ async fn test_percent_in_comment(cx: &mut TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.simulate_at_each_offset("%", "// ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
+ .await
+ .assert_matches();
+ cx.simulate_at_each_offset("%", "// ˇ{ ˇ{ˇ}ˇ }ˇ")
+ .await
+ .assert_matches();
+ // Template-style brackets (like Liquid {% %} and {{ }})
+ cx.simulate_at_each_offset("%", "ˇ{ˇ% block %ˇ}ˇ")
+ .await
+ .assert_matches();
+ cx.simulate_at_each_offset("%", "ˇ{ˇ{ˇ var ˇ}ˇ}ˇ")
+ .await
+ .assert_matches();
+ }
+
#[gpui::test]
async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -0,0 +1,62 @@
+{"Put":{"state":"// ˇconsole.log(var);"}}
+{"Key":"%"}
+{"Get":{"state":"// console.log(varˇ);","mode":"Normal"}}
+{"Put":{"state":"// console.logˇ(var);"}}
+{"Key":"%"}
+{"Get":{"state":"// console.log(varˇ);","mode":"Normal"}}
+{"Put":{"state":"// console.log(ˇvar);"}}
+{"Key":"%"}
+{"Get":{"state":"// console.logˇ(var);","mode":"Normal"}}
+{"Put":{"state":"// console.log(vaˇr);"}}
+{"Key":"%"}
+{"Get":{"state":"// console.logˇ(var);","mode":"Normal"}}
+{"Put":{"state":"// console.log(varˇ);"}}
+{"Key":"%"}
+{"Get":{"state":"// console.logˇ(var);","mode":"Normal"}}
+{"Put":{"state":"// console.log(var)ˇ;"}}
+{"Key":"%"}
+{"Get":{"state":"// console.log(var)ˇ;","mode":"Normal"}}
+{"Put":{"state":"// ˇ{ {} }"}}
+{"Key":"%"}
+{"Get":{"state":"// { {} ˇ}","mode":"Normal"}}
+{"Put":{"state":"// { ˇ{} }"}}
+{"Key":"%"}
+{"Get":{"state":"// { {ˇ} }","mode":"Normal"}}
+{"Key":"%"}
+{"Get":{"state":"// { ˇ{} }","mode":"Normal"}}
+{"Put":{"state":"// { {}ˇ }"}}
+{"Key":"%"}
+{"Get":{"state":"// ˇ{ {} }","mode":"Normal"}}
+{"Put":{"state":"// { {} }ˇ"}}
+{"Key":"%"}
+{"Get":{"state":"// ˇ{ {} }","mode":"Normal"}}
+{"Put":{"state":"ˇ{% block %}"}}
+{"Key":"%"}
+{"Get":{"state":"{% block %ˇ}","mode":"Normal"}}
+{"Put":{"state":"{ˇ% block %}"}}
+{"Key":"%"}
+{"Get":{"state":"ˇ{% block %}","mode":"Normal"}}
+{"Put":{"state":"{% block %ˇ}"}}
+{"Key":"%"}
+{"Get":{"state":"ˇ{% block %}","mode":"Normal"}}
+{"Put":{"state":"{% block %}ˇ"}}
+{"Key":"%"}
+{"Get":{"state":"ˇ{% block %}","mode":"Normal"}}
+{"Put":{"state":"ˇ{{ var }}"}}
+{"Key":"%"}
+{"Get":{"state":"{{ var }ˇ}","mode":"Normal"}}
+{"Put":{"state":"{ˇ{ var }}"}}
+{"Key":"%"}
+{"Get":{"state":"{{ var ˇ}}","mode":"Normal"}}
+{"Put":{"state":"{{ˇ var }}"}}
+{"Key":"%"}
+{"Get":{"state":"{ˇ{ var }}","mode":"Normal"}}
+{"Put":{"state":"{{ var ˇ}}"}}
+{"Key":"%"}
+{"Get":{"state":"{ˇ{ var }}","mode":"Normal"}}
+{"Put":{"state":"{{ var }ˇ}"}}
+{"Key":"%"}
+{"Get":{"state":"ˇ{{ var }}","mode":"Normal"}}
+{"Put":{"state":"{{ var }}ˇ"}}
+{"Key":"%"}
+{"Get":{"state":"ˇ{{ var }}","mode":"Normal"}}