diff --git a/Cargo.lock b/Cargo.lock index caeb7e714da6c7f2c689d51e82500cc69d68f35d..2b1427a435a07c7485388a668e51035d55b56b39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5039,6 +5039,22 @@ dependencies = [ "zeta", ] +[[package]] +name = "edit_prediction_context" +version = "0.1.0" +dependencies = [ + "gpui", + "indoc", + "language", + "log", + "pretty_assertions", + "text", + "tree-sitter", + "util", + "workspace-hack", + "zlog", +] + [[package]] name = "editor" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index de1d216561b9d3074eecff5c36d6405a29c4f7d3..5cad901d18d5832f0d593c6c436613e507cdabe1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ members = [ "crates/docs_preprocessor", "crates/edit_prediction", "crates/edit_prediction_button", + "crates/edit_prediction_context", "crates/editor", "crates/eval", "crates/explorer_command_injector", @@ -312,6 +313,7 @@ icons = { path = "crates/icons" } image_viewer = { path = "crates/image_viewer" } edit_prediction = { path = "crates/edit_prediction" } edit_prediction_button = { path = "crates/edit_prediction_button" } +edit_prediction_context = { path = "crates/edit_prediction_context" } inspector_ui = { path = "crates/inspector_ui" } install_cli = { path = "crates/install_cli" } jj = { path = "crates/jj" } diff --git a/crates/edit_prediction_context/Cargo.toml b/crates/edit_prediction_context/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..6729dcd39b67c5374d745b1811b73a8b8af4f2aa --- /dev/null +++ b/crates/edit_prediction_context/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "edit_prediction_context" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/edit_prediction_context.rs" + +[dependencies] +language.workspace = true +workspace-hack.workspace = true +tree-sitter.workspace = true +text.workspace = true +log.workspace = true +util.workspace = true + +[dev-dependencies] +gpui = { workspace = true, features = ["test-support"] } +indoc.workspace = true +language = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true +text = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } +zlog.workspace = true diff --git a/crates/edit_prediction_context/LICENSE-GPL b/crates/edit_prediction_context/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/edit_prediction_context/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/edit_prediction_context/src/edit_prediction_context.rs b/crates/edit_prediction_context/src/edit_prediction_context.rs new file mode 100644 index 0000000000000000000000000000000000000000..7e6cad45a8032cb1afaafd95eb10c80e61cff097 --- /dev/null +++ b/crates/edit_prediction_context/src/edit_prediction_context.rs @@ -0,0 +1,3 @@ +mod excerpt; + +pub use excerpt::{EditPredictionExcerpt, EditPredictionExcerptOptions}; diff --git a/crates/edit_prediction_context/src/excerpt.rs b/crates/edit_prediction_context/src/excerpt.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a20da76f71834d226707fe36f93d3667158372c --- /dev/null +++ b/crates/edit_prediction_context/src/excerpt.rs @@ -0,0 +1,595 @@ +use language::BufferSnapshot; +use std::ops::Range; +use text::{OffsetRangeExt as _, Point, ToOffset as _, ToPoint as _}; +use tree_sitter::{Node, TreeCursor}; +use util::RangeExt; + +// TODO: +// +// - Test parent signatures +// +// - Decide whether to count signatures against the excerpt size. Could instead defer this to prompt +// planning. +// +// - Still return an excerpt even if the line around the cursor doesn't fit (e.g. for a markdown +// paragraph). +// +// - Truncation of long lines. +// +// - Filter outer syntax layers that don't support edit prediction. + +#[derive(Debug, Clone)] +pub struct EditPredictionExcerptOptions { + /// Limit for the number of bytes in the window around the cursor. + pub max_bytes: usize, + /// Minimum number of bytes in the window around the cursor. When syntax tree selection results + /// in an excerpt smaller than this, it will fall back on line-based selection. + pub min_bytes: usize, + /// Target ratio of bytes before the cursor divided by total bytes in the window. + pub target_before_cursor_over_total_bytes: f32, + /// Whether to include parent signatures + pub include_parent_signatures: bool, +} + +#[derive(Clone)] +pub struct EditPredictionExcerpt { + pub range: Range, + pub parent_signature_ranges: Vec>, + pub size: usize, +} + +impl EditPredictionExcerpt { + /// Selects an excerpt around a buffer position, attempting to choose logical boundaries based + /// on TreeSitter structure and approximately targeting a goal ratio of bytesbefore vs after the + /// cursor. When `include_parent_signatures` is true, the excerpt also includes the signatures + /// of parent outline items. + /// + /// First tries to use AST node boundaries to select the excerpt, and falls back on line-based + /// expansion. + /// + /// Returns `None` if the line around the cursor doesn't fit. + pub fn select_from_buffer( + query_point: Point, + buffer: &BufferSnapshot, + options: &EditPredictionExcerptOptions, + ) -> Option { + if buffer.len() <= options.max_bytes { + log::debug!( + "using entire file for excerpt since source length ({}) <= window max bytes ({})", + buffer.len(), + options.max_bytes + ); + return Some(EditPredictionExcerpt::new(0..buffer.len(), Vec::new())); + } + + let query_offset = query_point.to_offset(buffer); + let query_range = Point::new(query_point.row, 0).to_offset(buffer) + ..Point::new(query_point.row + 1, 0).to_offset(buffer); + if query_range.len() >= options.max_bytes { + return None; + } + + // TODO: Don't compute text / annotation_range / skip converting to and from anchors. + let outline_items = if options.include_parent_signatures { + buffer + .outline_items_containing(query_range.clone(), false, None) + .into_iter() + .flat_map(|item| { + Some(ExcerptOutlineItem { + item_range: item.range.to_offset(&buffer), + signature_range: item.signature_range?.to_offset(&buffer), + }) + }) + .collect() + } else { + Vec::new() + }; + + let excerpt_selector = ExcerptSelector { + query_offset, + query_range, + outline_items: &outline_items, + buffer, + options, + }; + + if let Some(excerpt_ranges) = excerpt_selector.select_tree_sitter_nodes() { + if excerpt_ranges.size >= options.min_bytes { + return Some(excerpt_ranges); + } + log::debug!( + "tree-sitter excerpt was {} bytes, smaller than min of {}, falling back on line-based selection", + excerpt_ranges.size, + options.min_bytes + ); + } else { + log::debug!( + "couldn't find excerpt via tree-sitter, falling back on line-based selection" + ); + } + + excerpt_selector.select_lines() + } + + fn new(range: Range, parent_signature_ranges: Vec>) -> Self { + let size = range.len() + + parent_signature_ranges + .iter() + .map(|r| r.len()) + .sum::(); + Self { + range, + parent_signature_ranges, + size, + } + } + + fn with_expanded_range(&self, new_range: Range) -> Self { + if !new_range.contains_inclusive(&self.range) { + // this is an issue because parent_signature_ranges may be incorrect + log::error!("bug: with_expanded_range called with disjoint range"); + } + let mut parent_signature_ranges = Vec::with_capacity(self.parent_signature_ranges.len()); + let mut size = new_range.len(); + for range in &self.parent_signature_ranges { + if range.contains_inclusive(&new_range) { + break; + } + parent_signature_ranges.push(range.clone()); + size += range.len(); + } + Self { + range: new_range, + parent_signature_ranges, + size, + } + } + + fn parent_signatures_size(&self) -> usize { + self.size - self.range.len() + } +} + +struct ExcerptSelector<'a> { + query_offset: usize, + query_range: Range, + outline_items: &'a [ExcerptOutlineItem], + buffer: &'a BufferSnapshot, + options: &'a EditPredictionExcerptOptions, +} + +struct ExcerptOutlineItem { + item_range: Range, + signature_range: Range, +} + +impl<'a> ExcerptSelector<'a> { + /// Finds the largest node that is smaller than the window size and contains `query_range`. + fn select_tree_sitter_nodes(&self) -> Option { + let selected_layer_root = self.select_syntax_layer()?; + let mut cursor = selected_layer_root.walk(); + + loop { + let excerpt_range = node_line_start(cursor.node()).to_offset(&self.buffer) + ..node_line_end(cursor.node()).to_offset(&self.buffer); + if excerpt_range.contains_inclusive(&self.query_range) { + let excerpt = self.make_excerpt(excerpt_range); + if excerpt.size <= self.options.max_bytes { + return Some(self.expand_to_siblings(&mut cursor, excerpt)); + } + } else { + // TODO: Should still be able to handle this case via AST nodes. For example, this + // can happen if the cursor is between two methods in a large class file. + return None; + } + + if cursor + .goto_first_child_for_byte(self.query_range.start) + .is_none() + { + return None; + } + } + } + + /// Select the smallest syntax layer that exceeds max_len, or the largest if none exceed max_len. + fn select_syntax_layer(&self) -> Option> { + let mut smallest_exceeding_max_len: Option> = None; + let mut largest: Option> = None; + for layer in self + .buffer + .syntax_layers_for_range(self.query_range.start..self.query_range.start, true) + { + let layer_range = layer.node().byte_range(); + if !layer_range.contains_inclusive(&self.query_range) { + continue; + } + + if layer_range.len() > self.options.max_bytes { + match &smallest_exceeding_max_len { + None => smallest_exceeding_max_len = Some(layer.node()), + Some(existing) => { + if layer_range.len() < existing.byte_range().len() { + smallest_exceeding_max_len = Some(layer.node()); + } + } + } + } else { + match &largest { + None => largest = Some(layer.node()), + Some(existing) if layer_range.len() > existing.byte_range().len() => { + largest = Some(layer.node()) + } + _ => {} + } + } + } + + smallest_exceeding_max_len.or(largest) + } + + // motivation for this and `goto_previous_named_sibling` is to avoid including things like + // trailing unnamed "}" in body nodes + fn goto_next_named_sibling(cursor: &mut TreeCursor) -> bool { + while cursor.goto_next_sibling() { + if cursor.node().is_named() { + return true; + } + } + false + } + + fn goto_previous_named_sibling(cursor: &mut TreeCursor) -> bool { + while cursor.goto_previous_sibling() { + if cursor.node().is_named() { + return true; + } + } + false + } + + fn expand_to_siblings( + &self, + cursor: &mut TreeCursor, + mut excerpt: EditPredictionExcerpt, + ) -> EditPredictionExcerpt { + let mut forward_cursor = cursor.clone(); + let backward_cursor = cursor; + let mut forward_done = !Self::goto_next_named_sibling(&mut forward_cursor); + let mut backward_done = !Self::goto_previous_named_sibling(backward_cursor); + loop { + if backward_done && forward_done { + break; + } + + let mut forward = None; + while !forward_done { + let new_end = node_line_end(forward_cursor.node()).to_offset(&self.buffer); + if new_end > excerpt.range.end { + let new_excerpt = excerpt.with_expanded_range(excerpt.range.start..new_end); + if new_excerpt.size <= self.options.max_bytes { + forward = Some(new_excerpt); + break; + } else { + log::debug!("halting forward expansion, as it doesn't fit"); + forward_done = true; + break; + } + } + forward_done = !Self::goto_next_named_sibling(&mut forward_cursor); + } + + let mut backward = None; + while !backward_done { + let new_start = node_line_start(backward_cursor.node()).to_offset(&self.buffer); + if new_start < excerpt.range.start { + let new_excerpt = excerpt.with_expanded_range(new_start..excerpt.range.end); + if new_excerpt.size <= self.options.max_bytes { + backward = Some(new_excerpt); + break; + } else { + log::debug!("halting backward expansion, as it doesn't fit"); + backward_done = true; + break; + } + } + backward_done = !Self::goto_previous_named_sibling(backward_cursor); + } + + let go_forward = match (forward, backward) { + (Some(forward), Some(backward)) => { + let go_forward = self.is_better_excerpt(&forward, &backward); + if go_forward { + excerpt = forward; + } else { + excerpt = backward; + } + go_forward + } + (Some(forward), None) => { + log::debug!("expanding forward, since backward expansion has halted"); + excerpt = forward; + true + } + (None, Some(backward)) => { + log::debug!("expanding backward, since forward expansion has halted"); + excerpt = backward; + false + } + (None, None) => break, + }; + + if go_forward { + forward_done = !Self::goto_next_named_sibling(&mut forward_cursor); + } else { + backward_done = !Self::goto_previous_named_sibling(backward_cursor); + } + } + + excerpt + } + + fn select_lines(&self) -> Option { + // early return if line containing query_offset is already too large + let excerpt = self.make_excerpt(self.query_range.clone()); + if excerpt.size > self.options.max_bytes { + log::debug!( + "excerpt for cursor line is {} bytes, which exceeds the window", + excerpt.size + ); + return None; + } + let signatures_size = excerpt.parent_signatures_size(); + let bytes_remaining = self.options.max_bytes.saturating_sub(signatures_size); + + let before_bytes = + (self.options.target_before_cursor_over_total_bytes * bytes_remaining as f32) as usize; + + let start_point = { + let offset = self.query_offset.saturating_sub(before_bytes); + let point = offset.to_point(self.buffer); + Point::new(point.row + 1, 0) + }; + let start_offset = start_point.to_offset(&self.buffer); + let end_point = { + let offset = start_offset + bytes_remaining; + let point = offset.to_point(self.buffer); + Point::new(point.row, 0) + }; + let end_offset = end_point.to_offset(&self.buffer); + + // this could be expanded further since recalculated `signature_size` may be smaller, but + // skipping that for now for simplicity + // + // TODO: could also consider checking if lines immediately before / after fit. + let excerpt = self.make_excerpt(start_offset..end_offset); + if excerpt.size > self.options.max_bytes { + log::error!( + "bug: line-based excerpt selection has size {}, \ + which is {} bytes larger than the max size", + excerpt.size, + excerpt.size - self.options.max_bytes + ); + } + return Some(excerpt); + } + + fn make_excerpt(&self, range: Range) -> EditPredictionExcerpt { + let parent_signature_ranges = self + .outline_items + .iter() + .filter(|item| item.item_range.contains_inclusive(&range)) + .map(|item| item.signature_range.clone()) + .collect(); + EditPredictionExcerpt::new(range, parent_signature_ranges) + } + + /// Returns `true` if the `forward` excerpt is a better choice than the `backward` excerpt. + fn is_better_excerpt( + &self, + forward: &EditPredictionExcerpt, + backward: &EditPredictionExcerpt, + ) -> bool { + let forward_ratio = self.excerpt_range_ratio(forward); + let backward_ratio = self.excerpt_range_ratio(backward); + let forward_delta = + (forward_ratio - self.options.target_before_cursor_over_total_bytes).abs(); + let backward_delta = + (backward_ratio - self.options.target_before_cursor_over_total_bytes).abs(); + let forward_is_better = forward_delta <= backward_delta; + if forward_is_better { + log::debug!( + "expanding forward since {} is closer than {} to {}", + forward_ratio, + backward_ratio, + self.options.target_before_cursor_over_total_bytes + ); + } else { + log::debug!( + "expanding backward since {} is closer than {} to {}", + backward_ratio, + forward_ratio, + self.options.target_before_cursor_over_total_bytes + ); + } + forward_is_better + } + + /// Returns the ratio of bytes before the cursor over bytes within the range. + fn excerpt_range_ratio(&self, excerpt: &EditPredictionExcerpt) -> f32 { + let Some(bytes_before_cursor) = self.query_offset.checked_sub(excerpt.range.start) else { + log::error!("bug: edit prediction cursor offset is not outside the excerpt"); + return 0.0; + }; + bytes_before_cursor as f32 / excerpt.range.len() as f32 + } +} + +fn node_line_start(node: Node) -> Point { + Point::new(node.start_position().row as u32, 0) +} + +fn node_line_end(node: Node) -> Point { + Point::new(node.end_position().row as u32 + 1, 0) +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{AppContext, TestAppContext}; + use language::{Buffer, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust}; + use util::test::{generate_marked_text, marked_text_offsets_by}; + + fn create_buffer(text: &str, cx: &mut TestAppContext) -> BufferSnapshot { + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(rust_lang().into(), cx)); + buffer.read_with(cx, |buffer, _| buffer.snapshot()) + } + + fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_outline_query(include_str!("../../languages/src/rust/outline.scm")) + .unwrap() + } + + fn cursor_and_excerpt_range(text: &str) -> (String, usize, Range) { + let (text, offsets) = marked_text_offsets_by(text, vec!['ˇ', '«', '»']); + (text, offsets[&'ˇ'][0], offsets[&'«'][0]..offsets[&'»'][0]) + } + + fn check_example(options: EditPredictionExcerptOptions, text: &str, cx: &mut TestAppContext) { + let (text, cursor, expected_excerpt) = cursor_and_excerpt_range(text); + + let buffer = create_buffer(&text, cx); + let cursor_point = cursor.to_point(&buffer); + + let excerpt = EditPredictionExcerpt::select_from_buffer(cursor_point, &buffer, &options) + .expect("Should select an excerpt"); + pretty_assertions::assert_eq!( + generate_marked_text(&text, std::slice::from_ref(&excerpt.range), false), + generate_marked_text(&text, &[expected_excerpt], false) + ); + assert!(excerpt.size <= options.max_bytes); + assert!(excerpt.range.contains(&cursor)); + } + + #[gpui::test] + fn test_ast_based_selection_current_node(cx: &mut TestAppContext) { + zlog::init_test(); + let text = r#" +fn main() { + let x = 1; +« let ˇy = 2; +» let z = 3; +}"#; + + let options = EditPredictionExcerptOptions { + max_bytes: 20, + min_bytes: 10, + target_before_cursor_over_total_bytes: 0.5, + include_parent_signatures: false, + }; + + check_example(options, text, cx); + } + + #[gpui::test] + fn test_ast_based_selection_parent_node(cx: &mut TestAppContext) { + zlog::init_test(); + let text = r#" +fn foo() {} + +«fn main() { + let x = 1; + let ˇy = 2; + let z = 3; +} +» +fn bar() {}"#; + + let options = EditPredictionExcerptOptions { + max_bytes: 65, + min_bytes: 10, + target_before_cursor_over_total_bytes: 0.5, + include_parent_signatures: false, + }; + + check_example(options, text, cx); + } + + #[gpui::test] + fn test_ast_based_selection_expands_to_siblings(cx: &mut TestAppContext) { + zlog::init_test(); + let text = r#" +fn main() { +« let x = 1; + let ˇy = 2; + let z = 3; +»}"#; + + let options = EditPredictionExcerptOptions { + max_bytes: 50, + min_bytes: 10, + target_before_cursor_over_total_bytes: 0.5, + include_parent_signatures: false, + }; + + check_example(options, text, cx); + } + + #[gpui::test] + fn test_line_based_selection(cx: &mut TestAppContext) { + zlog::init_test(); + let text = r#" +fn main() { + let x = 1; +« if true { + let ˇy = 2; + } + let z = 3; +»}"#; + + let options = EditPredictionExcerptOptions { + max_bytes: 60, + min_bytes: 45, + target_before_cursor_over_total_bytes: 0.5, + include_parent_signatures: false, + }; + + check_example(options, text, cx); + } + + #[gpui::test] + fn test_line_based_selection_with_before_cursor_ratio(cx: &mut TestAppContext) { + zlog::init_test(); + let text = r#" + fn main() { +« let a = 1; + let b = 2; + let c = 3; + let ˇd = 4; + let e = 5; + let f = 6; +» + let g = 7; + }"#; + + let options = EditPredictionExcerptOptions { + max_bytes: 120, + min_bytes: 10, + target_before_cursor_over_total_bytes: 0.6, + include_parent_signatures: false, + }; + + check_example(options, text, cx); + } +} diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f03df08a55b2759885d133b9a7dc3556b549a184..77270807644830a233cbd6f3c47e4912ff47f543 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3310,18 +3310,25 @@ impl BufferSnapshot { /// Iterates over every [`SyntaxLayer`] in the buffer. pub fn syntax_layers(&self) -> impl Iterator> + '_ { - self.syntax - .layers_for_range(0..self.len(), &self.text, true) + self.syntax_layers_for_range(0..self.len(), true) } pub fn syntax_layer_at(&self, position: D) -> Option> { let offset = position.to_offset(self); - self.syntax - .layers_for_range(offset..offset, &self.text, false) + self.syntax_layers_for_range(offset..offset, false) .filter(|l| l.node().end_byte() > offset) .last() } + pub fn syntax_layers_for_range( + &self, + range: Range, + include_hidden: bool, + ) -> impl Iterator> + '_ { + self.syntax + .layers_for_range(range, &self.text, include_hidden) + } + pub fn smallest_syntax_layer_containing( &self, range: Range, @@ -3859,9 +3866,12 @@ impl BufferSnapshot { text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, - body_range: item.body_range.map(|body_range| { - self.anchor_after(body_range.start)..self.anchor_before(body_range.end) - }), + signature_range: item + .signature_range + .map(|r| self.anchor_after(r.start)..self.anchor_before(r.end)), + body_range: item + .body_range + .map(|r| self.anchor_after(r.start)..self.anchor_before(r.end)), annotation_range: annotation_row_range.map(|annotation_range| { self.anchor_after(Point::new(annotation_range.start, 0)) ..self.anchor_before(Point::new( @@ -3901,38 +3911,51 @@ impl BufferSnapshot { let mut open_point = None; let mut close_point = None; + + let mut signature_start = None; + let mut signature_end = None; + let mut extend_signature_range = |node: tree_sitter::Node| { + if signature_start.is_none() { + signature_start = Some(Point::from_ts_point(node.start_position())); + } + signature_end = Some(Point::from_ts_point(node.end_position())); + }; + let mut buffer_ranges = Vec::new(); + let mut add_to_buffer_ranges = |node: tree_sitter::Node, node_is_name| { + let mut range = node.start_byte()..node.end_byte(); + let start = node.start_position(); + if 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)); + } + }; + for capture in mat.captures { - let node_is_name; if capture.index == config.name_capture_ix { - node_is_name = true; + add_to_buffer_ranges(capture.node, true); + extend_signature_range(capture.node); } else if Some(capture.index) == config.context_capture_ix || (Some(capture.index) == config.extra_context_capture_ix && include_extra_context) { - node_is_name = false; + add_to_buffer_ranges(capture.node, false); + extend_signature_range(capture.node); } 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(); @@ -3941,7 +3964,6 @@ impl BufferSnapshot { true, ); let mut last_buffer_range_end = 0; - for (buffer_range, is_name) in buffer_ranges { let space_added = !text.is_empty() && buffer_range.start > last_buffer_range_end; if space_added { @@ -3983,12 +4005,17 @@ impl BufferSnapshot { last_buffer_range_end = buffer_range.end; } + let signature_range = signature_start + .zip(signature_end) + .map(|(start, end)| start..end); + Some(OutlineItem { depth: 0, // We'll calculate the depth later range: item_point_range, text, highlight_ranges, name_ranges, + signature_range, body_range: open_point.zip(close_point).map(|(start, end)| start..end), annotation_range: None, }) diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index d96cd90e03142c6498ae17bc63e1787d99e8557a..09c556cf98f58ea26925e1df8bde9d43ec72e6c7 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -19,6 +19,7 @@ pub struct OutlineItem { pub text: String, pub highlight_ranges: Vec<(Range, HighlightStyle)>, pub name_ranges: Vec>, + pub signature_range: Option>, pub body_range: Option>, pub annotation_range: Option>, } @@ -35,6 +36,10 @@ impl OutlineItem { text: self.text.clone(), highlight_ranges: self.highlight_ranges.clone(), name_ranges: self.name_ranges.clone(), + signature_range: self + .signature_range + .as_ref() + .map(|r| r.start.to_point(buffer)..r.end.to_point(buffer)), body_range: self .body_range .as_ref() @@ -208,6 +213,7 @@ mod tests { text: "class Foo".to_string(), highlight_ranges: vec![], name_ranges: vec![6..9], + signature_range: None, body_range: None, annotation_range: None, }, @@ -217,6 +223,7 @@ mod tests { text: "private".to_string(), highlight_ranges: vec![], name_ranges: vec![], + signature_range: None, body_range: None, annotation_range: None, }, @@ -241,6 +248,7 @@ mod tests { text: "fn process".to_string(), highlight_ranges: vec![], name_ranges: vec![3..10], + signature_range: None, body_range: None, annotation_range: None, }, @@ -250,6 +258,7 @@ mod tests { text: "struct DataProcessor".to_string(), highlight_ranges: vec![], name_ranges: vec![7..20], + signature_range: None, body_range: None, annotation_range: None, }, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f9d99473fbd196152418376d1bfbf63e21ad8b00..1d4c26cf126089261e3534af1da9083d2c126093 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -6129,6 +6129,12 @@ impl MultiBufferSnapshot { text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, + signature_range: item.signature_range.and_then(|signature_range| { + Some( + self.anchor_in_excerpt(*excerpt_id, signature_range.start)? + ..self.anchor_in_excerpt(*excerpt_id, signature_range.end)?, + ) + }), body_range: item.body_range.and_then(|body_range| { Some( self.anchor_in_excerpt(*excerpt_id, body_range.start)? @@ -6169,6 +6175,12 @@ impl MultiBufferSnapshot { text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, + signature_range: item.signature_range.and_then(|signature_range| { + Some( + self.anchor_in_excerpt(excerpt_id, signature_range.start)? + ..self.anchor_in_excerpt(excerpt_id, signature_range.end)?, + ) + }), body_range: item.body_range.and_then(|body_range| { Some( self.anchor_in_excerpt(excerpt_id, body_range.start)? diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 5b1c5b313d29751e53e08bd479eeb72d15527373..bde35a44cb06967a71e585203f3e5f6ee72f96d5 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -2481,6 +2481,7 @@ impl OutlinePanel { &OutlineItem { depth, annotation_range: None, + signature_range: None, range: search_data.context_range.clone(), text: search_data.context_text.clone(), highlight_ranges: search_data