use more predictable rules for selecting which bracket to jump to and where

This commit is contained in:
Kay Simmons 2023-02-15 14:04:16 -08:00
parent 5041300b52
commit 0ba051a754
15 changed files with 457 additions and 217 deletions

2
Cargo.lock generated
View File

@ -1956,6 +1956,7 @@ dependencies = [
"tree-sitter-html",
"tree-sitter-javascript",
"tree-sitter-rust",
"tree-sitter-typescript",
"unindent",
"util",
"workspace",
@ -3249,6 +3250,7 @@ dependencies = [
"fuzzy",
"git",
"gpui",
"indoc",
"lazy_static",
"log",
"lsp",

View File

@ -58,6 +58,7 @@ smol = "1.2"
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true }
tree-sitter-javascript = { version = "*", optional = true }
tree-sitter-typescript = { version = "*", optional = true }
[dev-dependencies]
text = { path = "../text", features = ["test-support"] }
@ -75,4 +76,5 @@ unindent = "0.1.7"
tree-sitter = "0.20"
tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"
tree-sitter-typescript = "0.20.1"
tree-sitter-javascript = "0.20"

View File

@ -4752,27 +4752,52 @@ impl Editor {
_: &MoveToEnclosingBracket,
cx: &mut ViewContext<Self>,
) {
let buffer = self.buffer.read(cx).snapshot(cx);
let mut selections = self.selections.all::<usize>(cx);
for selection in &mut selections {
if let Some((open_range, close_range)) =
buffer.enclosing_bracket_ranges(selection.start..selection.end)
{
let close_range = close_range.to_inclusive();
let destination = if close_range.contains(&selection.start)
&& close_range.contains(&selection.end)
{
open_range.end
} else {
*close_range.start()
};
selection.start = destination;
selection.end = destination;
}
}
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(selections);
s.move_offsets_with(|snapshot, selection| {
let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else { return; };
let mut best_length = usize::MAX;
let mut best_inside = false;
let mut best_in_bracket_range = false;
let mut best_destination = None;
for (open, close) in enclosing_bracket_ranges {
let close = close.to_inclusive();
let length = close.end() - open.start;
let inside = selection.start >= open.end && selection.end <= *close.start();
let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head());
// If best is next to a bracket and current isn't, skip
if !in_bracket_range && best_in_bracket_range {
continue;
}
// Prefer smaller lengths unless best is inside and current isn't
if length > best_length && (best_inside || !inside) {
continue;
}
best_length = length;
best_inside = inside;
best_in_bracket_range = in_bracket_range;
best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) {
if inside {
open.end
} else {
open.start
}
} else {
if inside {
*close.start()
} else {
*close.end()
}
});
}
if let Some(destination) = best_destination {
selection.collapse_to(destination, SelectionGoal::None);
}
})
});
}

View File

@ -5459,6 +5459,54 @@ fn test_split_words() {
assert_eq!(split("helloworld"), &["helloworld"]);
}
#[gpui::test]
async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
let mut assert = |before, after| {
let _state_context = cx.set_state(before);
cx.update_editor(|editor, cx| {
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
});
cx.assert_editor_state(after);
};
// Outside bracket jumps to outside of matching bracket
assert("console.logˇ(var);", "console.log(var)ˇ;");
assert("console.log(var)ˇ;", "console.logˇ(var);");
// Inside bracket jumps to inside of matching bracket
assert("console.log(ˇvar);", "console.log(varˇ);");
assert("console.log(varˇ);", "console.log(ˇvar);");
// When outside a bracket and inside, favor jumping to the inside bracket
assert(
"console.log('foo', [1, 2, 3]ˇ);",
"console.log(ˇ'foo', [1, 2, 3]);",
);
assert(
"console.log(ˇ'foo', [1, 2, 3]);",
"console.log('foo', [1, 2, 3]ˇ);",
);
// Bias forward if two options are equally likely
assert(
"let result = curried_fun()ˇ();",
"let result = curried_fun()()ˇ;",
);
// If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
assert(
indoc! {"
function test() {
console.log('test')ˇ
}"},
indoc! {"
function test() {
console.logˇ('test')
}"},
);
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point

View File

@ -17,7 +17,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
let snapshot = editor.snapshot(cx);
if let Some((opening_range, closing_range)) = snapshot
.buffer_snapshot
.enclosing_bracket_ranges(head..head)
.innermost_enclosing_bracket_ranges(head..head)
{
editor.highlight_background::<MatchingBracketHighlight>(
vec![

View File

@ -2621,56 +2621,71 @@ impl MultiBufferSnapshot {
self.parse_count
}
pub fn enclosing_bracket_ranges<T: ToOffset>(
pub fn innermost_enclosing_bracket_ranges<T: ToOffset>(
&self,
range: Range<T>,
) -> Option<(Range<usize>, Range<usize>)> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&range.start, Bias::Right, &());
let start_excerpt = cursor.item();
// Get the ranges of the innermost pair of brackets.
let mut result: Option<(Range<usize>, Range<usize>)> = None;
cursor.seek(&range.end, Bias::Right, &());
let end_excerpt = cursor.item();
let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; };
start_excerpt
.zip(end_excerpt)
.and_then(|(start_excerpt, end_excerpt)| {
if start_excerpt.id != end_excerpt.id {
return None;
for (open, close) in enclosing_bracket_ranges {
let len = close.end - open.start;
if let Some((existing_open, existing_close)) = &result {
let existing_len = existing_close.end - existing_open.start;
if len > existing_len {
continue;
}
}
let excerpt_buffer_start = start_excerpt
.range
.context
.start
.to_offset(&start_excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
result = Some((open, close));
}
result
}
/// Returns enclosingn bracket ranges containing the given range or returns None if the range is not contained in a single excerpt
pub fn enclosing_bracket_ranges<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
self.excerpt_containing(range.clone())
.map(|(excerpt, excerpt_offset)| {
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
let start_in_buffer =
excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
let end_in_buffer =
excerpt_buffer_start + range.end.saturating_sub(*cursor.start());
let (mut start_bracket_range, mut end_bracket_range) = start_excerpt
.buffer
.enclosing_bracket_ranges(start_in_buffer..end_in_buffer)?;
excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
if start_bracket_range.start >= excerpt_buffer_start
&& end_bracket_range.end <= excerpt_buffer_end
{
start_bracket_range.start =
cursor.start() + (start_bracket_range.start - excerpt_buffer_start);
start_bracket_range.end =
cursor.start() + (start_bracket_range.end - excerpt_buffer_start);
end_bracket_range.start =
cursor.start() + (end_bracket_range.start - excerpt_buffer_start);
end_bracket_range.end =
cursor.start() + (end_bracket_range.end - excerpt_buffer_start);
Some((start_bracket_range, end_bracket_range))
} else {
None
}
excerpt
.buffer
.enclosing_bracket_ranges(start_in_buffer..end_in_buffer)
.filter_map(move |(start_bracket_range, end_bracket_range)| {
if start_bracket_range.start < excerpt_buffer_start
|| end_bracket_range.end > excerpt_buffer_end
{
return None;
}
let mut start_bracket_range = start_bracket_range.clone();
start_bracket_range.start =
excerpt_offset + (start_bracket_range.start - excerpt_buffer_start);
start_bracket_range.end =
excerpt_offset + (start_bracket_range.end - excerpt_buffer_start);
let mut end_bracket_range = end_bracket_range.clone();
end_bracket_range.start =
excerpt_offset + (end_bracket_range.start - excerpt_buffer_start);
end_bracket_range.end =
excerpt_offset + (end_bracket_range.end - excerpt_buffer_start);
Some((start_bracket_range, end_bracket_range))
})
})
}
@ -2812,40 +2827,23 @@ impl MultiBufferSnapshot {
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&range.start, Bias::Right, &());
let start_excerpt = cursor.item();
cursor.seek(&range.end, Bias::Right, &());
let end_excerpt = cursor.item();
start_excerpt
.zip(end_excerpt)
.and_then(|(start_excerpt, end_excerpt)| {
if start_excerpt.id != end_excerpt.id {
return None;
}
let excerpt_buffer_start = start_excerpt
.range
.context
.start
.to_offset(&start_excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
self.excerpt_containing(range.clone())
.and_then(|(excerpt, excerpt_offset)| {
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
let start_in_buffer =
excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
let end_in_buffer =
excerpt_buffer_start + range.end.saturating_sub(*cursor.start());
let mut ancestor_buffer_range = start_excerpt
excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
let mut ancestor_buffer_range = excerpt
.buffer
.range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?;
ancestor_buffer_range.start =
cmp::max(ancestor_buffer_range.start, excerpt_buffer_start);
ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end);
let start = cursor.start() + (ancestor_buffer_range.start - excerpt_buffer_start);
let end = cursor.start() + (ancestor_buffer_range.end - excerpt_buffer_start);
let start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start);
let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start);
Some(start..end)
})
}
@ -2929,6 +2927,31 @@ impl MultiBufferSnapshot {
None
}
/// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts
fn excerpt_containing<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<(&'a Excerpt, usize)> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&range.start, Bias::Right, &());
let start_excerpt = cursor.item();
cursor.seek(&range.end, Bias::Right, &());
let end_excerpt = cursor.item();
start_excerpt
.zip(end_excerpt)
.and_then(|(start_excerpt, end_excerpt)| {
if start_excerpt.id != end_excerpt.id {
return None;
}
Some((start_excerpt, *cursor.start()))
})
}
pub fn remote_selections_in_range<'a>(
&'a self,
range: &'a Range<Anchor>,

View File

@ -659,6 +659,31 @@ impl<'a> MutableSelectionsCollection<'a> {
}
}
pub fn move_offsets_with(
&mut self,
mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection<usize>),
) {
let mut changed = false;
let snapshot = self.buffer().clone();
let selections = self
.all::<usize>(self.cx)
.into_iter()
.map(|selection| {
let mut moved_selection = selection.clone();
move_selection(&snapshot, &mut moved_selection);
if selection != moved_selection {
changed = true;
}
moved_selection
})
.collect();
drop(snapshot);
if changed {
self.select(selections)
}
}
pub fn move_heads_with(
&mut self,
mut update_head: impl FnMut(

View File

@ -1,4 +1,5 @@
use std::{
borrow::Cow,
ops::{Deref, DerefMut, Range},
sync::Arc,
};
@ -7,7 +8,8 @@ use anyhow::Result;
use futures::Future;
use gpui::{json, ViewContext, ViewHandle};
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig};
use indoc::indoc;
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
use lsp::{notification, request};
use project::Project;
use smol::stream::StreamExt;
@ -125,6 +127,32 @@ impl<'a> EditorLspTestContext<'a> {
Self::new(language, capabilities, cx).await
}
pub async fn new_typescript(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let language = Language::new(
LanguageConfig {
name: "Typescript".into(),
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
)
.with_queries(LanguageQueries {
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)"#})),
..Default::default()
})
.expect("Could not parse brackets");
Self::new(language, capabilities, cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);

View File

@ -162,10 +162,13 @@ impl<'a> EditorTestContext<'a> {
/// embedded range markers that represent the ranges and directions of
/// each selection.
///
/// Returns a context handle so that assertion failures can print what
/// editor state was needed to cause the failure.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
let _state_context = self.add_assertion_context(format!(
"Editor State: \"{}\"",
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);

View File

@ -1,11 +1,15 @@
#[cfg(any(test, feature = "test-support"))]
use std::sync::Arc;
use lazy_static::lazy_static;
#[cfg(any(test, feature = "test-support"))]
use parking_lot::Mutex;
use collections::{hash_map::Entry, HashMap, HashSet};
use crate::{util::post_inc, ElementStateId};
#[cfg(any(test, feature = "test-support"))]
use crate::util::post_inc;
use crate::ElementStateId;
lazy_static! {
static ref LEAK_BACKTRACE: bool =
@ -30,9 +34,8 @@ pub struct RefCounts {
}
impl RefCounts {
pub fn new(
#[cfg(any(test, feature = "test-support"))] leak_detector: Arc<Mutex<LeakDetector>>,
) -> Self {
#[cfg(any(test, feature = "test-support"))]
pub fn new(leak_detector: Arc<Mutex<LeakDetector>>) -> Self {
Self {
#[cfg(any(test, feature = "test-support"))]
leak_detector,

View File

@ -621,6 +621,8 @@ impl<T: View> ViewHandle<T> {
}
}
/// Tracks string context to be printed when assertions fail.
/// Often this is done by storing a context string in the manager and returning the handle.
#[derive(Clone)]
pub struct AssertionContextManager {
id: Arc<AtomicUsize>,
@ -651,6 +653,9 @@ impl AssertionContextManager {
}
}
/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
/// the state that was set initially for the failure can be printed in the error message
pub struct ContextHandle {
id: usize,
manager: AssertionContextManager,

View File

@ -66,6 +66,7 @@ settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
indoc = "1.0.4"
rand = "0.8.3"
tree-sitter-embedded-template = "*"
tree-sitter-html = "*"

View File

@ -2346,12 +2346,13 @@ impl BufferSnapshot {
Some(items)
}
pub fn enclosing_bracket_ranges<T: ToOffset>(
&self,
pub fn enclosing_bracket_ranges<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<(Range<usize>, Range<usize>)> {
) -> impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a {
// Find bracket pairs that *inclusively* contain the given range.
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut matches = self.syntax.matches(
range.start.saturating_sub(1)..self.len().min(range.end + 1),
&self.text,
@ -2363,39 +2364,31 @@ impl BufferSnapshot {
.map(|grammar| grammar.brackets_config.as_ref().unwrap())
.collect::<Vec<_>>();
// Get the ranges of the innermost pair of brackets.
let mut result: Option<(Range<usize>, Range<usize>)> = None;
while let Some(mat) = matches.peek() {
let mut open = None;
let mut close = None;
let config = &configs[mat.grammar_index];
for capture in mat.captures {
if capture.index == config.open_capture_ix {
open = Some(capture.node.byte_range());
} else if capture.index == config.close_capture_ix {
close = Some(capture.node.byte_range());
iter::from_fn(move || {
while let Some(mat) = matches.peek() {
let mut open = None;
let mut close = None;
let config = &configs[mat.grammar_index];
for capture in mat.captures {
if capture.index == config.open_capture_ix {
open = Some(capture.node.byte_range());
} else if capture.index == config.close_capture_ix {
close = Some(capture.node.byte_range());
}
}
}
matches.advance();
matches.advance();
let Some((open, close)) = open.zip(close) else { continue };
if open.start > range.start || close.end < range.end {
continue;
}
let len = close.end - open.start;
let Some((open, close)) = open.zip(close) else { continue };
if let Some((existing_open, existing_close)) = &result {
let existing_len = existing_close.end - existing_open.start;
if len > existing_len {
if open.start > range.start || close.end < range.end {
continue;
}
return Some((open, close));
}
result = Some((open, close));
}
result
None
})
}
#[allow(clippy::type_complexity)]

View File

@ -3,6 +3,7 @@ use clock::ReplicaId;
use collections::BTreeMap;
use fs::LineEnding;
use gpui::{ModelHandle, MutableAppContext};
use indoc::indoc;
use proto::deserialize_operation;
use rand::prelude::*;
use settings::Settings;
@ -15,7 +16,7 @@ use std::{
};
use text::network::Network;
use unindent::Unindent as _;
use util::{post_inc, test::marked_text_ranges, RandomCharIter};
use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
#[cfg(test)]
#[ctor::ctor]
@ -576,53 +577,117 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
let buffer = cx.add_model(|cx| {
let text = "
mod x {
mod y {
let mut assert = |selection_text, range_markers| {
assert_enclosing_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
};
assert(
indoc! {"
mod x {
moˇd y {
}
}
"
.unindent();
Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)
});
let buffer = buffer.read(cx);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
Some((
Point::new(0, 6)..Point::new(0, 7),
Point::new(4, 0)..Point::new(4, 1)
))
);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
Some((
Point::new(1, 10)..Point::new(1, 11),
Point::new(3, 4)..Point::new(3, 5)
))
);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
Some((
Point::new(1, 10)..Point::new(1, 11),
Point::new(3, 4)..Point::new(3, 5)
))
let foo = 1;"},
vec![indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"}],
);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(4, 1)),
Some((
Point::new(0, 6)..Point::new(0, 7),
Point::new(4, 0)..Point::new(4, 1)
))
assert(
indoc! {"
mod x {
mod y ˇ{
}
}
let foo = 1;"},
vec![
indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"},
indoc! {"
mod x {
mod y «{»
«}»
}
let foo = 1;"},
],
);
assert(
indoc! {"
mod x {
mod y {
}ˇ
}
let foo = 1;"},
vec![
indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"},
indoc! {"
mod x {
mod y «{»
«}»
}
let foo = 1;"},
],
);
assert(
indoc! {"
mod x {
mod y {
}
ˇ}
let foo = 1;"},
vec![indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"}],
);
assert(
indoc! {"
mod x {
mod y {
}
}
let fˇoo = 1;"},
vec![],
);
// Regression test: avoid crash when querying at the end of the buffer.
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)),
None
assert(
indoc! {"
mod x {
mod y {
}
}
let foo = 1;ˇ"},
vec![],
);
}
@ -630,52 +695,33 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
cx: &mut MutableAppContext,
) {
let javascript_language = Arc::new(
Language::new(
LanguageConfig {
name: "JavaScript".into(),
..Default::default()
},
Some(tree_sitter_javascript::language()),
)
.with_brackets_query(
r#"
("{" @open "}" @close)
("(" @open ")" @close)
"#,
)
.unwrap(),
);
let mut assert = |selection_text, bracket_pair_texts| {
assert_enclosing_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx)
};
cx.set_global(Settings::test(cx));
let buffer = cx.add_model(|cx| {
let text = "
for (const a in b) {
// a comment that's longer than the for-loop header
}
"
.unindent();
Buffer::new(0, text, cx).with_language(javascript_language, cx)
});
let buffer = buffer.read(cx);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(0, 18)..Point::new(0, 18)),
Some((
Point::new(0, 4)..Point::new(0, 5),
Point::new(0, 17)..Point::new(0, 18)
))
assert(
indoc! {"
for (const a in b)ˇ {
// a comment that's longer than the for-loop header
}"},
vec![indoc! {"
for «(»const a in b«)» {
// a comment that's longer than the for-loop header
}"}],
);
// Regression test: even though the parent node of the parentheses (the for loop) does
// intersect the given range, the parentheses themselves do not contain the range, so
// they should not be returned. Only the curly braces contain the range.
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)),
Some((
Point::new(0, 19)..Point::new(0, 20),
Point::new(2, 0)..Point::new(2, 1)
))
assert(
indoc! {"
for (const a in b) {ˇ
// a comment that's longer than the for-loop header
}"},
vec![indoc! {"
for (const a in b) «{»
// a comment that's longer than the for-loop header
«}»"}],
);
}
@ -1892,21 +1938,6 @@ fn test_contiguous_ranges() {
);
}
impl Buffer {
pub fn enclosing_bracket_point_ranges<T: ToOffset>(
&self,
range: Range<T>,
) -> Option<(Range<Point>, Range<Point>)> {
self.snapshot()
.enclosing_bracket_ranges(range)
.map(|(start, end)| {
let point_start = start.start.to_point(self)..start.end.to_point(self);
let point_end = end.start.to_point(self)..end.end.to_point(self);
(point_start, point_end)
})
}
}
fn ruby_lang() -> Language {
Language::new(
LanguageConfig {
@ -1990,6 +2021,23 @@ fn json_lang() -> Language {
)
}
fn javascript_lang() -> Language {
Language::new(
LanguageConfig {
name: "JavaScript".into(),
..Default::default()
},
Some(tree_sitter_javascript::language()),
)
.with_brackets_query(
r#"
("{" @open "}" @close)
("(" @open ")" @close)
"#,
)
.unwrap()
}
fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
@ -1997,3 +2045,36 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
layers[0].node.to_sexp()
})
}
// Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers`
fn assert_enclosing_bracket_pairs(
selection_text: &'static str,
bracket_pair_texts: Vec<&'static str>,
language: Language,
cx: &mut MutableAppContext,
) {
cx.set_global(Settings::test(cx));
let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
let buffer = cx.add_model(|cx| {
Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx)
});
let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot());
let selection_range = selection_ranges[0].clone();
let bracket_pairs = bracket_pair_texts
.into_iter()
.map(|pair_text| {
let (bracket_text, ranges) = marked_text_ranges(pair_text, false);
assert_eq!(bracket_text, expected_text);
(ranges[0].clone(), ranges[1].clone())
})
.collect::<Vec<_>>();
assert_set_eq!(
buffer
.enclosing_bracket_ranges(selection_range)
.collect::<Vec<_>>(),
bracket_pairs
);
}

View File

@ -452,8 +452,9 @@ fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> D
fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let offset = point.to_offset(map, Bias::Left);
if let Some((open_range, close_range)) =
map.buffer_snapshot.enclosing_bracket_ranges(offset..offset)
if let Some((open_range, close_range)) = map
.buffer_snapshot
.innermost_enclosing_bracket_ranges(offset..offset)
{
if open_range.contains(&offset) {
close_range.start.to_display_point(map)