use more predictable rules for selecting which bracket to jump to and where
This commit is contained in:
parent
5041300b52
commit
0ba051a754
15 changed files with 457 additions and 217 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1956,6 +1956,7 @@ dependencies = [
|
||||||
"tree-sitter-html",
|
"tree-sitter-html",
|
||||||
"tree-sitter-javascript",
|
"tree-sitter-javascript",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
|
"tree-sitter-typescript",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
@ -3249,6 +3250,7 @@ dependencies = [
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"git",
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"indoc",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
|
|
@ -58,6 +58,7 @@ smol = "1.2"
|
||||||
tree-sitter-rust = { version = "*", optional = true }
|
tree-sitter-rust = { version = "*", optional = true }
|
||||||
tree-sitter-html = { version = "*", optional = true }
|
tree-sitter-html = { version = "*", optional = true }
|
||||||
tree-sitter-javascript = { version = "*", optional = true }
|
tree-sitter-javascript = { version = "*", optional = true }
|
||||||
|
tree-sitter-typescript = { version = "*", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
text = { path = "../text", features = ["test-support"] }
|
text = { path = "../text", features = ["test-support"] }
|
||||||
|
@ -75,4 +76,5 @@ unindent = "0.1.7"
|
||||||
tree-sitter = "0.20"
|
tree-sitter = "0.20"
|
||||||
tree-sitter-rust = "0.20"
|
tree-sitter-rust = "0.20"
|
||||||
tree-sitter-html = "0.19"
|
tree-sitter-html = "0.19"
|
||||||
|
tree-sitter-typescript = "0.20.1"
|
||||||
tree-sitter-javascript = "0.20"
|
tree-sitter-javascript = "0.20"
|
||||||
|
|
|
@ -4752,27 +4752,52 @@ impl Editor {
|
||||||
_: &MoveToEnclosingBracket,
|
_: &MoveToEnclosingBracket,
|
||||||
cx: &mut ViewContext<Self>,
|
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| {
|
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);
|
||||||
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5459,6 +5459,54 @@ fn test_split_words() {
|
||||||
assert_eq!(split("helloworld"), &["helloworld"]);
|
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> {
|
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||||
let point = DisplayPoint::new(row as u32, column as u32);
|
let point = DisplayPoint::new(row as u32, column as u32);
|
||||||
point..point
|
point..point
|
||||||
|
|
|
@ -17,7 +17,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
||||||
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(head..head)
|
.innermost_enclosing_bracket_ranges(head..head)
|
||||||
{
|
{
|
||||||
editor.highlight_background::<MatchingBracketHighlight>(
|
editor.highlight_background::<MatchingBracketHighlight>(
|
||||||
vec![
|
vec![
|
||||||
|
|
|
@ -2621,56 +2621,71 @@ impl MultiBufferSnapshot {
|
||||||
self.parse_count
|
self.parse_count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enclosing_bracket_ranges<T: ToOffset>(
|
pub fn innermost_enclosing_bracket_ranges<T: ToOffset>(
|
||||||
&self,
|
&self,
|
||||||
range: Range<T>,
|
range: Range<T>,
|
||||||
) -> Option<(Range<usize>, Range<usize>)> {
|
) -> Option<(Range<usize>, Range<usize>)> {
|
||||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
|
||||||
let mut cursor = self.excerpts.cursor::<usize>();
|
// Get the ranges of the innermost pair of brackets.
|
||||||
cursor.seek(&range.start, Bias::Right, &());
|
let mut result: Option<(Range<usize>, Range<usize>)> = None;
|
||||||
let start_excerpt = cursor.item();
|
|
||||||
|
|
||||||
cursor.seek(&range.end, Bias::Right, &());
|
let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; };
|
||||||
let end_excerpt = cursor.item();
|
|
||||||
|
|
||||||
start_excerpt
|
for (open, close) in enclosing_bracket_ranges {
|
||||||
.zip(end_excerpt)
|
let len = close.end - open.start;
|
||||||
.and_then(|(start_excerpt, end_excerpt)| {
|
|
||||||
if start_excerpt.id != end_excerpt.id {
|
if let Some((existing_open, existing_close)) = &result {
|
||||||
return None;
|
let existing_len = existing_close.end - existing_open.start;
|
||||||
|
if len > existing_len {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let excerpt_buffer_start = start_excerpt
|
result = Some((open, close));
|
||||||
.range
|
}
|
||||||
.context
|
|
||||||
.start
|
result
|
||||||
.to_offset(&start_excerpt.buffer);
|
}
|
||||||
let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
|
|
||||||
|
/// 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 =
|
let start_in_buffer =
|
||||||
excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
|
excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
|
||||||
let end_in_buffer =
|
let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
|
||||||
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)?;
|
|
||||||
|
|
||||||
if start_bracket_range.start >= excerpt_buffer_start
|
excerpt
|
||||||
&& end_bracket_range.end <= excerpt_buffer_end
|
.buffer
|
||||||
{
|
.enclosing_bracket_ranges(start_in_buffer..end_in_buffer)
|
||||||
start_bracket_range.start =
|
.filter_map(move |(start_bracket_range, end_bracket_range)| {
|
||||||
cursor.start() + (start_bracket_range.start - excerpt_buffer_start);
|
if start_bracket_range.start < excerpt_buffer_start
|
||||||
start_bracket_range.end =
|
|| end_bracket_range.end > excerpt_buffer_end
|
||||||
cursor.start() + (start_bracket_range.end - excerpt_buffer_start);
|
{
|
||||||
end_bracket_range.start =
|
return None;
|
||||||
cursor.start() + (end_bracket_range.start - excerpt_buffer_start);
|
}
|
||||||
end_bracket_range.end =
|
|
||||||
cursor.start() + (end_bracket_range.end - excerpt_buffer_start);
|
let mut start_bracket_range = start_bracket_range.clone();
|
||||||
Some((start_bracket_range, end_bracket_range))
|
start_bracket_range.start =
|
||||||
} else {
|
excerpt_offset + (start_bracket_range.start - excerpt_buffer_start);
|
||||||
None
|
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>> {
|
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 range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
|
||||||
let mut cursor = self.excerpts.cursor::<usize>();
|
self.excerpt_containing(range.clone())
|
||||||
cursor.seek(&range.start, Bias::Right, &());
|
.and_then(|(excerpt, excerpt_offset)| {
|
||||||
let start_excerpt = cursor.item();
|
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
|
||||||
|
let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
|
||||||
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;
|
|
||||||
|
|
||||||
let start_in_buffer =
|
let start_in_buffer =
|
||||||
excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
|
excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
|
||||||
let end_in_buffer =
|
let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
|
||||||
excerpt_buffer_start + range.end.saturating_sub(*cursor.start());
|
let mut ancestor_buffer_range = excerpt
|
||||||
let mut ancestor_buffer_range = start_excerpt
|
|
||||||
.buffer
|
.buffer
|
||||||
.range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?;
|
.range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?;
|
||||||
ancestor_buffer_range.start =
|
ancestor_buffer_range.start =
|
||||||
cmp::max(ancestor_buffer_range.start, excerpt_buffer_start);
|
cmp::max(ancestor_buffer_range.start, excerpt_buffer_start);
|
||||||
ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end);
|
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 start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start);
|
||||||
let end = cursor.start() + (ancestor_buffer_range.end - excerpt_buffer_start);
|
let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start);
|
||||||
Some(start..end)
|
Some(start..end)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2929,6 +2927,31 @@ impl MultiBufferSnapshot {
|
||||||
None
|
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>(
|
pub fn remote_selections_in_range<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
range: &'a Range<Anchor>,
|
range: &'a Range<Anchor>,
|
||||||
|
|
|
@ -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(
|
pub fn move_heads_with(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut update_head: impl FnMut(
|
mut update_head: impl FnMut(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
ops::{Deref, DerefMut, Range},
|
ops::{Deref, DerefMut, Range},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
@ -7,7 +8,8 @@ use anyhow::Result;
|
||||||
|
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use gpui::{json, ViewContext, ViewHandle};
|
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 lsp::{notification, request};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
|
@ -125,6 +127,32 @@ impl<'a> EditorLspTestContext<'a> {
|
||||||
Self::new(language, capabilities, cx).await
|
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
|
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||||
let ranges = self.ranges(marked_text);
|
let ranges = self.ranges(marked_text);
|
||||||
|
|
|
@ -162,10 +162,13 @@ impl<'a> EditorTestContext<'a> {
|
||||||
/// embedded range markers that represent the ranges and directions of
|
/// embedded range markers that represent the ranges and directions of
|
||||||
/// each selection.
|
/// 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.
|
/// See the `util::test::marked_text_ranges` function for more information.
|
||||||
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
|
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||||
let _state_context = self.add_assertion_context(format!(
|
let _state_context = self.add_assertion_context(format!(
|
||||||
"Editor State: \"{}\"",
|
"Initial Editor State: \"{}\"",
|
||||||
marked_text.escape_debug().to_string()
|
marked_text.escape_debug().to_string()
|
||||||
));
|
));
|
||||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
use collections::{hash_map::Entry, HashMap, HashSet};
|
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! {
|
lazy_static! {
|
||||||
static ref LEAK_BACKTRACE: bool =
|
static ref LEAK_BACKTRACE: bool =
|
||||||
|
@ -30,9 +34,8 @@ pub struct RefCounts {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RefCounts {
|
impl RefCounts {
|
||||||
pub fn new(
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
#[cfg(any(test, feature = "test-support"))] leak_detector: Arc<Mutex<LeakDetector>>,
|
pub fn new(leak_detector: Arc<Mutex<LeakDetector>>) -> Self {
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
leak_detector,
|
leak_detector,
|
||||||
|
|
|
@ -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)]
|
#[derive(Clone)]
|
||||||
pub struct AssertionContextManager {
|
pub struct AssertionContextManager {
|
||||||
id: Arc<AtomicUsize>,
|
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 {
|
pub struct ContextHandle {
|
||||||
id: usize,
|
id: usize,
|
||||||
manager: AssertionContextManager,
|
manager: AssertionContextManager,
|
||||||
|
|
|
@ -66,6 +66,7 @@ settings = { path = "../settings", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
ctor = "0.1"
|
ctor = "0.1"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
|
indoc = "1.0.4"
|
||||||
rand = "0.8.3"
|
rand = "0.8.3"
|
||||||
tree-sitter-embedded-template = "*"
|
tree-sitter-embedded-template = "*"
|
||||||
tree-sitter-html = "*"
|
tree-sitter-html = "*"
|
||||||
|
|
|
@ -2346,12 +2346,13 @@ impl BufferSnapshot {
|
||||||
Some(items)
|
Some(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enclosing_bracket_ranges<T: ToOffset>(
|
pub fn enclosing_bracket_ranges<'a, T: ToOffset>(
|
||||||
&self,
|
&'a self,
|
||||||
range: Range<T>,
|
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.
|
// Find bracket pairs that *inclusively* contain the given range.
|
||||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
|
||||||
let mut matches = self.syntax.matches(
|
let mut matches = self.syntax.matches(
|
||||||
range.start.saturating_sub(1)..self.len().min(range.end + 1),
|
range.start.saturating_sub(1)..self.len().min(range.end + 1),
|
||||||
&self.text,
|
&self.text,
|
||||||
|
@ -2363,39 +2364,31 @@ impl BufferSnapshot {
|
||||||
.map(|grammar| grammar.brackets_config.as_ref().unwrap())
|
.map(|grammar| grammar.brackets_config.as_ref().unwrap())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Get the ranges of the innermost pair of brackets.
|
iter::from_fn(move || {
|
||||||
let mut result: Option<(Range<usize>, Range<usize>)> = None;
|
while let Some(mat) = matches.peek() {
|
||||||
while let Some(mat) = matches.peek() {
|
let mut open = None;
|
||||||
let mut open = None;
|
let mut close = None;
|
||||||
let mut close = None;
|
let config = &configs[mat.grammar_index];
|
||||||
let config = &configs[mat.grammar_index];
|
for capture in mat.captures {
|
||||||
for capture in mat.captures {
|
if capture.index == config.open_capture_ix {
|
||||||
if capture.index == config.open_capture_ix {
|
open = Some(capture.node.byte_range());
|
||||||
open = Some(capture.node.byte_range());
|
} else if capture.index == config.close_capture_ix {
|
||||||
} else if capture.index == config.close_capture_ix {
|
close = Some(capture.node.byte_range());
|
||||||
close = Some(capture.node.byte_range());
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
matches.advance();
|
matches.advance();
|
||||||
|
|
||||||
let Some((open, close)) = open.zip(close) else { continue };
|
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;
|
|
||||||
|
|
||||||
if let Some((existing_open, existing_close)) = &result {
|
if open.start > range.start || close.end < range.end {
|
||||||
let existing_len = existing_close.end - existing_open.start;
|
|
||||||
if len > existing_len {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Some((open, close));
|
||||||
}
|
}
|
||||||
|
None
|
||||||
result = Some((open, close));
|
})
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
|
|
|
@ -3,6 +3,7 @@ use clock::ReplicaId;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use fs::LineEnding;
|
use fs::LineEnding;
|
||||||
use gpui::{ModelHandle, MutableAppContext};
|
use gpui::{ModelHandle, MutableAppContext};
|
||||||
|
use indoc::indoc;
|
||||||
use proto::deserialize_operation;
|
use proto::deserialize_operation;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -15,7 +16,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use text::network::Network;
|
use text::network::Network;
|
||||||
use unindent::Unindent as _;
|
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)]
|
#[cfg(test)]
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
|
@ -576,53 +577,117 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
let mut assert = |selection_text, range_markers| {
|
||||||
let buffer = cx.add_model(|cx| {
|
assert_enclosing_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
|
||||||
let text = "
|
};
|
||||||
mod x {
|
|
||||||
mod y {
|
|
||||||
|
|
||||||
|
assert(
|
||||||
|
indoc! {"
|
||||||
|
mod x {
|
||||||
|
moˇd y {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"
|
let foo = 1;"},
|
||||||
.unindent();
|
vec![indoc! {"
|
||||||
Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)
|
mod x «{»
|
||||||
});
|
mod y {
|
||||||
let buffer = buffer.read(cx);
|
|
||||||
assert_eq!(
|
}
|
||||||
buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
|
«}»
|
||||||
Some((
|
let foo = 1;"}],
|
||||||
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)
|
|
||||||
))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert(
|
||||||
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(4, 1)),
|
indoc! {"
|
||||||
Some((
|
mod x {
|
||||||
Point::new(0, 6)..Point::new(0, 7),
|
mod y ˇ{
|
||||||
Point::new(4, 0)..Point::new(4, 1)
|
|
||||||
))
|
}
|
||||||
|
}
|
||||||
|
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.
|
// Regression test: avoid crash when querying at the end of the buffer.
|
||||||
assert_eq!(
|
assert(
|
||||||
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)),
|
indoc! {"
|
||||||
None
|
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(
|
fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
|
||||||
cx: &mut MutableAppContext,
|
cx: &mut MutableAppContext,
|
||||||
) {
|
) {
|
||||||
let javascript_language = Arc::new(
|
let mut assert = |selection_text, bracket_pair_texts| {
|
||||||
Language::new(
|
assert_enclosing_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx)
|
||||||
LanguageConfig {
|
};
|
||||||
name: "JavaScript".into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Some(tree_sitter_javascript::language()),
|
|
||||||
)
|
|
||||||
.with_brackets_query(
|
|
||||||
r#"
|
|
||||||
("{" @open "}" @close)
|
|
||||||
("(" @open ")" @close)
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
cx.set_global(Settings::test(cx));
|
assert(
|
||||||
let buffer = cx.add_model(|cx| {
|
indoc! {"
|
||||||
let text = "
|
for (const a in b)ˇ {
|
||||||
for (const a in b) {
|
// a comment that's longer than the for-loop header
|
||||||
// a comment that's longer than the for-loop header
|
}"},
|
||||||
}
|
vec![indoc! {"
|
||||||
"
|
for «(»const a in b«)» {
|
||||||
.unindent();
|
// a comment that's longer than the for-loop header
|
||||||
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)
|
|
||||||
))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Regression test: even though the parent node of the parentheses (the for loop) does
|
// 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
|
// 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.
|
// they should not be returned. Only the curly braces contain the range.
|
||||||
assert_eq!(
|
assert(
|
||||||
buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)),
|
indoc! {"
|
||||||
Some((
|
for (const a in b) {ˇ
|
||||||
Point::new(0, 19)..Point::new(0, 20),
|
// a comment that's longer than the for-loop header
|
||||||
Point::new(2, 0)..Point::new(2, 1)
|
}"},
|
||||||
))
|
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 {
|
fn ruby_lang() -> Language {
|
||||||
Language::new(
|
Language::new(
|
||||||
LanguageConfig {
|
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 {
|
fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
|
||||||
buffer.read_with(cx, |buffer, _| {
|
buffer.read_with(cx, |buffer, _| {
|
||||||
let snapshot = buffer.snapshot();
|
let snapshot = buffer.snapshot();
|
||||||
|
@ -1997,3 +2045,36 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
|
||||||
layers[0].node.to_sexp()
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -452,8 +452,9 @@ fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> D
|
||||||
|
|
||||||
fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
let offset = point.to_offset(map, Bias::Left);
|
let offset = point.to_offset(map, Bias::Left);
|
||||||
if let Some((open_range, close_range)) =
|
if let Some((open_range, close_range)) = map
|
||||||
map.buffer_snapshot.enclosing_bracket_ranges(offset..offset)
|
.buffer_snapshot
|
||||||
|
.innermost_enclosing_bracket_ranges(offset..offset)
|
||||||
{
|
{
|
||||||
if open_range.contains(&offset) {
|
if open_range.contains(&offset) {
|
||||||
close_range.start.to_display_point(map)
|
close_range.start.to_display_point(map)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue