diff --git a/Cargo.lock b/Cargo.lock index 0009d37afa..f2cb97bf96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9652,6 +9652,7 @@ dependencies = [ "toml 0.8.23", "tracing", "tree-sitter", + "tree-sitter-c", "tree-sitter-elixir", "tree-sitter-embedded-template", "tree-sitter-heex", diff --git a/crates/editor/src/document_symbols.rs b/crates/editor/src/document_symbols.rs index 8523bcb7b0..58483e6dab 100644 --- a/crates/editor/src/document_symbols.rs +++ b/crates/editor/src/document_symbols.rs @@ -103,6 +103,9 @@ impl Editor { Some(OutlineItem { depth: item.depth, range: range_start..range_end, + selection_range: multi_buffer_snapshot + .anchor_in_buffer(item.selection_range.start)? + ..multi_buffer_snapshot.anchor_in_buffer(item.selection_range.end)?, source_range_for_text: source_range_for_text_start..source_range_for_text_end, text: item.text.clone(), highlight_ranges: item.highlight_ranges.clone(), @@ -331,7 +334,9 @@ mod tests { use futures::StreamExt as _; use gpui::TestAppContext; + use multi_buffer::ToPoint; use settings::{DocumentSymbols, SettingsStore}; + use text::Point; use util::path; use zed_actions::editor::{MoveDown, MoveUp}; @@ -755,7 +760,7 @@ mod tests { assert!(symbol_request.next().await.is_some()); cx.run_until_parked(); - cx.update_editor(|editor, _window, _cx| { + cx.update_editor(|editor, _window, cx| { let (_, symbols) = editor .outline_symbols_at_cursor .as_ref() @@ -763,7 +768,19 @@ mod tests { assert_eq!(symbols.len(), 1); let symbol = &symbols[0]; + let multi_buffer_snapshot = editor.buffer.read(cx).snapshot(cx); assert_eq!(symbol.text, "fn test"); + assert_eq!( + symbol + .selection_range + .start + .to_point(&multi_buffer_snapshot), + Point::new(1, 3) + ); + assert_eq!( + symbol.selection_range.end.to_point(&multi_buffer_snapshot), + Point::new(1, 7) + ); // Verify all highlight ranges are valid byte boundaries in the text for (range, _style) in &symbol.highlight_ranges { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c91f92f563..e65140d53f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1793,6 +1793,8 @@ impl Editor { depth: outline_item.depth, range: multi_buffer .buffer_anchor_range_to_anchor_range(outline_item.range)?, + selection_range: multi_buffer + .buffer_anchor_range_to_anchor_range(outline_item.selection_range)?, source_range_for_text: multi_buffer.buffer_anchor_range_to_anchor_range( outline_item.source_range_for_text, )?, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 22b0e1a635..6a71776648 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -33931,6 +33931,66 @@ async fn test_sticky_scroll_with_decoration_prefix_in_item(cx: &mut TestAppConte assert_eq!(sticky_headers(7.0), vec![]); } +#[gpui::test] +async fn test_sticky_scroll_anchors_multiline_c_signature_on_name_row(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let buffer = indoc! {" + ˇvoid + evdev_post_scroll(struct evdev_device *device, + usec_t time, + enum libinput_pointer_axis_source source, + const struct normalized_coords *delta) + { + const struct normalized_coords tilt_rot = { + cos(SCROLL_DELTA_TILT_ANGLE), + sin(SCROLL_DELTA_TILT_ANGLE), + }; + } + "}; + cx.set_state(buffer); + + cx.update_editor(|editor, _, cx| { + editor + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .update(cx, |buffer, cx| { + buffer.set_language( + Some(languages::language("c", tree_sitter_c::LANGUAGE.into())), + cx, + ); + }) + }); + + let mut sticky_headers = |offset: ScrollOffset| { + cx.update_editor(|editor, window, cx| { + editor.scroll(gpui::Point { x: 0., y: offset }, None, window, cx); + }); + cx.run_until_parked(); + cx.update_editor(|editor, window, cx| { + EditorElement::sticky_headers(&editor, &editor.snapshot(window, cx)) + .into_iter() + .map( + |StickyHeader { + start_point, + offset, + .. + }| { (start_point, offset) }, + ) + .collect::>() + }) + }; + + let function_name_row = Point { row: 1, column: 0 }; + + assert_eq!(sticky_headers(1.0), vec![]); + assert_eq!(sticky_headers(1.5), vec![(function_name_row, 0.0)]); + assert_eq!(sticky_headers(5.0), vec![(function_name_row, 0.0)]); +} + #[gpui::test] async fn test_sticky_scroll_with_expanded_deleted_diff_hunks( executor: BackgroundExecutor, diff --git a/crates/editor/src/element/header.rs b/crates/editor/src/element/header.rs index 052afe1de6..e51f29514a 100644 --- a/crates/editor/src/element/header.rs +++ b/crates/editor/src/element/header.rs @@ -69,10 +69,20 @@ impl EditorElement { let mut rows = Vec::::new(); for item in editor.sticky_headers.iter().flatten() { - let start_point = item + let selection_start = item + .selection_range + .start + .to_point(snapshot.buffer_snapshot()); + let source_text_start = item .source_range_for_text .start .to_point(snapshot.buffer_snapshot()); + let start_column = if source_text_start.row == selection_start.row { + source_text_start.column + } else { + 0 + }; + let start_point = Point::new(selection_start.row, start_column); let end_point = item.range.end.to_point(snapshot.buffer_snapshot()); let sticky_row = snapshot diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index bde0659cf1..58a7517d2c 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -95,6 +95,7 @@ tree-sitter-json.workspace = true tree-sitter-md.workspace = true tree-sitter-python.workspace = true tree-sitter-ruby.workspace = true +tree-sitter-c.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true toml.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2973414211..5b2b511192 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -4535,6 +4535,7 @@ impl BufferSnapshot { anchor_items.push(OutlineItem { depth: item_ends_stack.len(), range: range_callback(self, item.range.clone()), + selection_range: range_callback(self, item.selection_range.clone()), source_range_for_text: range_callback(self, item.source_range_for_text.clone()), text: item.text, highlight_ranges: item.highlight_ranges, @@ -4612,6 +4613,14 @@ impl BufferSnapshot { } let source_range_for_text = buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end; + let selection_range = buffer_ranges + .iter() + .filter(|(_, node_is_name)| *node_is_name) + .map(|(buffer_range, _)| buffer_range.clone()) + .reduce(|mut combined_range, next_range| { + combined_range.end = next_range.end; + combined_range + })?; let mut text = String::new(); let mut highlight_ranges = Vec::new(); @@ -4669,6 +4678,7 @@ impl BufferSnapshot { Some(OutlineItem { depth: 0, // We'll calculate the depth later range: item_point_range, + selection_range: selection_range.to_point(self), source_range_for_text: source_range_for_text.to_point(self), text: text.into(), highlight_ranges, diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 691a6f9448..ba7fd7ea95 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -977,6 +977,35 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_outline_selection_range_for_multiline_c_signature(cx: &mut gpui::TestAppContext) { + let text = indoc! {" + void + evdev_post_scroll(struct evdev_device *device, + usec_t time, + enum libinput_pointer_axis_source source, + const struct normalized_coords *delta) + { + return; + } + "}; + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(c_lang(), cx)); + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + let outline = snapshot.outline(None); + + let item = outline + .items + .iter() + .find(|item| item.text.contains("evdev_post_scroll")) + .unwrap() + .to_point(&snapshot); + + assert_eq!(item.source_range_for_text.start, Point::new(0, 0)); + assert_eq!(item.selection_range.start, Point::new(1, 0)); + assert_eq!(item.text, "void evdev_post_scroll( )"); +} + #[gpui::test] fn test_outline_annotations(cx: &mut App) { // Add this new test case @@ -4049,6 +4078,20 @@ fn javascript_lang() -> Language { .unwrap() } +fn c_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "C".into(), + ..Default::default() + }, + Some(tree_sitter_c::LANGUAGE.into()), + ) + .with_outline_query(include_str!("../../grammars/src/c/outline.scm")) + .unwrap(), + ) +} + pub fn markdown_inline_lang() -> Language { Language::new( LanguageConfig { diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 5ff5c028ff..c85b7be78f 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -19,6 +19,7 @@ pub struct Outline { pub struct OutlineItem { pub depth: usize, pub range: Range, + pub selection_range: Range, pub source_range_for_text: Range, pub text: SharedString, pub highlight_ranges: Vec<(Range, HighlightStyle)>, @@ -71,6 +72,8 @@ impl OutlineItem { OutlineItem { depth: self.depth, range: self.range.start.to_point(buffer)..self.range.end.to_point(buffer), + selection_range: self.selection_range.start.to_point(buffer) + ..self.selection_range.end.to_point(buffer), source_range_for_text: self.source_range_for_text.start.to_point(buffer) ..self.source_range_for_text.end.to_point(buffer), text: self.text.clone(), @@ -315,6 +318,7 @@ mod tests { let item = OutlineItem { depth: 0, range: range.clone(), + selection_range: range.clone(), source_range_for_text: range, text: "completion".into(), highlight_ranges: Vec::new(), @@ -332,6 +336,7 @@ mod tests { OutlineItem { depth: 0, range: Point::new(0, 0)..Point::new(5, 0), + selection_range: Point::new(0, 6)..Point::new(0, 9), source_range_for_text: Point::new(0, 0)..Point::new(0, 9), text: "class Foo".into(), highlight_ranges: vec![], @@ -342,6 +347,7 @@ mod tests { OutlineItem { depth: 0, range: Point::new(2, 0)..Point::new(2, 7), + selection_range: Point::new(2, 0)..Point::new(2, 7), source_range_for_text: Point::new(0, 0)..Point::new(0, 7), text: "private".into(), highlight_ranges: vec![], @@ -373,6 +379,7 @@ mod tests { OutlineItem { depth: 0, range: Point::new(0, 0)..Point::new(5, 0), + selection_range: Point::new(0, 3)..Point::new(0, 10), source_range_for_text: Point::new(0, 0)..Point::new(0, 10), text: "fn process".into(), highlight_ranges: vec![], @@ -383,6 +390,7 @@ mod tests { OutlineItem { depth: 0, range: Point::new(7, 0)..Point::new(12, 0), + selection_range: Point::new(0, 7)..Point::new(0, 20), source_range_for_text: Point::new(0, 0)..Point::new(0, 20), text: "struct DataProcessor".into(), highlight_ranges: vec![], diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 4b776b2d81..5b61e6237e 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -6293,6 +6293,7 @@ impl MultiBufferSnapshot { .map(|item| OutlineItem { depth: item.depth, range: Anchor::range_in_buffer(path_key_index, item.range), + selection_range: Anchor::range_in_buffer(path_key_index, item.selection_range), source_range_for_text: Anchor::range_in_buffer( path_key_index, item.source_range_for_text, @@ -6335,6 +6336,10 @@ impl MultiBufferSnapshot { .flat_map(|item| { Some(OutlineItem { depth: item.depth, + selection_range: Anchor::range_in_buffer( + excerpt.path_key_index, + item.selection_range, + ), source_range_for_text: Anchor::range_in_buffer( excerpt.path_key_index, item.source_range_for_text, diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index c3a5cc4804..20662f322f 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -90,6 +90,8 @@ fn outline_for_editor( depth: item.depth, range: multibuffer.anchor_in_buffer(item.range.start)? ..multibuffer.anchor_in_buffer(item.range.end)?, + selection_range: multibuffer.anchor_in_buffer(item.selection_range.start)? + ..multibuffer.anchor_in_buffer(item.selection_range.end)?, source_range_for_text: multibuffer .anchor_in_buffer(item.source_range_for_text.start)? ..multibuffer.anchor_in_buffer(item.source_range_for_text.end)?, diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 6bd05b807d..a8786aaa40 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -2567,6 +2567,7 @@ impl OutlinePanel { depth, annotation_range: None, range: search_data.context_range.clone(), + selection_range: search_data.context_range.clone(), text: search_data.context_text.clone().into(), source_range_for_text: search_data.context_range.clone(), highlight_ranges: search_data diff --git a/crates/project/src/lsp_store/document_symbols.rs b/crates/project/src/lsp_store/document_symbols.rs index e993865d4f..98f66d9fdb 100644 --- a/crates/project/src/lsp_store/document_symbols.rs +++ b/crates/project/src/lsp_store/document_symbols.rs @@ -270,6 +270,7 @@ fn flatten_document_symbols( output.push(OutlineItem { depth, range, + selection_range: selection_range.clone(), source_range_for_text, text: text.into(), highlight_ranges: Vec::new(), @@ -327,7 +328,7 @@ fn enriched_symbol_text( mod tests { use super::*; use gpui::TestAppContext; - use text::Unclipped; + use text::{OffsetRangeExt, Point, Unclipped}; fn make_symbol( name: &str, @@ -422,22 +423,42 @@ mod tests { assert_eq!(items[0].depth, 0); assert_eq!(items[0].text, "struct Foo"); assert_eq!(items[0].name_ranges, vec![7..10]); + assert_eq!( + items[0].selection_range.to_point(&snapshot), + Point::new(0, 7)..Point::new(0, 10) + ); assert_eq!(items[1].depth, 1); assert_eq!(items[1].text, "bar"); assert_eq!(items[1].name_ranges, vec![0..3]); + assert_eq!( + items[1].selection_range.to_point(&snapshot), + Point::new(1, 4)..Point::new(1, 7) + ); assert_eq!(items[2].depth, 1); assert_eq!(items[2].text, "baz"); assert_eq!(items[2].name_ranges, vec![0..3]); + assert_eq!( + items[2].selection_range.to_point(&snapshot), + Point::new(2, 4)..Point::new(2, 7) + ); assert_eq!(items[3].depth, 0); assert_eq!(items[3].text, "impl Foo"); assert_eq!(items[3].name_ranges, vec![5..8]); + assert_eq!( + items[3].selection_range.to_point(&snapshot), + Point::new(5, 5)..Point::new(5, 8) + ); assert_eq!(items[4].depth, 1); assert_eq!(items[4].text, "fn new"); assert_eq!(items[4].name_ranges, vec![3..6]); + assert_eq!( + items[4].selection_range.to_point(&snapshot), + Point::new(6, 7)..Point::new(6, 10) + ); } #[gpui::test]