mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
edit predictions: Add new excerpt logic (not yet used) (#38226)
Release Notes: - N/A --------- Co-authored-by: agus <agus@zed.dev>
This commit is contained in:
parent
0784bb8192
commit
853e625259
10 changed files with 722 additions and 27 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
29
crates/edit_prediction_context/Cargo.toml
Normal file
29
crates/edit_prediction_context/Cargo.toml
Normal file
|
|
@ -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
|
||||
1
crates/edit_prediction_context/LICENSE-GPL
Symbolic link
1
crates/edit_prediction_context/LICENSE-GPL
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
mod excerpt;
|
||||
|
||||
pub use excerpt::{EditPredictionExcerpt, EditPredictionExcerptOptions};
|
||||
595
crates/edit_prediction_context/src/excerpt.rs
Normal file
595
crates/edit_prediction_context/src/excerpt.rs
Normal file
|
|
@ -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<usize>,
|
||||
pub parent_signature_ranges: Vec<Range<usize>>,
|
||||
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<Self> {
|
||||
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<usize>, parent_signature_ranges: Vec<Range<usize>>) -> Self {
|
||||
let size = range.len()
|
||||
+ parent_signature_ranges
|
||||
.iter()
|
||||
.map(|r| r.len())
|
||||
.sum::<usize>();
|
||||
Self {
|
||||
range,
|
||||
parent_signature_ranges,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_expanded_range(&self, new_range: Range<usize>) -> 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<usize>,
|
||||
outline_items: &'a [ExcerptOutlineItem],
|
||||
buffer: &'a BufferSnapshot,
|
||||
options: &'a EditPredictionExcerptOptions,
|
||||
}
|
||||
|
||||
struct ExcerptOutlineItem {
|
||||
item_range: Range<usize>,
|
||||
signature_range: Range<usize>,
|
||||
}
|
||||
|
||||
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<EditPredictionExcerpt> {
|
||||
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<Node<'_>> {
|
||||
let mut smallest_exceeding_max_len: Option<Node<'_>> = None;
|
||||
let mut largest: Option<Node<'_>> = 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<EditPredictionExcerpt> {
|
||||
// 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<usize>) -> 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<usize>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -3310,18 +3310,25 @@ impl BufferSnapshot {
|
|||
|
||||
/// Iterates over every [`SyntaxLayer`] in the buffer.
|
||||
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayer<'_>> + '_ {
|
||||
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<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer<'_>> {
|
||||
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<D: ToOffset>(
|
||||
&self,
|
||||
range: Range<D>,
|
||||
include_hidden: bool,
|
||||
) -> impl Iterator<Item = SyntaxLayer<'_>> + '_ {
|
||||
self.syntax
|
||||
.layers_for_range(range, &self.text, include_hidden)
|
||||
}
|
||||
|
||||
pub fn smallest_syntax_layer_containing<D: ToOffset>(
|
||||
&self,
|
||||
range: Range<D>,
|
||||
|
|
@ -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 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 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 range = capture.node.start_byte()..capture.node.end_byte();
|
||||
let start = capture.node.start_position();
|
||||
if capture.node.end_position().row > start.row {
|
||||
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 {
|
||||
if capture.index == config.name_capture_ix {
|
||||
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)
|
||||
{
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub struct OutlineItem<T> {
|
|||
pub text: String,
|
||||
pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
|
||||
pub name_ranges: Vec<Range<usize>>,
|
||||
pub signature_range: Option<Range<T>>,
|
||||
pub body_range: Option<Range<T>>,
|
||||
pub annotation_range: Option<Range<T>>,
|
||||
}
|
||||
|
|
@ -35,6 +36,10 @@ impl<T: ToPoint> OutlineItem<T> {
|
|||
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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue