Extend symbol ranges by their annotation range when suggesting edits (#15677)

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2024-08-02 11:40:29 +02:00 committed by GitHub
parent e4608e7f12
commit b88b9dcdd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 261 additions and 121 deletions

View file

@ -598,6 +598,10 @@ impl EditOperation {
buffer.update(&mut cx, |buffer, _| {
let outline_item = &outline.items[candidate.id];
let symbol_range = outline_item.range.to_point(buffer);
let annotation_range = outline_item
.annotation_range
.as_ref()
.map(|range| range.to_point(buffer));
let body_range = outline_item
.body_range
.as_ref()
@ -606,23 +610,28 @@ impl EditOperation {
match kind {
EditOperationKind::PrependChild { .. } => {
let position = buffer.anchor_after(body_range.start);
position..position
let anchor = buffer.anchor_after(body_range.start);
anchor..anchor
}
EditOperationKind::AppendChild { .. } => {
let position = buffer.anchor_before(body_range.end);
position..position
let anchor = buffer.anchor_before(body_range.end);
anchor..anchor
}
EditOperationKind::InsertSiblingBefore { .. } => {
let position = buffer.anchor_before(symbol_range.start);
position..position
let anchor = buffer.anchor_before(
annotation_range.map_or(symbol_range.start, |annotation_range| {
annotation_range.start
}),
);
anchor..anchor
}
EditOperationKind::InsertSiblingAfter { .. } => {
let position = buffer.anchor_after(symbol_range.end);
position..position
let anchor = buffer.anchor_after(symbol_range.end);
anchor..anchor
}
EditOperationKind::Update { .. } | EditOperationKind::Delete { .. } => {
let start = Point::new(symbol_range.start.row, 0);
let start = annotation_range.map_or(symbol_range.start, |range| range.start);
let start = Point::new(start.row, 0);
let end = Point::new(
symbol_range.end.row,
buffer.line_len(symbol_range.end.row),

View file

@ -10,11 +10,11 @@ use crate::{
markdown::parse_markdown,
outline::OutlineItem,
syntax_map::{
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
SyntaxSnapshot, ToTreeSitterPoint,
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatch,
SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint,
},
task_context::RunnableRange,
LanguageScope, Outline, RunnableCapture, RunnableTag,
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
};
use anyhow::{anyhow, Context, Result};
use async_watch as watch;
@ -2768,130 +2768,44 @@ impl BufferSnapshot {
.collect::<Vec<_>>();
let mut items = Vec::new();
let mut annotation_row_ranges: Vec<Range<u32>> = Vec::new();
while let Some(mat) = matches.peek() {
let config = &configs[mat.grammar_index];
let item_node = mat.captures.iter().find_map(|cap| {
if cap.index == config.item_capture_ix {
Some(cap.node)
} else {
None
}
})?;
let item_range = item_node.byte_range();
if item_range.end < range.start || item_range.start > range.end {
matches.advance();
continue;
}
let mut open_index = None;
let mut close_index = None;
let mut buffer_ranges = Vec::new();
for capture in mat.captures {
let node_is_name;
if capture.index == config.name_capture_ix {
node_is_name = true;
} else if Some(capture.index) == config.context_capture_ix
|| (Some(capture.index) == config.extra_context_capture_ix
&& include_extra_context)
if let Some(item) =
self.next_outline_item(config, &mat, &range, include_extra_context, theme)
{
items.push(item);
} else if let Some(capture) = mat
.captures
.iter()
.find(|capture| Some(capture.index) == config.annotation_capture_ix)
{
let capture_range = capture.node.start_position()..capture.node.end_position();
let mut capture_row_range =
capture_range.start.row as u32..capture_range.end.row as u32;
if capture_range.end.row > capture_range.start.row && capture_range.end.column == 0
{
node_is_name = false;
} else {
if Some(capture.index) == config.open_capture_ix {
open_index = Some(capture.node.end_byte());
} else if Some(capture.index) == config.close_capture_ix {
close_index = Some(capture.node.start_byte());
}
continue;
capture_row_range.end -= 1;
}
let mut range = capture.node.start_byte()..capture.node.end_byte();
let start = capture.node.start_position();
if capture.node.end_position().row > start.row {
range.end =
range.start + self.line_len(start.row as u32) as usize - start.column;
}
if !range.is_empty() {
buffer_ranges.push((range, node_is_name));
}
}
if buffer_ranges.is_empty() {
matches.advance();
continue;
}
let mut text = String::new();
let mut highlight_ranges = Vec::new();
let mut name_ranges = Vec::new();
let mut chunks = self.chunks(
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
true,
);
let mut last_buffer_range_end = 0;
for (buffer_range, is_name) in buffer_ranges {
if !text.is_empty() && buffer_range.start > last_buffer_range_end {
text.push(' ');
}
last_buffer_range_end = buffer_range.end;
if is_name {
let mut start = text.len();
let end = start + buffer_range.len();
// When multiple names are captured, then the matcheable text
// includes the whitespace in between the names.
if !name_ranges.is_empty() {
start -= 1;
}
name_ranges.push(start..end);
}
let mut offset = buffer_range.start;
chunks.seek(offset);
for mut chunk in chunks.by_ref() {
if chunk.text.len() > buffer_range.end - offset {
chunk.text = &chunk.text[0..(buffer_range.end - offset)];
offset = buffer_range.end;
if let Some(last_row_range) = annotation_row_ranges.last_mut() {
if last_row_range.end >= capture_row_range.start.saturating_sub(1) {
last_row_range.end = capture_row_range.end;
} else {
offset += chunk.text.len();
}
let style = chunk
.syntax_highlight_id
.zip(theme)
.and_then(|(highlight, theme)| highlight.style(theme));
if let Some(style) = style {
let start = text.len();
let end = start + chunk.text.len();
highlight_ranges.push((start..end, style));
}
text.push_str(chunk.text);
if offset >= buffer_range.end {
break;
annotation_row_ranges.push(capture_row_range);
}
} else {
annotation_row_ranges.push(capture_row_range);
}
}
matches.advance();
items.push(OutlineItem {
depth: 0, // We'll calculate the depth later
range: item_range,
text,
highlight_ranges,
name_ranges,
body_range: open_index.zip(close_index).map(|(start, end)| start..end),
});
}
items.sort_by_key(|item| (item.range.start, Reverse(item.range.end)));
// Assign depths based on containment relationships and convert to anchors.
let mut item_ends_stack = Vec::<usize>::new();
let mut item_ends_stack = Vec::<Point>::new();
let mut anchor_items = Vec::new();
let mut annotation_row_ranges = annotation_row_ranges.into_iter().peekable();
for item in items {
while let Some(last_end) = item_ends_stack.last().copied() {
if last_end < item.range.end {
@ -2901,6 +2815,20 @@ impl BufferSnapshot {
}
}
let mut annotation_row_range = None;
while let Some(next_annotation_row_range) = annotation_row_ranges.peek() {
let row_preceding_item = item.range.start.row.saturating_sub(1);
if next_annotation_row_range.end < row_preceding_item {
annotation_row_ranges.next();
} else {
if next_annotation_row_range.end == row_preceding_item {
annotation_row_range = Some(next_annotation_row_range.clone());
annotation_row_ranges.next();
}
break;
}
}
anchor_items.push(OutlineItem {
depth: item_ends_stack.len(),
range: self.anchor_after(item.range.start)..self.anchor_before(item.range.end),
@ -2910,6 +2838,13 @@ impl BufferSnapshot {
body_range: item.body_range.map(|body_range| {
self.anchor_after(body_range.start)..self.anchor_before(body_range.end)
}),
annotation_range: annotation_row_range.map(|annotation_range| {
self.anchor_after(Point::new(annotation_range.start, 0))
..self.anchor_before(Point::new(
annotation_range.end,
self.line_len(annotation_range.end),
))
}),
});
item_ends_stack.push(item.range.end);
}
@ -2917,6 +2852,125 @@ impl BufferSnapshot {
Some(anchor_items)
}
fn next_outline_item(
&self,
config: &OutlineConfig,
mat: &SyntaxMapMatch,
range: &Range<usize>,
include_extra_context: bool,
theme: Option<&SyntaxTheme>,
) -> Option<OutlineItem<Point>> {
let item_node = mat.captures.iter().find_map(|cap| {
if cap.index == config.item_capture_ix {
Some(cap.node)
} else {
None
}
})?;
let item_byte_range = item_node.byte_range();
if item_byte_range.end < range.start || item_byte_range.start > range.end {
return None;
}
let item_point_range = Point::from_ts_point(item_node.start_position())
..Point::from_ts_point(item_node.end_position());
let mut open_point = None;
let mut close_point = None;
let mut buffer_ranges = Vec::new();
for capture in mat.captures {
let node_is_name;
if capture.index == config.name_capture_ix {
node_is_name = true;
} else if Some(capture.index) == config.context_capture_ix
|| (Some(capture.index) == config.extra_context_capture_ix && include_extra_context)
{
node_is_name = false;
} else {
if Some(capture.index) == config.open_capture_ix {
open_point = Some(Point::from_ts_point(capture.node.end_position()));
} else if Some(capture.index) == config.close_capture_ix {
close_point = Some(Point::from_ts_point(capture.node.start_position()));
}
continue;
}
let mut range = capture.node.start_byte()..capture.node.end_byte();
let start = capture.node.start_position();
if capture.node.end_position().row > start.row {
range.end = range.start + self.line_len(start.row as u32) as usize - start.column;
}
if !range.is_empty() {
buffer_ranges.push((range, node_is_name));
}
}
if buffer_ranges.is_empty() {
return None;
}
let mut text = String::new();
let mut highlight_ranges = Vec::new();
let mut name_ranges = Vec::new();
let mut chunks = self.chunks(
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
true,
);
let mut last_buffer_range_end = 0;
for (buffer_range, is_name) in buffer_ranges {
if !text.is_empty() && buffer_range.start > last_buffer_range_end {
text.push(' ');
}
last_buffer_range_end = buffer_range.end;
if is_name {
let mut start = text.len();
let end = start + buffer_range.len();
// When multiple names are captured, then the matcheable text
// includes the whitespace in between the names.
if !name_ranges.is_empty() {
start -= 1;
}
name_ranges.push(start..end);
}
let mut offset = buffer_range.start;
chunks.seek(offset);
for mut chunk in chunks.by_ref() {
if chunk.text.len() > buffer_range.end - offset {
chunk.text = &chunk.text[0..(buffer_range.end - offset)];
offset = buffer_range.end;
} else {
offset += chunk.text.len();
}
let style = chunk
.syntax_highlight_id
.zip(theme)
.and_then(|(highlight, theme)| highlight.style(theme));
if let Some(style) = style {
let start = text.len();
let end = start + chunk.text.len();
highlight_ranges.push((start..end, style));
}
text.push_str(chunk.text);
if offset >= buffer_range.end {
break;
}
}
}
Some(OutlineItem {
depth: 0, // We'll calculate the depth later
range: item_point_range,
text,
highlight_ranges,
name_ranges,
body_range: open_point.zip(close_point).map(|(start, end)| start..end),
annotation_range: None,
})
}
/// For each grammar in the language, runs the provided
/// [tree_sitter::Query] against the given range.
pub fn matches(

View file

@ -775,6 +775,61 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test]
fn test_outline_annotations(cx: &mut AppContext) {
// Add this new test case
let text = r#"
/// This is a doc comment
/// that spans multiple lines
fn annotated_function() {
// This is not an annotation
}
// This is a single-line annotation
fn another_function() {}
fn unannotated_function() {}
// This comment is not an annotation
fn function_after_blank_line() {}
"#
.unindent();
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let outline = buffer
.update(cx, |buffer, _| buffer.snapshot().outline(None))
.unwrap();
assert_eq!(
outline
.items
.into_iter()
.map(|item| (
item.text,
item.depth,
item.annotation_range
.map(|range| { buffer.read(cx).text_for_range(range).collect::<String>() })
))
.collect::<Vec<_>>(),
&[
(
"fn annotated_function".to_string(),
0,
Some("/// This is a doc comment\n/// that spans multiple lines".to_string())
),
(
"fn another_function".to_string(),
0,
Some("// This is a single-line annotation".to_string())
),
("fn unannotated_function".to_string(), 0, None),
("fn function_after_blank_line".to_string(), 0, None),
]
);
}
#[gpui::test]
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
let text = r#"
@ -2603,6 +2658,8 @@ fn rust_lang() -> Language {
.unwrap()
.with_outline_query(
r#"
(line_comment) @annotation
(struct_item
"struct" @context
name: (_) @name) @item

View file

@ -864,6 +864,7 @@ pub struct OutlineConfig {
pub extra_context_capture_ix: Option<u32>,
pub open_capture_ix: Option<u32>,
pub close_capture_ix: Option<u32>,
pub annotation_capture_ix: Option<u32>,
}
#[derive(Debug)]
@ -1049,6 +1050,7 @@ impl Language {
let mut extra_context_capture_ix = None;
let mut open_capture_ix = None;
let mut close_capture_ix = None;
let mut annotation_capture_ix = None;
get_capture_indices(
&query,
&mut [
@ -1058,6 +1060,7 @@ impl Language {
("context.extra", &mut extra_context_capture_ix),
("open", &mut open_capture_ix),
("close", &mut close_capture_ix),
("annotation", &mut annotation_capture_ix),
],
);
if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
@ -1069,6 +1072,7 @@ impl Language {
extra_context_capture_ix,
open_capture_ix,
close_capture_ix,
annotation_capture_ix,
});
}
Ok(self)

View file

@ -21,6 +21,7 @@ pub struct OutlineItem<T> {
pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
pub name_ranges: Vec<Range<usize>>,
pub body_range: Option<Range<T>>,
pub annotation_range: Option<Range<T>>,
}
impl<T> Outline<T> {

View file

@ -1,3 +1,6 @@
(attribute_item) @annotation
(line_comment) @annotation
(struct_item
(visibility_modifier)? @context
"struct" @context

View file

@ -3646,6 +3646,12 @@ impl MultiBufferSnapshot {
..self.anchor_in_excerpt(*excerpt_id, body_range.end)?,
)
}),
annotation_range: item.annotation_range.and_then(|annotation_range| {
Some(
self.anchor_in_excerpt(*excerpt_id, annotation_range.start)?
..self.anchor_in_excerpt(*excerpt_id, annotation_range.end)?,
)
}),
})
})
.collect(),
@ -3681,6 +3687,12 @@ impl MultiBufferSnapshot {
..self.anchor_in_excerpt(excerpt_id, body_range.end)?,
)
}),
annotation_range: item.annotation_range.and_then(|body_range| {
Some(
self.anchor_in_excerpt(excerpt_id, body_range.start)?
..self.anchor_in_excerpt(excerpt_id, body_range.end)?,
)
}),
})
})
.collect(),