Reorganize edit prediction code and remove old experiments (#44187)
Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
co-authored by
Agus Zubiaga
Ben Kunkle
parent
76167109db
commit
42583c1141
@@ -0,0 +1,324 @@
|
||||
use crate::RelatedExcerpt;
|
||||
use language::{BufferSnapshot, OffsetRangeExt as _, Point};
|
||||
use std::ops::Range;
|
||||
|
||||
#[cfg(not(test))]
|
||||
const MAX_OUTLINE_ITEM_BODY_SIZE: usize = 512;
|
||||
#[cfg(test)]
|
||||
const MAX_OUTLINE_ITEM_BODY_SIZE: usize = 24;
|
||||
|
||||
pub fn assemble_excerpts(
|
||||
buffer: &BufferSnapshot,
|
||||
mut input_ranges: Vec<Range<Point>>,
|
||||
) -> Vec<RelatedExcerpt> {
|
||||
merge_ranges(&mut input_ranges);
|
||||
|
||||
let mut outline_ranges = Vec::new();
|
||||
let outline_items = buffer.outline_items_as_points_containing(0..buffer.len(), false, None);
|
||||
let mut outline_ix = 0;
|
||||
for input_range in &mut input_ranges {
|
||||
*input_range = clip_range_to_lines(input_range, false, buffer);
|
||||
|
||||
while let Some(outline_item) = outline_items.get(outline_ix) {
|
||||
let item_range = clip_range_to_lines(&outline_item.range, false, buffer);
|
||||
|
||||
if item_range.start > input_range.start {
|
||||
break;
|
||||
}
|
||||
|
||||
if item_range.end > input_range.start {
|
||||
let body_range = outline_item
|
||||
.body_range(buffer)
|
||||
.map(|body| clip_range_to_lines(&body, true, buffer))
|
||||
.filter(|body_range| {
|
||||
body_range.to_offset(buffer).len() > MAX_OUTLINE_ITEM_BODY_SIZE
|
||||
});
|
||||
|
||||
add_outline_item(
|
||||
item_range.clone(),
|
||||
body_range.clone(),
|
||||
buffer,
|
||||
&mut outline_ranges,
|
||||
);
|
||||
|
||||
if let Some(body_range) = body_range
|
||||
&& input_range.start < body_range.start
|
||||
{
|
||||
let mut child_outline_ix = outline_ix + 1;
|
||||
while let Some(next_outline_item) = outline_items.get(child_outline_ix) {
|
||||
if next_outline_item.range.end > body_range.end {
|
||||
break;
|
||||
}
|
||||
if next_outline_item.depth == outline_item.depth + 1 {
|
||||
let next_item_range =
|
||||
clip_range_to_lines(&next_outline_item.range, false, buffer);
|
||||
|
||||
add_outline_item(
|
||||
next_item_range,
|
||||
next_outline_item
|
||||
.body_range(buffer)
|
||||
.map(|body| clip_range_to_lines(&body, true, buffer)),
|
||||
buffer,
|
||||
&mut outline_ranges,
|
||||
);
|
||||
child_outline_ix += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outline_ix += 1;
|
||||
}
|
||||
}
|
||||
|
||||
input_ranges.extend_from_slice(&outline_ranges);
|
||||
merge_ranges(&mut input_ranges);
|
||||
|
||||
input_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let offset_range = range.to_offset(buffer);
|
||||
RelatedExcerpt {
|
||||
point_range: range,
|
||||
anchor_range: buffer.anchor_before(offset_range.start)
|
||||
..buffer.anchor_after(offset_range.end),
|
||||
text: buffer.as_rope().slice(offset_range),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn clip_range_to_lines(
|
||||
range: &Range<Point>,
|
||||
inward: bool,
|
||||
buffer: &BufferSnapshot,
|
||||
) -> Range<Point> {
|
||||
let mut range = range.clone();
|
||||
if inward {
|
||||
if range.start.column > 0 {
|
||||
range.start.column = buffer.line_len(range.start.row);
|
||||
}
|
||||
range.end.column = 0;
|
||||
} else {
|
||||
range.start.column = 0;
|
||||
if range.end.column > 0 {
|
||||
range.end.column = buffer.line_len(range.end.row);
|
||||
}
|
||||
}
|
||||
range
|
||||
}
|
||||
|
||||
fn add_outline_item(
|
||||
mut item_range: Range<Point>,
|
||||
body_range: Option<Range<Point>>,
|
||||
buffer: &BufferSnapshot,
|
||||
outline_ranges: &mut Vec<Range<Point>>,
|
||||
) {
|
||||
if let Some(mut body_range) = body_range {
|
||||
if body_range.start.column > 0 {
|
||||
body_range.start.column = buffer.line_len(body_range.start.row);
|
||||
}
|
||||
body_range.end.column = 0;
|
||||
|
||||
let head_range = item_range.start..body_range.start;
|
||||
if head_range.start < head_range.end {
|
||||
outline_ranges.push(head_range);
|
||||
}
|
||||
|
||||
let tail_range = body_range.end..item_range.end;
|
||||
if tail_range.start < tail_range.end {
|
||||
outline_ranges.push(tail_range);
|
||||
}
|
||||
} else {
|
||||
item_range.start.column = 0;
|
||||
item_range.end.column = buffer.line_len(item_range.end.row);
|
||||
outline_ranges.push(item_range);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_ranges(ranges: &mut Vec<Range<Point>>) {
|
||||
ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
|
||||
|
||||
let mut index = 1;
|
||||
while index < ranges.len() {
|
||||
let mut prev_range_end = ranges[index - 1].end;
|
||||
if prev_range_end.column > 0 {
|
||||
prev_range_end += Point::new(1, 0);
|
||||
}
|
||||
|
||||
if (prev_range_end + Point::new(1, 0))
|
||||
.cmp(&ranges[index].start)
|
||||
.is_ge()
|
||||
{
|
||||
let removed = ranges.remove(index);
|
||||
if removed.end.cmp(&ranges[index - 1].end).is_gt() {
|
||||
ranges[index - 1].end = removed.end;
|
||||
}
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{TestAppContext, prelude::*};
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, Language, LanguageConfig, LanguageMatcher, OffsetRangeExt};
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::{fmt::Write as _, sync::Arc};
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_rust(cx: &mut TestAppContext) {
|
||||
let table = [
|
||||
(
|
||||
indoc! {r#"
|
||||
struct User {
|
||||
first_name: String,
|
||||
«last_name»: String,
|
||||
age: u32,
|
||||
email: String,
|
||||
create_at: Instant,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn first_name(&self) -> String {
|
||||
self.first_name.clone()
|
||||
}
|
||||
|
||||
pub fn full_name(&self) -> String {
|
||||
« format!("{} {}", self.first_name, self.last_name)
|
||||
» }
|
||||
}
|
||||
"#},
|
||||
indoc! {r#"
|
||||
struct User {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
…
|
||||
}
|
||||
|
||||
impl User {
|
||||
…
|
||||
pub fn full_name(&self) -> String {
|
||||
format!("{} {}", self.first_name, self.last_name)
|
||||
}
|
||||
}
|
||||
"#},
|
||||
),
|
||||
(
|
||||
indoc! {r#"
|
||||
struct «User» {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
impl User {
|
||||
// methods
|
||||
}
|
||||
"#
|
||||
},
|
||||
indoc! {r#"
|
||||
struct User {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
age: u32,
|
||||
}
|
||||
…
|
||||
"#},
|
||||
),
|
||||
(
|
||||
indoc! {r#"
|
||||
trait «FooProvider» {
|
||||
const NAME: &'static str;
|
||||
|
||||
fn provide_foo(&self, id: usize) -> Foo;
|
||||
|
||||
fn provide_foo_batched(&self, ids: &[usize]) -> Vec<Foo> {
|
||||
ids.iter()
|
||||
.map(|id| self.provide_foo(*id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sync(&self);
|
||||
}
|
||||
"#
|
||||
},
|
||||
indoc! {r#"
|
||||
trait FooProvider {
|
||||
const NAME: &'static str;
|
||||
|
||||
fn provide_foo(&self, id: usize) -> Foo;
|
||||
|
||||
fn provide_foo_batched(&self, ids: &[usize]) -> Vec<Foo> {
|
||||
…
|
||||
}
|
||||
|
||||
fn sync(&self);
|
||||
}
|
||||
"#},
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected_output) in table {
|
||||
let (input, ranges) = marked_text_ranges(&input, false);
|
||||
let buffer =
|
||||
cx.new(|cx| Buffer::local(input, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
buffer.read_with(cx, |buffer, _cx| {
|
||||
let ranges: Vec<Range<Point>> = ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_point(&buffer))
|
||||
.collect();
|
||||
|
||||
let excerpts = assemble_excerpts(&buffer.snapshot(), ranges);
|
||||
|
||||
let output = format_excerpts(buffer, &excerpts);
|
||||
assert_eq!(output, expected_output);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn format_excerpts(buffer: &Buffer, excerpts: &[RelatedExcerpt]) -> String {
|
||||
let mut output = String::new();
|
||||
let file_line_count = buffer.max_point().row;
|
||||
let mut current_row = 0;
|
||||
for excerpt in excerpts {
|
||||
if excerpt.text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if current_row < excerpt.point_range.start.row {
|
||||
writeln!(&mut output, "…").unwrap();
|
||||
}
|
||||
current_row = excerpt.point_range.start.row;
|
||||
|
||||
for line in excerpt.text.to_string().lines() {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
current_row += 1;
|
||||
}
|
||||
}
|
||||
if current_row < file_line_count {
|
||||
writeln!(&mut output, "…").unwrap();
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(language::tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
use cloud_llm_client::predict_edits_v3::{self, Line};
|
||||
use language::{Language, LanguageId};
|
||||
use project::ProjectEntryId;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
use text::{Bias, BufferId, Rope};
|
||||
use util::paths::{path_ends_with, strip_path_suffix};
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
use crate::outline::OutlineDeclaration;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Identifier {
|
||||
pub name: Arc<str>,
|
||||
pub language_id: LanguageId,
|
||||
}
|
||||
|
||||
slotmap::new_key_type! {
|
||||
pub struct DeclarationId;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Declaration {
|
||||
File {
|
||||
project_entry_id: ProjectEntryId,
|
||||
declaration: FileDeclaration,
|
||||
cached_path: CachedDeclarationPath,
|
||||
},
|
||||
Buffer {
|
||||
project_entry_id: ProjectEntryId,
|
||||
buffer_id: BufferId,
|
||||
rope: Rope,
|
||||
declaration: BufferDeclaration,
|
||||
cached_path: CachedDeclarationPath,
|
||||
},
|
||||
}
|
||||
|
||||
const ITEM_TEXT_TRUNCATION_LENGTH: usize = 1024;
|
||||
|
||||
impl Declaration {
|
||||
pub fn identifier(&self) -> &Identifier {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => &declaration.identifier,
|
||||
Declaration::Buffer { declaration, .. } => &declaration.identifier,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<DeclarationId> {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => declaration.parent,
|
||||
Declaration::Buffer { declaration, .. } => declaration.parent,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_buffer(&self) -> Option<&BufferDeclaration> {
|
||||
match self {
|
||||
Declaration::File { .. } => None,
|
||||
Declaration::Buffer { declaration, .. } => Some(declaration),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_file(&self) -> Option<&FileDeclaration> {
|
||||
match self {
|
||||
Declaration::Buffer { .. } => None,
|
||||
Declaration::File { declaration, .. } => Some(declaration),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project_entry_id(&self) -> ProjectEntryId {
|
||||
match self {
|
||||
Declaration::File {
|
||||
project_entry_id, ..
|
||||
} => *project_entry_id,
|
||||
Declaration::Buffer {
|
||||
project_entry_id, ..
|
||||
} => *project_entry_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cached_path(&self) -> &CachedDeclarationPath {
|
||||
match self {
|
||||
Declaration::File { cached_path, .. } => cached_path,
|
||||
Declaration::Buffer { cached_path, .. } => cached_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_range(&self) -> Range<usize> {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => declaration.item_range.clone(),
|
||||
Declaration::Buffer { declaration, .. } => declaration.item_range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_line_range(&self) -> Range<Line> {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => declaration.item_line_range.clone(),
|
||||
Declaration::Buffer {
|
||||
declaration, rope, ..
|
||||
} => {
|
||||
Line(rope.offset_to_point(declaration.item_range.start).row)
|
||||
..Line(rope.offset_to_point(declaration.item_range.end).row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_text(&self) -> (Cow<'_, str>, bool) {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => (
|
||||
declaration.text.as_ref().into(),
|
||||
declaration.text_is_truncated,
|
||||
),
|
||||
Declaration::Buffer {
|
||||
rope, declaration, ..
|
||||
} => (
|
||||
rope.chunks_in_range(declaration.item_range.clone())
|
||||
.collect::<Cow<str>>(),
|
||||
declaration.item_range_is_truncated,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_text(&self) -> (Cow<'_, str>, bool) {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => (
|
||||
declaration.text[self.signature_range_in_item_text()].into(),
|
||||
declaration.signature_is_truncated,
|
||||
),
|
||||
Declaration::Buffer {
|
||||
rope, declaration, ..
|
||||
} => (
|
||||
rope.chunks_in_range(declaration.signature_range.clone())
|
||||
.collect::<Cow<str>>(),
|
||||
declaration.signature_range_is_truncated,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_range(&self) -> Range<usize> {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => declaration.signature_range.clone(),
|
||||
Declaration::Buffer { declaration, .. } => declaration.signature_range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_line_range(&self) -> Range<Line> {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => declaration.signature_line_range.clone(),
|
||||
Declaration::Buffer {
|
||||
declaration, rope, ..
|
||||
} => {
|
||||
Line(rope.offset_to_point(declaration.signature_range.start).row)
|
||||
..Line(rope.offset_to_point(declaration.signature_range.end).row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_range_in_item_text(&self) -> Range<usize> {
|
||||
let signature_range = self.signature_range();
|
||||
let item_range = self.item_range();
|
||||
signature_range.start.saturating_sub(item_range.start)
|
||||
..(signature_range.end.saturating_sub(item_range.start)).min(item_range.len())
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_range_to_line_boundaries_and_truncate(
|
||||
range: &Range<usize>,
|
||||
limit: usize,
|
||||
rope: &Rope,
|
||||
) -> (Range<usize>, Range<predict_edits_v3::Line>, bool) {
|
||||
let mut point_range = rope.offset_to_point(range.start)..rope.offset_to_point(range.end);
|
||||
point_range.start.column = 0;
|
||||
point_range.end.row += 1;
|
||||
point_range.end.column = 0;
|
||||
|
||||
let mut item_range =
|
||||
rope.point_to_offset(point_range.start)..rope.point_to_offset(point_range.end);
|
||||
let is_truncated = item_range.len() > limit;
|
||||
if is_truncated {
|
||||
item_range.end = item_range.start + limit;
|
||||
}
|
||||
item_range.end = rope.clip_offset(item_range.end, Bias::Left);
|
||||
|
||||
let line_range =
|
||||
predict_edits_v3::Line(point_range.start.row)..predict_edits_v3::Line(point_range.end.row);
|
||||
(item_range, line_range, is_truncated)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileDeclaration {
|
||||
pub parent: Option<DeclarationId>,
|
||||
pub identifier: Identifier,
|
||||
/// offset range of the declaration in the file, expanded to line boundaries and truncated
|
||||
pub item_range: Range<usize>,
|
||||
/// line range of the declaration in the file, potentially truncated
|
||||
pub item_line_range: Range<predict_edits_v3::Line>,
|
||||
/// text of `item_range`
|
||||
pub text: Arc<str>,
|
||||
/// whether `text` was truncated
|
||||
pub text_is_truncated: bool,
|
||||
/// offset range of the signature in the file, expanded to line boundaries and truncated
|
||||
pub signature_range: Range<usize>,
|
||||
/// line range of the signature in the file, truncated
|
||||
pub signature_line_range: Range<Line>,
|
||||
/// whether `signature` was truncated
|
||||
pub signature_is_truncated: bool,
|
||||
}
|
||||
|
||||
impl FileDeclaration {
|
||||
pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> FileDeclaration {
|
||||
let (item_range_in_file, item_line_range_in_file, text_is_truncated) =
|
||||
expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.item_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
rope,
|
||||
);
|
||||
|
||||
let (mut signature_range_in_file, signature_line_range, mut signature_is_truncated) =
|
||||
expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.signature_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
rope,
|
||||
);
|
||||
|
||||
if signature_range_in_file.start < item_range_in_file.start {
|
||||
signature_range_in_file.start = item_range_in_file.start;
|
||||
signature_is_truncated = true;
|
||||
}
|
||||
if signature_range_in_file.end > item_range_in_file.end {
|
||||
signature_range_in_file.end = item_range_in_file.end;
|
||||
signature_is_truncated = true;
|
||||
}
|
||||
|
||||
FileDeclaration {
|
||||
parent: None,
|
||||
identifier: declaration.identifier,
|
||||
signature_range: signature_range_in_file,
|
||||
signature_line_range,
|
||||
signature_is_truncated,
|
||||
text: rope
|
||||
.chunks_in_range(item_range_in_file.clone())
|
||||
.collect::<String>()
|
||||
.into(),
|
||||
text_is_truncated,
|
||||
item_range: item_range_in_file,
|
||||
item_line_range: item_line_range_in_file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BufferDeclaration {
|
||||
pub parent: Option<DeclarationId>,
|
||||
pub identifier: Identifier,
|
||||
pub item_range: Range<usize>,
|
||||
pub item_range_is_truncated: bool,
|
||||
pub signature_range: Range<usize>,
|
||||
pub signature_range_is_truncated: bool,
|
||||
}
|
||||
|
||||
impl BufferDeclaration {
|
||||
pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> Self {
|
||||
let (item_range, _item_line_range, item_range_is_truncated) =
|
||||
expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.item_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
rope,
|
||||
);
|
||||
let (signature_range, _signature_line_range, signature_range_is_truncated) =
|
||||
expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.signature_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
rope,
|
||||
);
|
||||
Self {
|
||||
parent: None,
|
||||
identifier: declaration.identifier,
|
||||
item_range,
|
||||
item_range_is_truncated,
|
||||
signature_range,
|
||||
signature_range_is_truncated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedDeclarationPath {
|
||||
pub worktree_abs_path: Arc<Path>,
|
||||
pub rel_path: Arc<RelPath>,
|
||||
/// The relative path of the file, possibly stripped according to `import_path_strip_regex`.
|
||||
pub rel_path_after_regex_stripping: Arc<RelPath>,
|
||||
}
|
||||
|
||||
impl CachedDeclarationPath {
|
||||
pub fn new(
|
||||
worktree_abs_path: Arc<Path>,
|
||||
path: &Arc<RelPath>,
|
||||
language: Option<&Arc<Language>>,
|
||||
) -> Self {
|
||||
let rel_path = path.clone();
|
||||
let rel_path_after_regex_stripping = if let Some(language) = language
|
||||
&& let Some(strip_regex) = language.config().import_path_strip_regex.as_ref()
|
||||
&& let Ok(stripped) = RelPath::unix(&Path::new(
|
||||
strip_regex.replace_all(rel_path.as_unix_str(), "").as_ref(),
|
||||
)) {
|
||||
Arc::from(stripped)
|
||||
} else {
|
||||
rel_path.clone()
|
||||
};
|
||||
CachedDeclarationPath {
|
||||
worktree_abs_path,
|
||||
rel_path,
|
||||
rel_path_after_regex_stripping,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_for_test(worktree_abs_path: &str, rel_path: &str) -> Self {
|
||||
let rel_path: Arc<RelPath> = util::rel_path::rel_path(rel_path).into();
|
||||
CachedDeclarationPath {
|
||||
worktree_abs_path: std::path::PathBuf::from(worktree_abs_path).into(),
|
||||
rel_path_after_regex_stripping: rel_path.clone(),
|
||||
rel_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ends_with_posix_path(&self, path: &Path) -> bool {
|
||||
if path.as_os_str().len() <= self.rel_path_after_regex_stripping.as_unix_str().len() {
|
||||
path_ends_with(self.rel_path_after_regex_stripping.as_std_path(), path)
|
||||
} else {
|
||||
if let Some(remaining) =
|
||||
strip_path_suffix(path, self.rel_path_after_regex_stripping.as_std_path())
|
||||
{
|
||||
path_ends_with(&self.worktree_abs_path, remaining)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equals_absolute_path(&self, path: &Path) -> bool {
|
||||
if let Some(remaining) =
|
||||
strip_path_suffix(path, &self.rel_path_after_regex_stripping.as_std_path())
|
||||
{
|
||||
self.worktree_abs_path.as_ref() == remaining
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
use cloud_llm_client::predict_edits_v3::DeclarationScoreComponents;
|
||||
use collections::HashMap;
|
||||
use language::BufferSnapshot;
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::ProjectEntryId;
|
||||
use serde::Serialize;
|
||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||
use strum::EnumIter;
|
||||
use text::{Point, ToPoint};
|
||||
use util::RangeExt as _;
|
||||
|
||||
use crate::{
|
||||
CachedDeclarationPath, Declaration, EditPredictionExcerpt, Identifier,
|
||||
imports::{Import, Imports, Module},
|
||||
reference::{Reference, ReferenceRegion},
|
||||
syntax_index::SyntaxIndexState,
|
||||
text_similarity::{Occurrences, jaccard_similarity, weighted_overlap_coefficient},
|
||||
};
|
||||
|
||||
const MAX_IDENTIFIER_DECLARATION_COUNT: usize = 16;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditPredictionScoreOptions {
|
||||
pub omit_excerpt_overlaps: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScoredDeclaration {
|
||||
/// identifier used by the local reference
|
||||
pub identifier: Identifier,
|
||||
pub declaration: Declaration,
|
||||
pub components: DeclarationScoreComponents,
|
||||
}
|
||||
|
||||
#[derive(EnumIter, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum DeclarationStyle {
|
||||
Signature,
|
||||
Declaration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Default)]
|
||||
pub struct DeclarationScores {
|
||||
pub signature: f32,
|
||||
pub declaration: f32,
|
||||
pub retrieval: f32,
|
||||
}
|
||||
|
||||
impl ScoredDeclaration {
|
||||
/// Returns the score for this declaration with the specified style.
|
||||
pub fn score(&self, style: DeclarationStyle) -> f32 {
|
||||
// TODO: handle truncation
|
||||
|
||||
// Score related to how likely this is the correct declaration, range 0 to 1
|
||||
let retrieval = self.retrieval_score();
|
||||
|
||||
// Score related to the distance between the reference and cursor, range 0 to 1
|
||||
let distance_score = if self.components.is_referenced_nearby {
|
||||
1.0 / (1.0 + self.components.reference_line_distance as f32 / 10.0).powf(2.0)
|
||||
} else {
|
||||
// same score as ~14 lines away, rationale is to not overly penalize references from parent signatures
|
||||
0.5
|
||||
};
|
||||
|
||||
// For now instead of linear combination, the scores are just multiplied together.
|
||||
let combined_score = 10.0 * retrieval * distance_score;
|
||||
|
||||
match style {
|
||||
DeclarationStyle::Signature => {
|
||||
combined_score * self.components.excerpt_vs_signature_weighted_overlap
|
||||
}
|
||||
DeclarationStyle::Declaration => {
|
||||
2.0 * combined_score * self.components.excerpt_vs_item_weighted_overlap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retrieval_score(&self) -> f32 {
|
||||
let mut score = if self.components.is_same_file {
|
||||
10.0 / self.components.same_file_declaration_count as f32
|
||||
} else if self.components.path_import_match_count > 0 {
|
||||
3.0
|
||||
} else if self.components.wildcard_path_import_match_count > 0 {
|
||||
1.0
|
||||
} else if self.components.normalized_import_similarity > 0.0 {
|
||||
self.components.normalized_import_similarity
|
||||
} else if self.components.normalized_wildcard_import_similarity > 0.0 {
|
||||
0.5 * self.components.normalized_wildcard_import_similarity
|
||||
} else {
|
||||
1.0 / self.components.declaration_count as f32
|
||||
};
|
||||
score *= 1. + self.components.included_by_others as f32 / 2.;
|
||||
score *= 1. + self.components.includes_others as f32 / 4.;
|
||||
score
|
||||
}
|
||||
|
||||
pub fn size(&self, style: DeclarationStyle) -> usize {
|
||||
match &self.declaration {
|
||||
Declaration::File { declaration, .. } => match style {
|
||||
DeclarationStyle::Signature => declaration.signature_range.len(),
|
||||
DeclarationStyle::Declaration => declaration.text.len(),
|
||||
},
|
||||
Declaration::Buffer { declaration, .. } => match style {
|
||||
DeclarationStyle::Signature => declaration.signature_range.len(),
|
||||
DeclarationStyle::Declaration => declaration.item_range.len(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn score_density(&self, style: DeclarationStyle) -> f32 {
|
||||
self.score(style) / self.size(style) as f32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scored_declarations(
|
||||
options: &EditPredictionScoreOptions,
|
||||
index: &SyntaxIndexState,
|
||||
excerpt: &EditPredictionExcerpt,
|
||||
excerpt_occurrences: &Occurrences,
|
||||
adjacent_occurrences: &Occurrences,
|
||||
imports: &Imports,
|
||||
identifier_to_references: HashMap<Identifier, Vec<Reference>>,
|
||||
cursor_offset: usize,
|
||||
current_buffer: &BufferSnapshot,
|
||||
) -> Vec<ScoredDeclaration> {
|
||||
let cursor_point = cursor_offset.to_point(¤t_buffer);
|
||||
|
||||
let mut wildcard_import_occurrences = Vec::new();
|
||||
let mut wildcard_import_paths = Vec::new();
|
||||
for wildcard_import in imports.wildcard_modules.iter() {
|
||||
match wildcard_import {
|
||||
Module::Namespace(namespace) => {
|
||||
wildcard_import_occurrences.push(namespace.occurrences())
|
||||
}
|
||||
Module::SourceExact(path) => wildcard_import_paths.push(path),
|
||||
Module::SourceFuzzy(path) => {
|
||||
wildcard_import_occurrences.push(Occurrences::from_path(&path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut scored_declarations = Vec::new();
|
||||
let mut project_entry_id_to_outline_ranges: HashMap<ProjectEntryId, Vec<Range<usize>>> =
|
||||
HashMap::default();
|
||||
for (identifier, references) in identifier_to_references {
|
||||
let mut import_occurrences = Vec::new();
|
||||
let mut import_paths = Vec::new();
|
||||
let mut found_external_identifier: Option<&Identifier> = None;
|
||||
|
||||
if let Some(imports) = imports.identifier_to_imports.get(&identifier) {
|
||||
// only use alias when it's the only import, could be generalized if some language
|
||||
// has overlapping aliases
|
||||
//
|
||||
// TODO: when an aliased declaration is included in the prompt, should include the
|
||||
// aliasing in the prompt.
|
||||
//
|
||||
// TODO: For SourceFuzzy consider having componentwise comparison that pays
|
||||
// attention to ordering.
|
||||
if let [
|
||||
Import::Alias {
|
||||
module,
|
||||
external_identifier,
|
||||
},
|
||||
] = imports.as_slice()
|
||||
{
|
||||
match module {
|
||||
Module::Namespace(namespace) => {
|
||||
import_occurrences.push(namespace.occurrences())
|
||||
}
|
||||
Module::SourceExact(path) => import_paths.push(path),
|
||||
Module::SourceFuzzy(path) => {
|
||||
import_occurrences.push(Occurrences::from_path(&path))
|
||||
}
|
||||
}
|
||||
found_external_identifier = Some(&external_identifier);
|
||||
} else {
|
||||
for import in imports {
|
||||
match import {
|
||||
Import::Direct { module } => match module {
|
||||
Module::Namespace(namespace) => {
|
||||
import_occurrences.push(namespace.occurrences())
|
||||
}
|
||||
Module::SourceExact(path) => import_paths.push(path),
|
||||
Module::SourceFuzzy(path) => {
|
||||
import_occurrences.push(Occurrences::from_path(&path))
|
||||
}
|
||||
},
|
||||
Import::Alias { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let identifier_to_lookup = found_external_identifier.unwrap_or(&identifier);
|
||||
// TODO: update this to be able to return more declarations? Especially if there is the
|
||||
// ability to quickly filter a large list (based on imports)
|
||||
let identifier_declarations = index
|
||||
.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(&identifier_to_lookup);
|
||||
let declaration_count = identifier_declarations.len();
|
||||
|
||||
if declaration_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: option to filter out other candidates when same file / import match
|
||||
let mut checked_declarations = Vec::with_capacity(declaration_count);
|
||||
for (declaration_id, declaration) in identifier_declarations {
|
||||
match declaration {
|
||||
Declaration::Buffer {
|
||||
buffer_id,
|
||||
declaration: buffer_declaration,
|
||||
..
|
||||
} => {
|
||||
if buffer_id == ¤t_buffer.remote_id() {
|
||||
let already_included_in_prompt =
|
||||
range_intersection(&buffer_declaration.item_range, &excerpt.range)
|
||||
.is_some()
|
||||
|| excerpt
|
||||
.parent_declarations
|
||||
.iter()
|
||||
.any(|(excerpt_parent, _)| excerpt_parent == &declaration_id);
|
||||
if !options.omit_excerpt_overlaps || !already_included_in_prompt {
|
||||
let declaration_line = buffer_declaration
|
||||
.item_range
|
||||
.start
|
||||
.to_point(current_buffer)
|
||||
.row;
|
||||
let declaration_line_distance =
|
||||
(cursor_point.row as i32 - declaration_line as i32).unsigned_abs();
|
||||
checked_declarations.push(CheckedDeclaration {
|
||||
declaration,
|
||||
same_file_line_distance: Some(declaration_line_distance),
|
||||
path_import_match_count: 0,
|
||||
wildcard_path_import_match_count: 0,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
}
|
||||
}
|
||||
Declaration::File { .. } => {}
|
||||
}
|
||||
let declaration_path = declaration.cached_path();
|
||||
let path_import_match_count = import_paths
|
||||
.iter()
|
||||
.filter(|import_path| {
|
||||
declaration_path_matches_import(&declaration_path, import_path)
|
||||
})
|
||||
.count();
|
||||
let wildcard_path_import_match_count = wildcard_import_paths
|
||||
.iter()
|
||||
.filter(|import_path| {
|
||||
declaration_path_matches_import(&declaration_path, import_path)
|
||||
})
|
||||
.count();
|
||||
checked_declarations.push(CheckedDeclaration {
|
||||
declaration,
|
||||
same_file_line_distance: None,
|
||||
path_import_match_count,
|
||||
wildcard_path_import_match_count,
|
||||
});
|
||||
}
|
||||
|
||||
let mut max_import_similarity = 0.0;
|
||||
let mut max_wildcard_import_similarity = 0.0;
|
||||
|
||||
let mut scored_declarations_for_identifier = Vec::with_capacity(checked_declarations.len());
|
||||
for checked_declaration in checked_declarations {
|
||||
let same_file_declaration_count =
|
||||
index.file_declaration_count(checked_declaration.declaration);
|
||||
|
||||
let declaration = score_declaration(
|
||||
&identifier,
|
||||
&references,
|
||||
checked_declaration,
|
||||
same_file_declaration_count,
|
||||
declaration_count,
|
||||
&excerpt_occurrences,
|
||||
&adjacent_occurrences,
|
||||
&import_occurrences,
|
||||
&wildcard_import_occurrences,
|
||||
cursor_point,
|
||||
current_buffer,
|
||||
);
|
||||
|
||||
if declaration.components.import_similarity > max_import_similarity {
|
||||
max_import_similarity = declaration.components.import_similarity;
|
||||
}
|
||||
|
||||
if declaration.components.wildcard_import_similarity > max_wildcard_import_similarity {
|
||||
max_wildcard_import_similarity = declaration.components.wildcard_import_similarity;
|
||||
}
|
||||
|
||||
project_entry_id_to_outline_ranges
|
||||
.entry(declaration.declaration.project_entry_id())
|
||||
.or_default()
|
||||
.push(declaration.declaration.item_range());
|
||||
scored_declarations_for_identifier.push(declaration);
|
||||
}
|
||||
|
||||
if max_import_similarity > 0.0 || max_wildcard_import_similarity > 0.0 {
|
||||
for declaration in scored_declarations_for_identifier.iter_mut() {
|
||||
if max_import_similarity > 0.0 {
|
||||
declaration.components.max_import_similarity = max_import_similarity;
|
||||
declaration.components.normalized_import_similarity =
|
||||
declaration.components.import_similarity / max_import_similarity;
|
||||
}
|
||||
if max_wildcard_import_similarity > 0.0 {
|
||||
declaration.components.normalized_wildcard_import_similarity =
|
||||
declaration.components.wildcard_import_similarity
|
||||
/ max_wildcard_import_similarity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scored_declarations.extend(scored_declarations_for_identifier);
|
||||
}
|
||||
|
||||
// TODO: Inform this via import / retrieval scores of outline items
|
||||
// TODO: Consider using a sweepline
|
||||
for scored_declaration in scored_declarations.iter_mut() {
|
||||
let project_entry_id = scored_declaration.declaration.project_entry_id();
|
||||
let Some(ranges) = project_entry_id_to_outline_ranges.get(&project_entry_id) else {
|
||||
continue;
|
||||
};
|
||||
for range in ranges {
|
||||
if range.contains_inclusive(&scored_declaration.declaration.item_range()) {
|
||||
scored_declaration.components.included_by_others += 1
|
||||
} else if scored_declaration
|
||||
.declaration
|
||||
.item_range()
|
||||
.contains_inclusive(range)
|
||||
{
|
||||
scored_declaration.components.includes_others += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scored_declarations.sort_unstable_by_key(|declaration| {
|
||||
Reverse(OrderedFloat(
|
||||
declaration.score(DeclarationStyle::Declaration),
|
||||
))
|
||||
});
|
||||
|
||||
scored_declarations
|
||||
}
|
||||
|
||||
struct CheckedDeclaration<'a> {
|
||||
declaration: &'a Declaration,
|
||||
same_file_line_distance: Option<u32>,
|
||||
path_import_match_count: usize,
|
||||
wildcard_path_import_match_count: usize,
|
||||
}
|
||||
|
||||
fn declaration_path_matches_import(
|
||||
declaration_path: &CachedDeclarationPath,
|
||||
import_path: &Arc<Path>,
|
||||
) -> bool {
|
||||
if import_path.is_absolute() {
|
||||
declaration_path.equals_absolute_path(import_path)
|
||||
} else {
|
||||
declaration_path.ends_with_posix_path(import_path)
|
||||
}
|
||||
}
|
||||
|
||||
fn range_intersection<T: Ord + Clone>(a: &Range<T>, b: &Range<T>) -> Option<Range<T>> {
|
||||
let start = a.start.clone().max(b.start.clone());
|
||||
let end = a.end.clone().min(b.end.clone());
|
||||
if start < end {
|
||||
Some(Range { start, end })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn score_declaration(
|
||||
identifier: &Identifier,
|
||||
references: &[Reference],
|
||||
checked_declaration: CheckedDeclaration,
|
||||
same_file_declaration_count: usize,
|
||||
declaration_count: usize,
|
||||
excerpt_occurrences: &Occurrences,
|
||||
adjacent_occurrences: &Occurrences,
|
||||
import_occurrences: &[Occurrences],
|
||||
wildcard_import_occurrences: &[Occurrences],
|
||||
cursor: Point,
|
||||
current_buffer: &BufferSnapshot,
|
||||
) -> ScoredDeclaration {
|
||||
let CheckedDeclaration {
|
||||
declaration,
|
||||
same_file_line_distance,
|
||||
path_import_match_count,
|
||||
wildcard_path_import_match_count,
|
||||
} = checked_declaration;
|
||||
|
||||
let is_referenced_nearby = references
|
||||
.iter()
|
||||
.any(|r| r.region == ReferenceRegion::Nearby);
|
||||
let is_referenced_in_breadcrumb = references
|
||||
.iter()
|
||||
.any(|r| r.region == ReferenceRegion::Breadcrumb);
|
||||
let reference_count = references.len();
|
||||
let reference_line_distance = references
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let reference_line = r.range.start.to_point(current_buffer).row as i32;
|
||||
(cursor.row as i32 - reference_line).unsigned_abs()
|
||||
})
|
||||
.min()
|
||||
.unwrap();
|
||||
|
||||
let is_same_file = same_file_line_distance.is_some();
|
||||
let declaration_line_distance = same_file_line_distance.unwrap_or(u32::MAX);
|
||||
|
||||
let item_source_occurrences = Occurrences::within_string(&declaration.item_text().0);
|
||||
let item_signature_occurrences = Occurrences::within_string(&declaration.signature_text().0);
|
||||
let excerpt_vs_item_jaccard = jaccard_similarity(excerpt_occurrences, &item_source_occurrences);
|
||||
let excerpt_vs_signature_jaccard =
|
||||
jaccard_similarity(excerpt_occurrences, &item_signature_occurrences);
|
||||
let adjacent_vs_item_jaccard =
|
||||
jaccard_similarity(adjacent_occurrences, &item_source_occurrences);
|
||||
let adjacent_vs_signature_jaccard =
|
||||
jaccard_similarity(adjacent_occurrences, &item_signature_occurrences);
|
||||
|
||||
let excerpt_vs_item_weighted_overlap =
|
||||
weighted_overlap_coefficient(excerpt_occurrences, &item_source_occurrences);
|
||||
let excerpt_vs_signature_weighted_overlap =
|
||||
weighted_overlap_coefficient(excerpt_occurrences, &item_signature_occurrences);
|
||||
let adjacent_vs_item_weighted_overlap =
|
||||
weighted_overlap_coefficient(adjacent_occurrences, &item_source_occurrences);
|
||||
let adjacent_vs_signature_weighted_overlap =
|
||||
weighted_overlap_coefficient(adjacent_occurrences, &item_signature_occurrences);
|
||||
|
||||
let mut import_similarity = 0f32;
|
||||
let mut wildcard_import_similarity = 0f32;
|
||||
if !import_occurrences.is_empty() || !wildcard_import_occurrences.is_empty() {
|
||||
let cached_path = declaration.cached_path();
|
||||
let path_occurrences = Occurrences::from_worktree_path(
|
||||
cached_path
|
||||
.worktree_abs_path
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy()),
|
||||
&cached_path.rel_path,
|
||||
);
|
||||
import_similarity = import_occurrences
|
||||
.iter()
|
||||
.map(|namespace_occurrences| {
|
||||
OrderedFloat(jaccard_similarity(namespace_occurrences, &path_occurrences))
|
||||
})
|
||||
.max()
|
||||
.map(|similarity| similarity.into_inner())
|
||||
.unwrap_or_default();
|
||||
|
||||
// TODO: Consider something other than max
|
||||
wildcard_import_similarity = wildcard_import_occurrences
|
||||
.iter()
|
||||
.map(|namespace_occurrences| {
|
||||
OrderedFloat(jaccard_similarity(namespace_occurrences, &path_occurrences))
|
||||
})
|
||||
.max()
|
||||
.map(|similarity| similarity.into_inner())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
// TODO: Consider adding declaration_file_count
|
||||
let score_components = DeclarationScoreComponents {
|
||||
is_same_file,
|
||||
is_referenced_nearby,
|
||||
is_referenced_in_breadcrumb,
|
||||
reference_line_distance,
|
||||
declaration_line_distance,
|
||||
reference_count,
|
||||
same_file_declaration_count,
|
||||
declaration_count,
|
||||
excerpt_vs_item_jaccard,
|
||||
excerpt_vs_signature_jaccard,
|
||||
adjacent_vs_item_jaccard,
|
||||
adjacent_vs_signature_jaccard,
|
||||
excerpt_vs_item_weighted_overlap,
|
||||
excerpt_vs_signature_weighted_overlap,
|
||||
adjacent_vs_item_weighted_overlap,
|
||||
adjacent_vs_signature_weighted_overlap,
|
||||
path_import_match_count,
|
||||
wildcard_path_import_match_count,
|
||||
import_similarity,
|
||||
max_import_similarity: 0.0,
|
||||
normalized_import_similarity: 0.0,
|
||||
wildcard_import_similarity,
|
||||
normalized_wildcard_import_similarity: 0.0,
|
||||
included_by_others: 0,
|
||||
includes_others: 0,
|
||||
};
|
||||
|
||||
ScoredDeclaration {
|
||||
identifier: identifier.clone(),
|
||||
declaration: declaration.clone(),
|
||||
components: score_components,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_declaration_path_matches() {
|
||||
let declaration_path =
|
||||
CachedDeclarationPath::new_for_test("/home/user/project", "src/maths.ts");
|
||||
|
||||
assert!(declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("maths.ts").into()
|
||||
));
|
||||
|
||||
assert!(declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("project/src/maths.ts").into()
|
||||
));
|
||||
|
||||
assert!(declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("user/project/src/maths.ts").into()
|
||||
));
|
||||
|
||||
assert!(declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("/home/user/project/src/maths.ts").into()
|
||||
));
|
||||
|
||||
assert!(!declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("other.ts").into()
|
||||
));
|
||||
|
||||
assert!(!declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("/home/user/project/src/other.ts").into()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,335 +1,469 @@
|
||||
mod declaration;
|
||||
mod declaration_scoring;
|
||||
mod excerpt;
|
||||
mod imports;
|
||||
mod outline;
|
||||
mod reference;
|
||||
mod syntax_index;
|
||||
pub mod text_similarity;
|
||||
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use cloud_llm_client::predict_edits_v3;
|
||||
use crate::assemble_excerpts::assemble_excerpts;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use language::BufferSnapshot;
|
||||
use text::{Point, ToOffset as _};
|
||||
use futures::{FutureExt, StreamExt as _, channel::mpsc, future};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
|
||||
use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt as _, Point, Rope, ToOffset as _};
|
||||
use project::{LocationLink, Project, ProjectPath};
|
||||
use serde::{Serialize, Serializer};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
collections::hash_map,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use util::{RangeExt as _, ResultExt};
|
||||
|
||||
pub use declaration::*;
|
||||
pub use declaration_scoring::*;
|
||||
pub use excerpt::*;
|
||||
pub use imports::*;
|
||||
pub use reference::*;
|
||||
pub use syntax_index::*;
|
||||
mod assemble_excerpts;
|
||||
#[cfg(test)]
|
||||
mod edit_prediction_context_tests;
|
||||
mod excerpt;
|
||||
#[cfg(test)]
|
||||
mod fake_definition_lsp;
|
||||
|
||||
pub use predict_edits_v3::Line;
|
||||
pub use cloud_llm_client::predict_edits_v3::Line;
|
||||
pub use excerpt::{EditPredictionExcerpt, EditPredictionExcerptOptions, EditPredictionExcerptText};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct EditPredictionContextOptions {
|
||||
pub use_imports: bool,
|
||||
pub excerpt: EditPredictionExcerptOptions,
|
||||
pub score: EditPredictionScoreOptions,
|
||||
pub max_retrieved_declarations: u8,
|
||||
pub struct RelatedExcerptStore {
|
||||
project: WeakEntity<Project>,
|
||||
related_files: Vec<RelatedFile>,
|
||||
cache: HashMap<Identifier, Arc<CacheEntry>>,
|
||||
update_tx: mpsc::UnboundedSender<(Entity<Buffer>, Anchor)>,
|
||||
}
|
||||
|
||||
pub enum RelatedExcerptStoreEvent {
|
||||
StartedRefresh,
|
||||
FinishedRefresh {
|
||||
cache_hit_count: usize,
|
||||
cache_miss_count: usize,
|
||||
mean_definition_latency: Duration,
|
||||
max_definition_latency: Duration,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
struct Identifier {
|
||||
pub name: String,
|
||||
pub range: Range<Anchor>,
|
||||
}
|
||||
|
||||
enum DefinitionTask {
|
||||
CacheHit(Arc<CacheEntry>),
|
||||
CacheMiss(Task<Result<Option<Vec<LocationLink>>>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CacheEntry {
|
||||
definitions: SmallVec<[CachedDefinition; 1]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EditPredictionContext {
|
||||
pub excerpt: EditPredictionExcerpt,
|
||||
pub excerpt_text: EditPredictionExcerptText,
|
||||
pub cursor_point: Point,
|
||||
pub declarations: Vec<ScoredDeclaration>,
|
||||
struct CachedDefinition {
|
||||
path: ProjectPath,
|
||||
buffer: Entity<Buffer>,
|
||||
anchor_range: Range<Anchor>,
|
||||
}
|
||||
|
||||
impl EditPredictionContext {
|
||||
pub fn gather_context_in_background(
|
||||
cursor_point: Point,
|
||||
buffer: BufferSnapshot,
|
||||
options: EditPredictionContextOptions,
|
||||
syntax_index: Option<Entity<SyntaxIndex>>,
|
||||
cx: &mut App,
|
||||
) -> Task<Option<Self>> {
|
||||
let parent_abs_path = project::File::from_dyn(buffer.file()).and_then(|f| {
|
||||
let mut path = f.worktree.read(cx).absolutize(&f.path);
|
||||
if path.pop() { Some(path) } else { None }
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct RelatedFile {
|
||||
#[serde(serialize_with = "serialize_project_path")]
|
||||
pub path: ProjectPath,
|
||||
#[serde(skip)]
|
||||
pub buffer: WeakEntity<Buffer>,
|
||||
pub excerpts: Vec<RelatedExcerpt>,
|
||||
pub max_row: u32,
|
||||
}
|
||||
|
||||
impl RelatedFile {
|
||||
pub fn merge_excerpts(&mut self) {
|
||||
self.excerpts.sort_unstable_by(|a, b| {
|
||||
a.point_range
|
||||
.start
|
||||
.cmp(&b.point_range.start)
|
||||
.then(b.point_range.end.cmp(&a.point_range.end))
|
||||
});
|
||||
|
||||
if let Some(syntax_index) = syntax_index {
|
||||
let index_state =
|
||||
syntax_index.read_with(cx, |index, _cx| Arc::downgrade(index.state()));
|
||||
cx.background_spawn(async move {
|
||||
let parent_abs_path = parent_abs_path.as_deref();
|
||||
let index_state = index_state.upgrade()?;
|
||||
let index_state = index_state.lock().await;
|
||||
Self::gather_context(
|
||||
cursor_point,
|
||||
&buffer,
|
||||
parent_abs_path,
|
||||
&options,
|
||||
Some(&index_state),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
cx.background_spawn(async move {
|
||||
let parent_abs_path = parent_abs_path.as_deref();
|
||||
Self::gather_context(cursor_point, &buffer, parent_abs_path, &options, None)
|
||||
})
|
||||
let mut index = 1;
|
||||
while index < self.excerpts.len() {
|
||||
if self.excerpts[index - 1]
|
||||
.point_range
|
||||
.end
|
||||
.cmp(&self.excerpts[index].point_range.start)
|
||||
.is_ge()
|
||||
{
|
||||
let removed = self.excerpts.remove(index);
|
||||
if removed
|
||||
.point_range
|
||||
.end
|
||||
.cmp(&self.excerpts[index - 1].point_range.end)
|
||||
.is_gt()
|
||||
{
|
||||
self.excerpts[index - 1].point_range.end = removed.point_range.end;
|
||||
self.excerpts[index - 1].anchor_range.end = removed.anchor_range.end;
|
||||
}
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct RelatedExcerpt {
|
||||
#[serde(skip)]
|
||||
pub anchor_range: Range<Anchor>,
|
||||
#[serde(serialize_with = "serialize_point_range")]
|
||||
pub point_range: Range<Point>,
|
||||
#[serde(serialize_with = "serialize_rope")]
|
||||
pub text: Rope,
|
||||
}
|
||||
|
||||
fn serialize_project_path<S: Serializer>(
|
||||
project_path: &ProjectPath,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
project_path.path.serialize(serializer)
|
||||
}
|
||||
|
||||
fn serialize_rope<S: Serializer>(rope: &Rope, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
rope.to_string().serialize(serializer)
|
||||
}
|
||||
|
||||
fn serialize_point_range<S: Serializer>(
|
||||
range: &Range<Point>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
[
|
||||
[range.start.row, range.start.column],
|
||||
[range.end.row, range.end.column],
|
||||
]
|
||||
.serialize(serializer)
|
||||
}
|
||||
|
||||
const DEBOUNCE_DURATION: Duration = Duration::from_millis(100);
|
||||
|
||||
impl EventEmitter<RelatedExcerptStoreEvent> for RelatedExcerptStore {}
|
||||
|
||||
impl RelatedExcerptStore {
|
||||
pub fn new(project: &Entity<Project>, cx: &mut Context<Self>) -> Self {
|
||||
let (update_tx, mut update_rx) = mpsc::unbounded::<(Entity<Buffer>, Anchor)>();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let executor = cx.background_executor().clone();
|
||||
while let Some((mut buffer, mut position)) = update_rx.next().await {
|
||||
let mut timer = executor.timer(DEBOUNCE_DURATION).fuse();
|
||||
loop {
|
||||
futures::select_biased! {
|
||||
next = update_rx.next() => {
|
||||
if let Some((new_buffer, new_position)) = next {
|
||||
buffer = new_buffer;
|
||||
position = new_position;
|
||||
timer = executor.timer(DEBOUNCE_DURATION).fuse();
|
||||
} else {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
}
|
||||
_ = timer => break,
|
||||
}
|
||||
}
|
||||
|
||||
Self::fetch_excerpts(this.clone(), buffer, position, cx).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
RelatedExcerptStore {
|
||||
project: project.downgrade(),
|
||||
update_tx,
|
||||
related_files: Vec::new(),
|
||||
cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gather_context(
|
||||
cursor_point: Point,
|
||||
buffer: &BufferSnapshot,
|
||||
parent_abs_path: Option<&Path>,
|
||||
options: &EditPredictionContextOptions,
|
||||
index_state: Option<&SyntaxIndexState>,
|
||||
) -> Option<Self> {
|
||||
let imports = if options.use_imports {
|
||||
Imports::gather(&buffer, parent_abs_path)
|
||||
} else {
|
||||
Imports::default()
|
||||
};
|
||||
Self::gather_context_with_references_fn(
|
||||
cursor_point,
|
||||
buffer,
|
||||
&imports,
|
||||
options,
|
||||
index_state,
|
||||
references_in_excerpt,
|
||||
)
|
||||
pub fn refresh(&mut self, buffer: Entity<Buffer>, position: Anchor, _: &mut Context<Self>) {
|
||||
self.update_tx.unbounded_send((buffer, position)).ok();
|
||||
}
|
||||
|
||||
pub fn gather_context_with_references_fn(
|
||||
cursor_point: Point,
|
||||
buffer: &BufferSnapshot,
|
||||
imports: &Imports,
|
||||
options: &EditPredictionContextOptions,
|
||||
index_state: Option<&SyntaxIndexState>,
|
||||
get_references: impl FnOnce(
|
||||
&EditPredictionExcerpt,
|
||||
&EditPredictionExcerptText,
|
||||
&BufferSnapshot,
|
||||
) -> HashMap<Identifier, Vec<Reference>>,
|
||||
) -> Option<Self> {
|
||||
let excerpt = EditPredictionExcerpt::select_from_buffer(
|
||||
cursor_point,
|
||||
buffer,
|
||||
&options.excerpt,
|
||||
index_state,
|
||||
)?;
|
||||
let excerpt_text = excerpt.text(buffer);
|
||||
pub fn related_files(&self) -> &[RelatedFile] {
|
||||
&self.related_files
|
||||
}
|
||||
|
||||
let declarations = if options.max_retrieved_declarations > 0
|
||||
&& let Some(index_state) = index_state
|
||||
{
|
||||
let excerpt_occurrences =
|
||||
text_similarity::Occurrences::within_string(&excerpt_text.body);
|
||||
|
||||
let adjacent_start = Point::new(cursor_point.row.saturating_sub(2), 0);
|
||||
let adjacent_end = Point::new(cursor_point.row + 1, 0);
|
||||
let adjacent_occurrences = text_similarity::Occurrences::within_string(
|
||||
&buffer
|
||||
.text_for_range(adjacent_start..adjacent_end)
|
||||
.collect::<String>(),
|
||||
);
|
||||
|
||||
let cursor_offset_in_file = cursor_point.to_offset(buffer);
|
||||
|
||||
let references = get_references(&excerpt, &excerpt_text, buffer);
|
||||
|
||||
let mut declarations = scored_declarations(
|
||||
&options.score,
|
||||
&index_state,
|
||||
&excerpt,
|
||||
&excerpt_occurrences,
|
||||
&adjacent_occurrences,
|
||||
&imports,
|
||||
references,
|
||||
cursor_offset_in_file,
|
||||
buffer,
|
||||
);
|
||||
// TODO [zeta2] if we need this when we ship, we should probably do it in a smarter way
|
||||
declarations.truncate(options.max_retrieved_declarations as usize);
|
||||
declarations
|
||||
} else {
|
||||
vec![]
|
||||
async fn fetch_excerpts(
|
||||
this: WeakEntity<Self>,
|
||||
buffer: Entity<Buffer>,
|
||||
position: Anchor,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let (project, snapshot) = this.read_with(cx, |this, cx| {
|
||||
(this.project.upgrade(), buffer.read(cx).snapshot())
|
||||
})?;
|
||||
let Some(project) = project else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
excerpt,
|
||||
excerpt_text,
|
||||
cursor_point,
|
||||
declarations,
|
||||
})
|
||||
let file = snapshot.file().cloned();
|
||||
if let Some(file) = &file {
|
||||
log::debug!("retrieving_context buffer:{}", file.path().as_unix_str());
|
||||
}
|
||||
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(RelatedExcerptStoreEvent::StartedRefresh);
|
||||
})?;
|
||||
|
||||
let identifiers = cx
|
||||
.background_spawn(async move { identifiers_for_position(&snapshot, position) })
|
||||
.await;
|
||||
|
||||
let async_cx = cx.clone();
|
||||
let start_time = Instant::now();
|
||||
let futures = this.update(cx, |this, cx| {
|
||||
identifiers
|
||||
.into_iter()
|
||||
.filter_map(|identifier| {
|
||||
let task = if let Some(entry) = this.cache.get(&identifier) {
|
||||
DefinitionTask::CacheHit(entry.clone())
|
||||
} else {
|
||||
DefinitionTask::CacheMiss(
|
||||
this.project
|
||||
.update(cx, |project, cx| {
|
||||
project.definitions(&buffer, identifier.range.start, cx)
|
||||
})
|
||||
.ok()?,
|
||||
)
|
||||
};
|
||||
|
||||
let cx = async_cx.clone();
|
||||
let project = project.clone();
|
||||
Some(async move {
|
||||
match task {
|
||||
DefinitionTask::CacheHit(cache_entry) => {
|
||||
Some((identifier, cache_entry, None))
|
||||
}
|
||||
DefinitionTask::CacheMiss(task) => {
|
||||
let locations = task.await.log_err()??;
|
||||
let duration = start_time.elapsed();
|
||||
cx.update(|cx| {
|
||||
(
|
||||
identifier,
|
||||
Arc::new(CacheEntry {
|
||||
definitions: locations
|
||||
.into_iter()
|
||||
.filter_map(|location| {
|
||||
process_definition(location, &project, cx)
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
Some(duration),
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
let mut cache_hit_count = 0;
|
||||
let mut cache_miss_count = 0;
|
||||
let mut mean_definition_latency = Duration::ZERO;
|
||||
let mut max_definition_latency = Duration::ZERO;
|
||||
let mut new_cache = HashMap::default();
|
||||
new_cache.reserve(futures.len());
|
||||
for (identifier, entry, duration) in future::join_all(futures).await.into_iter().flatten() {
|
||||
new_cache.insert(identifier, entry);
|
||||
if let Some(duration) = duration {
|
||||
cache_miss_count += 1;
|
||||
mean_definition_latency += duration;
|
||||
max_definition_latency = max_definition_latency.max(duration);
|
||||
} else {
|
||||
cache_hit_count += 1;
|
||||
}
|
||||
}
|
||||
mean_definition_latency /= cache_miss_count.max(1) as u32;
|
||||
|
||||
let (new_cache, related_files) = rebuild_related_files(new_cache, cx).await?;
|
||||
|
||||
if let Some(file) = &file {
|
||||
log::debug!(
|
||||
"finished retrieving context buffer:{}, latency:{:?}",
|
||||
file.path().as_unix_str(),
|
||||
start_time.elapsed()
|
||||
);
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.cache = new_cache;
|
||||
this.related_files = related_files;
|
||||
cx.emit(RelatedExcerptStoreEvent::FinishedRefresh {
|
||||
cache_hit_count,
|
||||
cache_miss_count,
|
||||
mean_definition_latency,
|
||||
max_definition_latency,
|
||||
});
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{Language, LanguageConfig, LanguageId, LanguageMatcher, tree_sitter_rust};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
use crate::{EditPredictionExcerptOptions, SyntaxIndex};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_call_site(cx: &mut TestAppContext) {
|
||||
let (project, index, _rust_lang_id) = init_test(cx).await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
let project_path = project.find_project_path("c.rs", cx).unwrap();
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// first process_data call site
|
||||
let cursor_point = language::Point::new(8, 21);
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let context = cx
|
||||
.update(|cx| {
|
||||
EditPredictionContext::gather_context_in_background(
|
||||
cursor_point,
|
||||
buffer_snapshot,
|
||||
EditPredictionContextOptions {
|
||||
use_imports: true,
|
||||
excerpt: EditPredictionExcerptOptions {
|
||||
max_bytes: 60,
|
||||
min_bytes: 10,
|
||||
target_before_cursor_over_total_bytes: 0.5,
|
||||
},
|
||||
score: EditPredictionScoreOptions {
|
||||
omit_excerpt_overlaps: true,
|
||||
},
|
||||
max_retrieved_declarations: u8::MAX,
|
||||
},
|
||||
Some(index.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut snippet_identifiers = context
|
||||
.declarations
|
||||
.iter()
|
||||
.map(|snippet| snippet.identifier.name.as_ref())
|
||||
.collect::<Vec<_>>();
|
||||
snippet_identifiers.sort();
|
||||
assert_eq!(snippet_identifiers, vec!["main", "process_data"]);
|
||||
drop(buffer);
|
||||
async fn rebuild_related_files(
|
||||
new_entries: HashMap<Identifier, Arc<CacheEntry>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<(HashMap<Identifier, Arc<CacheEntry>>, Vec<RelatedFile>)> {
|
||||
let mut snapshots = HashMap::default();
|
||||
for entry in new_entries.values() {
|
||||
for definition in &entry.definitions {
|
||||
if let hash_map::Entry::Vacant(e) = snapshots.entry(definition.buffer.entity_id()) {
|
||||
definition
|
||||
.buffer
|
||||
.read_with(cx, |buffer, _| buffer.parsing_idle())?
|
||||
.await;
|
||||
e.insert(
|
||||
definition
|
||||
.buffer
|
||||
.read_with(cx, |buffer, _| buffer.snapshot())?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn init_test(
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Project>, Entity<SyntaxIndex>, LanguageId) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Ok(cx
|
||||
.background_spawn(async move {
|
||||
let mut files = Vec::<RelatedFile>::new();
|
||||
let mut ranges_by_buffer = HashMap::<_, Vec<Range<Point>>>::default();
|
||||
let mut paths_by_buffer = HashMap::default();
|
||||
for entry in new_entries.values() {
|
||||
for definition in &entry.definitions {
|
||||
let Some(snapshot) = snapshots.get(&definition.buffer.entity_id()) else {
|
||||
continue;
|
||||
};
|
||||
paths_by_buffer.insert(definition.buffer.entity_id(), definition.path.clone());
|
||||
ranges_by_buffer
|
||||
.entry(definition.buffer.clone())
|
||||
.or_default()
|
||||
.push(definition.anchor_range.to_point(snapshot));
|
||||
}
|
||||
}
|
||||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
let Some(snapshot) = snapshots.get(&buffer.entity_id()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(project_path) = paths_by_buffer.get(&buffer.entity_id()) else {
|
||||
continue;
|
||||
};
|
||||
let excerpts = assemble_excerpts(snapshot, ranges);
|
||||
files.push(RelatedFile {
|
||||
path: project_path.clone(),
|
||||
buffer: buffer.downgrade(),
|
||||
excerpts,
|
||||
max_row: snapshot.max_point().row,
|
||||
});
|
||||
}
|
||||
|
||||
files.sort_by_key(|file| file.path.clone());
|
||||
(new_entries, files)
|
||||
})
|
||||
.await)
|
||||
}
|
||||
|
||||
fn process_definition(
|
||||
location: LocationLink,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Option<CachedDefinition> {
|
||||
let buffer = location.target.buffer.read(cx);
|
||||
let anchor_range = location.target.range;
|
||||
let file = buffer.file()?;
|
||||
let worktree = project.read(cx).worktree_for_id(file.worktree_id(cx), cx)?;
|
||||
if worktree.read(cx).is_single_file() {
|
||||
return None;
|
||||
}
|
||||
Some(CachedDefinition {
|
||||
path: ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path().clone(),
|
||||
},
|
||||
buffer: location.target.buffer,
|
||||
anchor_range,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets all of the identifiers that are present in the given line, and its containing
|
||||
/// outline items.
|
||||
fn identifiers_for_position(buffer: &BufferSnapshot, position: Anchor) -> Vec<Identifier> {
|
||||
let offset = position.to_offset(buffer);
|
||||
let point = buffer.offset_to_point(offset);
|
||||
|
||||
let line_range = Point::new(point.row, 0)..Point::new(point.row + 1, 0).min(buffer.max_point());
|
||||
let mut ranges = vec![line_range.to_offset(&buffer)];
|
||||
|
||||
// Include the range of the outline item itself, but not its body.
|
||||
let outline_items = buffer.outline_items_as_offsets_containing(offset..offset, false, None);
|
||||
for item in outline_items {
|
||||
if let Some(body_range) = item.body_range(&buffer) {
|
||||
ranges.push(item.range.start..body_range.start.to_offset(&buffer));
|
||||
} else {
|
||||
ranges.push(item.range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
ranges.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
|
||||
ranges.dedup_by(|a, b| {
|
||||
if a.start <= b.end {
|
||||
b.start = b.start.min(a.start);
|
||||
b.end = b.end.max(a.end);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
let mut identifiers = Vec::new();
|
||||
let outer_range =
|
||||
ranges.first().map_or(0, |r| r.start)..ranges.last().map_or(buffer.len(), |r| r.end);
|
||||
|
||||
let mut captures = buffer
|
||||
.syntax
|
||||
.captures(outer_range.clone(), &buffer.text, |grammar| {
|
||||
grammar
|
||||
.highlights_config
|
||||
.as_ref()
|
||||
.map(|config| &config.query)
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a.rs": indoc! {r#"
|
||||
fn main() {
|
||||
let x = 1;
|
||||
let y = 2;
|
||||
let z = add(x, y);
|
||||
println!("Result: {}", z);
|
||||
}
|
||||
for range in ranges {
|
||||
captures.set_byte_range(range.start..outer_range.end);
|
||||
|
||||
fn add(a: i32, b: i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
"#},
|
||||
"b.rs": indoc! {"
|
||||
pub struct Config {
|
||||
pub name: String,
|
||||
pub value: i32,
|
||||
}
|
||||
let mut last_range = None;
|
||||
while let Some(capture) = captures.peek() {
|
||||
let node_range = capture.node.byte_range();
|
||||
if node_range.start > range.end {
|
||||
break;
|
||||
}
|
||||
let config = captures.grammars()[capture.grammar_index]
|
||||
.highlights_config
|
||||
.as_ref();
|
||||
|
||||
impl Config {
|
||||
pub fn new(name: String, value: i32) -> Self {
|
||||
Config { name, value }
|
||||
}
|
||||
}
|
||||
"},
|
||||
"c.rs": indoc! {r#"
|
||||
use std::collections::HashMap;
|
||||
if let Some(config) = config
|
||||
&& config.identifier_capture_indices.contains(&capture.index)
|
||||
&& range.contains_inclusive(&node_range)
|
||||
&& Some(&node_range) != last_range.as_ref()
|
||||
{
|
||||
let name = buffer.text_for_range(node_range.clone()).collect();
|
||||
identifiers.push(Identifier {
|
||||
range: buffer.anchor_after(node_range.start)
|
||||
..buffer.anchor_before(node_range.end),
|
||||
name,
|
||||
});
|
||||
last_range = Some(node_range);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let data: Vec<i32> = args[1..]
|
||||
.iter()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let result = process_data(data);
|
||||
println!("{:?}", result);
|
||||
}
|
||||
|
||||
fn process_data(data: Vec<i32>) -> HashMap<i32, usize> {
|
||||
let mut counts = HashMap::new();
|
||||
for value in data {
|
||||
*counts.entry(value).or_insert(0) += 1;
|
||||
}
|
||||
counts
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_process_data() {
|
||||
let data = vec![1, 2, 2, 3];
|
||||
let result = process_data(data);
|
||||
assert_eq!(result.get(&2), Some(&2));
|
||||
}
|
||||
}
|
||||
"#}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let lang = rust_lang();
|
||||
let lang_id = lang.id();
|
||||
language_registry.add(Arc::new(lang));
|
||||
|
||||
let file_indexing_parallelism = 2;
|
||||
let index = cx.new(|cx| SyntaxIndex::new(&project, file_indexing_parallelism, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
(project, index, lang_id)
|
||||
captures.advance();
|
||||
}
|
||||
}
|
||||
|
||||
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_highlights_query(include_str!("../../languages/src/rust/highlights.scm"))
|
||||
.unwrap()
|
||||
.with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
|
||||
.unwrap()
|
||||
}
|
||||
identifiers
|
||||
}
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
use super::*;
|
||||
use futures::channel::mpsc::UnboundedReceiver;
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
use language::{Language, LanguageConfig, LanguageMatcher, Point, ToPoint as _, tree_sitter_rust};
|
||||
use lsp::FakeLanguageServer;
|
||||
use project::{FakeFs, LocationLink, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_prediction_context(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), test_project_1()).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let mut servers = setup_fake_lsp(&project, cx);
|
||||
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _server = servers.next().await.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let related_excerpt_store = cx.new(|cx| RelatedExcerptStore::new(&project, cx));
|
||||
related_excerpt_store.update(cx, |store, cx| {
|
||||
let position = {
|
||||
let buffer = buffer.read(cx);
|
||||
let offset = buffer.text().find("todo").unwrap();
|
||||
buffer.anchor_before(offset)
|
||||
};
|
||||
|
||||
store.refresh(buffer.clone(), position, cx);
|
||||
});
|
||||
|
||||
cx.executor().advance_clock(DEBOUNCE_DURATION);
|
||||
related_excerpt_store.update(cx, |store, _| {
|
||||
let excerpts = store.related_files();
|
||||
assert_related_files(
|
||||
&excerpts,
|
||||
&[
|
||||
(
|
||||
"src/company.rs",
|
||||
&[indoc! {"
|
||||
pub struct Company {
|
||||
owner: Arc<Person>,
|
||||
address: Address,
|
||||
}"}],
|
||||
),
|
||||
(
|
||||
"src/main.rs",
|
||||
&[
|
||||
indoc! {"
|
||||
pub struct Session {
|
||||
company: Arc<Company>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn set_company(&mut self, company: Arc<Company>) {"},
|
||||
indoc! {"
|
||||
}
|
||||
}"},
|
||||
],
|
||||
),
|
||||
(
|
||||
"src/person.rs",
|
||||
&[
|
||||
indoc! {"
|
||||
impl Person {
|
||||
pub fn get_first_name(&self) -> &str {
|
||||
&self.first_name
|
||||
}"},
|
||||
"}",
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fake_definition_lsp(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), test_project_1()).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let mut servers = setup_fake_lsp(&project, cx);
|
||||
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _server = servers.next().await.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
|
||||
|
||||
let definitions = project
|
||||
.update(cx, |project, cx| {
|
||||
let offset = buffer_text.find("Address {").unwrap();
|
||||
project.definitions(&buffer, offset, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_definitions(&definitions, &["pub struct Address {"], cx);
|
||||
|
||||
let definitions = project
|
||||
.update(cx, |project, cx| {
|
||||
let offset = buffer_text.find("State::CA").unwrap();
|
||||
project.definitions(&buffer, offset, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_definitions(&definitions, &["pub enum State {"], cx);
|
||||
|
||||
let definitions = project
|
||||
.update(cx, |project, cx| {
|
||||
let offset = buffer_text.find("to_string()").unwrap();
|
||||
project.definitions(&buffer, offset, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_definitions(&definitions, &["pub fn to_string(&self) -> String {"], cx);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
let settings_store = cx.update(|cx| SettingsStore::test(cx));
|
||||
cx.set_global(settings_store);
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
|
||||
fn setup_fake_lsp(
|
||||
project: &Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> UnboundedReceiver<FakeLanguageServer> {
|
||||
let (language_registry, fs) = project.read_with(cx, |project, _| {
|
||||
(project.languages().clone(), project.fs().clone())
|
||||
});
|
||||
let language = rust_lang();
|
||||
language_registry.add(language.clone());
|
||||
fake_definition_lsp::register_fake_definition_server(&language_registry, language, fs)
|
||||
}
|
||||
|
||||
fn test_project_1() -> serde_json::Value {
|
||||
let person_rs = indoc! {r#"
|
||||
pub struct Person {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
pub fn get_first_name(&self) -> &str {
|
||||
&self.first_name
|
||||
}
|
||||
|
||||
pub fn get_last_name(&self) -> &str {
|
||||
&self.last_name
|
||||
}
|
||||
|
||||
pub fn get_email(&self) -> &str {
|
||||
&self.email
|
||||
}
|
||||
|
||||
pub fn get_age(&self) -> u32 {
|
||||
self.age
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let address_rs = indoc! {r#"
|
||||
pub struct Address {
|
||||
street: String,
|
||||
city: String,
|
||||
state: State,
|
||||
zip: u32,
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
CA,
|
||||
OR,
|
||||
WA,
|
||||
TX,
|
||||
// ...
|
||||
}
|
||||
|
||||
impl Address {
|
||||
pub fn get_street(&self) -> &str {
|
||||
&self.street
|
||||
}
|
||||
|
||||
pub fn get_city(&self) -> &str {
|
||||
&self.city
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> State {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn get_zip(&self) -> u32 {
|
||||
self.zip
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let company_rs = indoc! {r#"
|
||||
use super::person::Person;
|
||||
use super::address::Address;
|
||||
|
||||
pub struct Company {
|
||||
owner: Arc<Person>,
|
||||
address: Address,
|
||||
}
|
||||
|
||||
impl Company {
|
||||
pub fn get_owner(&self) -> &Person {
|
||||
&self.owner
|
||||
}
|
||||
|
||||
pub fn get_address(&self) -> &Address {
|
||||
&self.address
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
format!("{} ({})", self.owner.first_name, self.address.city)
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let main_rs = indoc! {r#"
|
||||
use std::sync::Arc;
|
||||
use super::person::Person;
|
||||
use super::address::Address;
|
||||
use super::company::Company;
|
||||
|
||||
pub struct Session {
|
||||
company: Arc<Company>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn set_company(&mut self, company: Arc<Company>) {
|
||||
self.company = company;
|
||||
if company.owner != self.company.owner {
|
||||
log("new owner", company.owner.get_first_name()); todo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let company = Company {
|
||||
owner: Arc::new(Person {
|
||||
first_name: "John".to_string(),
|
||||
last_name: "Doe".to_string(),
|
||||
email: "john@example.com".to_string(),
|
||||
age: 30,
|
||||
}),
|
||||
address: Address {
|
||||
street: "123 Main St".to_string(),
|
||||
city: "Anytown".to_string(),
|
||||
state: State::CA,
|
||||
zip: 12345,
|
||||
},
|
||||
};
|
||||
|
||||
println!("Company: {}", company.to_string());
|
||||
}
|
||||
"#};
|
||||
|
||||
json!({
|
||||
"src": {
|
||||
"person.rs": person_rs,
|
||||
"address.rs": address_rs,
|
||||
"company.rs": company_rs,
|
||||
"main.rs": main_rs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_related_files(actual_files: &[RelatedFile], expected_files: &[(&str, &[&str])]) {
|
||||
let actual_files = actual_files
|
||||
.iter()
|
||||
.map(|file| {
|
||||
let excerpts = file
|
||||
.excerpts
|
||||
.iter()
|
||||
.map(|excerpt| excerpt.text.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
(file.path.path.as_unix_str(), excerpts)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let expected_excerpts = expected_files
|
||||
.iter()
|
||||
.map(|(path, texts)| {
|
||||
(
|
||||
*path,
|
||||
texts
|
||||
.iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
pretty_assertions::assert_eq!(actual_files, expected_excerpts)
|
||||
}
|
||||
|
||||
fn assert_definitions(definitions: &[LocationLink], first_lines: &[&str], cx: &mut TestAppContext) {
|
||||
let actual_first_lines = definitions
|
||||
.iter()
|
||||
.map(|definition| {
|
||||
definition.target.buffer.read_with(cx, |buffer, _| {
|
||||
let mut start = definition.target.range.start.to_point(&buffer);
|
||||
start.column = 0;
|
||||
let end = Point::new(start.row, buffer.line_len(start.row));
|
||||
buffer
|
||||
.text_for_range(start..end)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
})
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
assert_eq!(actual_first_lines, first_lines);
|
||||
}
|
||||
|
||||
pub(crate) fn rust_lang() -> Arc<Language> {
|
||||
Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
first_line_pattern: None,
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_highlights_query(include_str!("../../languages/src/rust/highlights.scm"))
|
||||
.unwrap()
|
||||
.with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
use language::{BufferSnapshot, LanguageId};
|
||||
use cloud_llm_client::predict_edits_v3::Line;
|
||||
use language::{BufferSnapshot, LanguageId, Point, ToOffset as _, ToPoint as _};
|
||||
use std::ops::Range;
|
||||
use text::{Point, ToOffset as _, ToPoint as _};
|
||||
use tree_sitter::{Node, TreeCursor};
|
||||
use util::RangeExt;
|
||||
|
||||
use crate::{BufferDeclaration, Line, declaration::DeclarationId, syntax_index::SyntaxIndexState};
|
||||
|
||||
// TODO:
|
||||
//
|
||||
// - Test parent signatures
|
||||
@@ -31,19 +29,16 @@ pub struct EditPredictionExcerptOptions {
|
||||
pub target_before_cursor_over_total_bytes: f32,
|
||||
}
|
||||
|
||||
// TODO: consider merging these
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditPredictionExcerpt {
|
||||
pub range: Range<usize>,
|
||||
pub line_range: Range<Line>,
|
||||
pub parent_declarations: Vec<(DeclarationId, Range<usize>)>,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditPredictionExcerptText {
|
||||
pub body: String,
|
||||
pub parent_signatures: Vec<String>,
|
||||
pub language_id: Option<LanguageId>,
|
||||
}
|
||||
|
||||
@@ -52,17 +47,8 @@ impl EditPredictionExcerpt {
|
||||
let body = buffer
|
||||
.text_for_range(self.range.clone())
|
||||
.collect::<String>();
|
||||
let parent_signatures = self
|
||||
.parent_declarations
|
||||
.iter()
|
||||
.map(|(_, range)| buffer.text_for_range(range.clone()).collect::<String>())
|
||||
.collect();
|
||||
let language_id = buffer.language().map(|l| l.id());
|
||||
EditPredictionExcerptText {
|
||||
body,
|
||||
parent_signatures,
|
||||
language_id,
|
||||
}
|
||||
EditPredictionExcerptText { body, language_id }
|
||||
}
|
||||
|
||||
/// Selects an excerpt around a buffer position, attempting to choose logical boundaries based
|
||||
@@ -79,7 +65,6 @@ impl EditPredictionExcerpt {
|
||||
query_point: Point,
|
||||
buffer: &BufferSnapshot,
|
||||
options: &EditPredictionExcerptOptions,
|
||||
syntax_index: Option<&SyntaxIndexState>,
|
||||
) -> Option<Self> {
|
||||
if buffer.len() <= options.max_bytes {
|
||||
log::debug!(
|
||||
@@ -89,11 +74,7 @@ impl EditPredictionExcerpt {
|
||||
);
|
||||
let offset_range = 0..buffer.len();
|
||||
let line_range = Line(0)..Line(buffer.max_point().row);
|
||||
return Some(EditPredictionExcerpt::new(
|
||||
offset_range,
|
||||
line_range,
|
||||
Vec::new(),
|
||||
));
|
||||
return Some(EditPredictionExcerpt::new(offset_range, line_range));
|
||||
}
|
||||
|
||||
let query_offset = query_point.to_offset(buffer);
|
||||
@@ -104,19 +85,10 @@ impl EditPredictionExcerpt {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parent_declarations = if let Some(syntax_index) = syntax_index {
|
||||
syntax_index
|
||||
.buffer_declarations_containing_range(buffer.remote_id(), query_range.clone())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let excerpt_selector = ExcerptSelector {
|
||||
query_offset,
|
||||
query_range,
|
||||
query_line_range: Line(query_line_range.start)..Line(query_line_range.end),
|
||||
parent_declarations: &parent_declarations,
|
||||
buffer,
|
||||
options,
|
||||
};
|
||||
@@ -139,20 +111,10 @@ impl EditPredictionExcerpt {
|
||||
excerpt_selector.select_lines()
|
||||
}
|
||||
|
||||
fn new(
|
||||
range: Range<usize>,
|
||||
line_range: Range<Line>,
|
||||
parent_declarations: Vec<(DeclarationId, Range<usize>)>,
|
||||
) -> Self {
|
||||
let size = range.len()
|
||||
+ parent_declarations
|
||||
.iter()
|
||||
.map(|(_, range)| range.len())
|
||||
.sum::<usize>();
|
||||
fn new(range: Range<usize>, line_range: Range<Line>) -> Self {
|
||||
Self {
|
||||
size: range.len(),
|
||||
range,
|
||||
parent_declarations,
|
||||
size,
|
||||
line_range,
|
||||
}
|
||||
}
|
||||
@@ -162,14 +124,7 @@ impl EditPredictionExcerpt {
|
||||
// this is an issue because parent_signature_ranges may be incorrect
|
||||
log::error!("bug: with_expanded_range called with disjoint range");
|
||||
}
|
||||
let mut parent_declarations = Vec::with_capacity(self.parent_declarations.len());
|
||||
for (declaration_id, range) in &self.parent_declarations {
|
||||
if !range.contains_inclusive(&new_range) {
|
||||
break;
|
||||
}
|
||||
parent_declarations.push((*declaration_id, range.clone()));
|
||||
}
|
||||
Self::new(new_range, new_line_range, parent_declarations)
|
||||
Self::new(new_range, new_line_range)
|
||||
}
|
||||
|
||||
fn parent_signatures_size(&self) -> usize {
|
||||
@@ -181,7 +136,6 @@ struct ExcerptSelector<'a> {
|
||||
query_offset: usize,
|
||||
query_range: Range<usize>,
|
||||
query_line_range: Range<Line>,
|
||||
parent_declarations: &'a [(DeclarationId, &'a BufferDeclaration)],
|
||||
buffer: &'a BufferSnapshot,
|
||||
options: &'a EditPredictionExcerptOptions,
|
||||
}
|
||||
@@ -409,13 +363,7 @@ impl<'a> ExcerptSelector<'a> {
|
||||
}
|
||||
|
||||
fn make_excerpt(&self, range: Range<usize>, line_range: Range<Line>) -> EditPredictionExcerpt {
|
||||
let parent_declarations = self
|
||||
.parent_declarations
|
||||
.iter()
|
||||
.filter(|(_, declaration)| declaration.item_range.contains_inclusive(&range))
|
||||
.map(|(id, declaration)| (*id, declaration.signature_range.clone()))
|
||||
.collect();
|
||||
EditPredictionExcerpt::new(range, line_range, parent_declarations)
|
||||
EditPredictionExcerpt::new(range, line_range)
|
||||
}
|
||||
|
||||
/// Returns `true` if the `forward` excerpt is a better choice than the `backward` excerpt.
|
||||
@@ -506,9 +454,8 @@ mod tests {
|
||||
let buffer = create_buffer(&text, cx);
|
||||
let cursor_point = cursor.to_point(&buffer);
|
||||
|
||||
let excerpt =
|
||||
EditPredictionExcerpt::select_from_buffer(cursor_point, &buffer, &options, None)
|
||||
.expect("Should select an excerpt");
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
use collections::HashMap;
|
||||
use futures::channel::mpsc::UnboundedReceiver;
|
||||
use language::{Language, LanguageRegistry};
|
||||
use lsp::{
|
||||
FakeLanguageServer, LanguageServerBinary, TextDocumentSyncCapability, TextDocumentSyncKind, Uri,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::Fs;
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc};
|
||||
use tree_sitter::{Parser, QueryCursor, StreamingIterator, Tree};
|
||||
|
||||
/// Registers a fake language server that implements go-to-definition using tree-sitter,
|
||||
/// making the assumption that all names are unique, and all variables' types are
|
||||
/// explicitly declared.
|
||||
pub fn register_fake_definition_server(
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Arc<Language>,
|
||||
fs: Arc<dyn Fs>,
|
||||
) -> UnboundedReceiver<FakeLanguageServer> {
|
||||
let index = Arc::new(Mutex::new(DefinitionIndex::new(language.clone())));
|
||||
|
||||
language_registry.register_fake_lsp(
|
||||
language.name(),
|
||||
language::FakeLspAdapter {
|
||||
name: "fake-definition-lsp",
|
||||
initialization_options: None,
|
||||
prettier_plugins: Vec::new(),
|
||||
disk_based_diagnostics_progress_token: None,
|
||||
disk_based_diagnostics_sources: Vec::new(),
|
||||
language_server_binary: LanguageServerBinary {
|
||||
path: PathBuf::from("fake-definition-lsp"),
|
||||
arguments: Vec::new(),
|
||||
env: None,
|
||||
},
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
definition_provider: Some(lsp::OneOf::Left(true)),
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
||||
TextDocumentSyncKind::FULL,
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
label_for_completion: None,
|
||||
initializer: Some(Box::new({
|
||||
move |server| {
|
||||
server.handle_notification::<lsp::notification::DidOpenTextDocument, _>({
|
||||
let index = index.clone();
|
||||
move |params, _cx| {
|
||||
index
|
||||
.lock()
|
||||
.open_buffer(params.text_document.uri, ¶ms.text_document.text);
|
||||
}
|
||||
});
|
||||
|
||||
server.handle_notification::<lsp::notification::DidCloseTextDocument, _>({
|
||||
let index = index.clone();
|
||||
let fs = fs.clone();
|
||||
move |params, cx| {
|
||||
let uri = params.text_document.uri;
|
||||
let path = uri.to_file_path().ok();
|
||||
index.lock().mark_buffer_closed(&uri);
|
||||
|
||||
if let Some(path) = path {
|
||||
let index = index.clone();
|
||||
let fs = fs.clone();
|
||||
cx.spawn(async move |_cx| {
|
||||
if let Ok(content) = fs.load(&path).await {
|
||||
index.lock().index_file(uri, &content);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
|
||||
let index = index.clone();
|
||||
let fs = fs.clone();
|
||||
move |params, cx| {
|
||||
let index = index.clone();
|
||||
let fs = fs.clone();
|
||||
cx.spawn(async move |_cx| {
|
||||
for event in params.changes {
|
||||
if index.lock().is_buffer_open(&event.uri) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match event.typ {
|
||||
lsp::FileChangeType::DELETED => {
|
||||
index.lock().remove_definitions_for_file(&event.uri);
|
||||
}
|
||||
lsp::FileChangeType::CREATED
|
||||
| lsp::FileChangeType::CHANGED => {
|
||||
if let Some(path) = event.uri.to_file_path().ok() {
|
||||
if let Ok(content) = fs.load(&path).await {
|
||||
index.lock().index_file(event.uri, &content);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
|
||||
server.handle_notification::<lsp::notification::DidChangeTextDocument, _>({
|
||||
let index = index.clone();
|
||||
move |params, _cx| {
|
||||
if let Some(change) = params.content_changes.into_iter().last() {
|
||||
index
|
||||
.lock()
|
||||
.index_file(params.text_document.uri, &change.text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.handle_notification::<lsp::notification::DidChangeWorkspaceFolders, _>(
|
||||
{
|
||||
let index = index.clone();
|
||||
let fs = fs.clone();
|
||||
move |params, cx| {
|
||||
let index = index.clone();
|
||||
let fs = fs.clone();
|
||||
let files = fs.as_fake().files();
|
||||
cx.spawn(async move |_cx| {
|
||||
for folder in params.event.added {
|
||||
let Ok(path) = folder.uri.to_file_path() else {
|
||||
continue;
|
||||
};
|
||||
for file in &files {
|
||||
if let Some(uri) = Uri::from_file_path(&file).ok()
|
||||
&& file.starts_with(&path)
|
||||
&& let Ok(content) = fs.load(&file).await
|
||||
{
|
||||
index.lock().index_file(uri, &content);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.set_request_handler::<lsp::request::GotoDefinition, _, _>({
|
||||
let index = index.clone();
|
||||
move |params, _cx| {
|
||||
let result = index.lock().get_definitions(
|
||||
params.text_document_position_params.text_document.uri,
|
||||
params.text_document_position_params.position,
|
||||
);
|
||||
async move { Ok(result) }
|
||||
}
|
||||
});
|
||||
}
|
||||
})),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
struct DefinitionIndex {
|
||||
language: Arc<Language>,
|
||||
definitions: HashMap<String, Vec<lsp::Location>>,
|
||||
files: HashMap<Uri, FileEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FileEntry {
|
||||
contents: String,
|
||||
is_open_in_buffer: bool,
|
||||
}
|
||||
|
||||
impl DefinitionIndex {
|
||||
fn new(language: Arc<Language>) -> Self {
|
||||
Self {
|
||||
language,
|
||||
definitions: HashMap::default(),
|
||||
files: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_definitions_for_file(&mut self, uri: &Uri) {
|
||||
self.definitions.retain(|_, locations| {
|
||||
locations.retain(|loc| &loc.uri != uri);
|
||||
!locations.is_empty()
|
||||
});
|
||||
self.files.remove(uri);
|
||||
}
|
||||
|
||||
fn open_buffer(&mut self, uri: Uri, content: &str) {
|
||||
self.index_file_inner(uri, content, true);
|
||||
}
|
||||
|
||||
fn mark_buffer_closed(&mut self, uri: &Uri) {
|
||||
if let Some(entry) = self.files.get_mut(uri) {
|
||||
entry.is_open_in_buffer = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_buffer_open(&self, uri: &Uri) -> bool {
|
||||
self.files
|
||||
.get(uri)
|
||||
.map(|entry| entry.is_open_in_buffer)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn index_file(&mut self, uri: Uri, content: &str) {
|
||||
self.index_file_inner(uri, content, false);
|
||||
}
|
||||
|
||||
fn index_file_inner(&mut self, uri: Uri, content: &str, is_open_in_buffer: bool) -> Option<()> {
|
||||
self.remove_definitions_for_file(&uri);
|
||||
let grammar = self.language.grammar()?;
|
||||
let outline_config = grammar.outline_config.as_ref()?;
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&grammar.ts_language).ok()?;
|
||||
let tree = parser.parse(content, None)?;
|
||||
let declarations = extract_declarations_from_tree(&tree, content, outline_config);
|
||||
for (name, byte_range) in declarations {
|
||||
let range = byte_range_to_lsp_range(content, byte_range);
|
||||
let location = lsp::Location {
|
||||
uri: uri.clone(),
|
||||
range,
|
||||
};
|
||||
self.definitions
|
||||
.entry(name)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(location);
|
||||
}
|
||||
self.files.insert(
|
||||
uri,
|
||||
FileEntry {
|
||||
contents: content.to_string(),
|
||||
is_open_in_buffer,
|
||||
},
|
||||
);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn get_definitions(
|
||||
&mut self,
|
||||
uri: Uri,
|
||||
position: lsp::Position,
|
||||
) -> Option<lsp::GotoDefinitionResponse> {
|
||||
let entry = self.files.get(&uri)?;
|
||||
let name = word_at_position(&entry.contents, position)?;
|
||||
let locations = self.definitions.get(name).cloned()?;
|
||||
Some(lsp::GotoDefinitionResponse::Array(locations))
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_declarations_from_tree(
|
||||
tree: &Tree,
|
||||
content: &str,
|
||||
outline_config: &language::OutlineConfig,
|
||||
) -> Vec<(String, Range<usize>)> {
|
||||
let mut cursor = QueryCursor::new();
|
||||
let mut declarations = Vec::new();
|
||||
let mut matches = cursor.matches(&outline_config.query, tree.root_node(), content.as_bytes());
|
||||
while let Some(query_match) = matches.next() {
|
||||
let mut name_range: Option<Range<usize>> = None;
|
||||
let mut has_item_range = false;
|
||||
|
||||
for capture in query_match.captures {
|
||||
let range = capture.node.byte_range();
|
||||
if capture.index == outline_config.name_capture_ix {
|
||||
name_range = Some(range);
|
||||
} else if capture.index == outline_config.item_capture_ix {
|
||||
has_item_range = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name_range) = name_range
|
||||
&& has_item_range
|
||||
{
|
||||
let name = content[name_range.clone()].to_string();
|
||||
if declarations.iter().any(|(n, _)| n == &name) {
|
||||
continue;
|
||||
}
|
||||
declarations.push((name, name_range));
|
||||
}
|
||||
}
|
||||
declarations
|
||||
}
|
||||
|
||||
fn byte_range_to_lsp_range(content: &str, byte_range: Range<usize>) -> lsp::Range {
|
||||
let start = byte_offset_to_position(content, byte_range.start);
|
||||
let end = byte_offset_to_position(content, byte_range.end);
|
||||
lsp::Range { start, end }
|
||||
}
|
||||
|
||||
fn byte_offset_to_position(content: &str, offset: usize) -> lsp::Position {
|
||||
let mut line = 0;
|
||||
let mut character = 0;
|
||||
let mut current_offset = 0;
|
||||
for ch in content.chars() {
|
||||
if current_offset >= offset {
|
||||
break;
|
||||
}
|
||||
if ch == '\n' {
|
||||
line += 1;
|
||||
character = 0;
|
||||
} else {
|
||||
character += 1;
|
||||
}
|
||||
current_offset += ch.len_utf8();
|
||||
}
|
||||
lsp::Position { line, character }
|
||||
}
|
||||
|
||||
fn word_at_position(content: &str, position: lsp::Position) -> Option<&str> {
|
||||
let mut lines = content.lines();
|
||||
let line = lines.nth(position.line as usize)?;
|
||||
let column = position.character as usize;
|
||||
if column > line.len() {
|
||||
return None;
|
||||
}
|
||||
let start = line[..column]
|
||||
.rfind(|c: char| !c.is_alphanumeric() && c != '_')
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0);
|
||||
let end = line[column..]
|
||||
.find(|c: char| !c.is_alphanumeric() && c != '_')
|
||||
.map(|i| i + column)
|
||||
.unwrap_or(line.len());
|
||||
Some(&line[start..end]).filter(|word| !word.is_empty())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,126 +0,0 @@
|
||||
use language::{BufferSnapshot, SyntaxMapMatches};
|
||||
use std::{cmp::Reverse, ops::Range};
|
||||
|
||||
use crate::declaration::Identifier;
|
||||
|
||||
// TODO:
|
||||
//
|
||||
// * how to handle multiple name captures? for now last one wins
|
||||
//
|
||||
// * annotation ranges
|
||||
//
|
||||
// * new "signature" capture for outline queries
|
||||
//
|
||||
// * Check parent behavior of "int x, y = 0" declarations in a test
|
||||
|
||||
pub struct OutlineDeclaration {
|
||||
pub parent_index: Option<usize>,
|
||||
pub identifier: Identifier,
|
||||
pub item_range: Range<usize>,
|
||||
pub signature_range: Range<usize>,
|
||||
}
|
||||
|
||||
pub fn declarations_in_buffer(buffer: &BufferSnapshot) -> Vec<OutlineDeclaration> {
|
||||
declarations_overlapping_range(0..buffer.len(), buffer)
|
||||
}
|
||||
|
||||
pub fn declarations_overlapping_range(
|
||||
range: Range<usize>,
|
||||
buffer: &BufferSnapshot,
|
||||
) -> Vec<OutlineDeclaration> {
|
||||
let mut declarations = OutlineIterator::new(range, buffer).collect::<Vec<_>>();
|
||||
declarations.sort_unstable_by_key(|item| (item.item_range.start, Reverse(item.item_range.end)));
|
||||
|
||||
let mut parent_stack: Vec<(usize, Range<usize>)> = Vec::new();
|
||||
for (index, declaration) in declarations.iter_mut().enumerate() {
|
||||
while let Some((top_parent_index, top_parent_range)) = parent_stack.last() {
|
||||
if declaration.item_range.start >= top_parent_range.end {
|
||||
parent_stack.pop();
|
||||
} else {
|
||||
declaration.parent_index = Some(*top_parent_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
parent_stack.push((index, declaration.item_range.clone()));
|
||||
}
|
||||
declarations
|
||||
}
|
||||
|
||||
/// Iterates outline items without being ordered w.r.t. nested items and without populating
|
||||
/// `parent`.
|
||||
pub struct OutlineIterator<'a> {
|
||||
buffer: &'a BufferSnapshot,
|
||||
matches: SyntaxMapMatches<'a>,
|
||||
}
|
||||
|
||||
impl<'a> OutlineIterator<'a> {
|
||||
pub fn new(range: Range<usize>, buffer: &'a BufferSnapshot) -> Self {
|
||||
let matches = buffer.syntax.matches(range, &buffer.text, |grammar| {
|
||||
grammar.outline_config.as_ref().map(|c| &c.query)
|
||||
});
|
||||
|
||||
Self { buffer, matches }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for OutlineIterator<'a> {
|
||||
type Item = OutlineDeclaration;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some(mat) = self.matches.peek() {
|
||||
let config = self.matches.grammars()[mat.grammar_index]
|
||||
.outline_config
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
|
||||
let mut name_range = None;
|
||||
let mut item_range = None;
|
||||
let mut signature_start = None;
|
||||
let mut signature_end = None;
|
||||
|
||||
let mut add_to_signature = |range: Range<usize>| {
|
||||
if signature_start.is_none() {
|
||||
signature_start = Some(range.start);
|
||||
}
|
||||
signature_end = Some(range.end);
|
||||
};
|
||||
|
||||
for capture in mat.captures {
|
||||
let range = capture.node.byte_range();
|
||||
if capture.index == config.name_capture_ix {
|
||||
name_range = Some(range.clone());
|
||||
add_to_signature(range);
|
||||
} else if Some(capture.index) == config.context_capture_ix
|
||||
|| Some(capture.index) == config.extra_context_capture_ix
|
||||
{
|
||||
add_to_signature(range);
|
||||
} else if capture.index == config.item_capture_ix {
|
||||
item_range = Some(range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let language_id = mat.language.id();
|
||||
self.matches.advance();
|
||||
|
||||
if let Some(name_range) = name_range
|
||||
&& let Some(item_range) = item_range
|
||||
&& let Some(signature_start) = signature_start
|
||||
&& let Some(signature_end) = signature_end
|
||||
{
|
||||
let name = self
|
||||
.buffer
|
||||
.text_for_range(name_range)
|
||||
.collect::<String>()
|
||||
.into();
|
||||
|
||||
return Some(OutlineDeclaration {
|
||||
identifier: Identifier { name, language_id },
|
||||
item_range: item_range,
|
||||
signature_range: signature_start..signature_end,
|
||||
parent_index: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
use collections::HashMap;
|
||||
use language::BufferSnapshot;
|
||||
use std::ops::Range;
|
||||
use util::RangeExt;
|
||||
|
||||
use crate::{
|
||||
declaration::Identifier,
|
||||
excerpt::{EditPredictionExcerpt, EditPredictionExcerptText},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Reference {
|
||||
pub identifier: Identifier,
|
||||
pub range: Range<usize>,
|
||||
pub region: ReferenceRegion,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ReferenceRegion {
|
||||
Breadcrumb,
|
||||
Nearby,
|
||||
}
|
||||
|
||||
pub fn references_in_excerpt(
|
||||
excerpt: &EditPredictionExcerpt,
|
||||
excerpt_text: &EditPredictionExcerptText,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> HashMap<Identifier, Vec<Reference>> {
|
||||
let mut references = references_in_range(
|
||||
excerpt.range.clone(),
|
||||
excerpt_text.body.as_str(),
|
||||
ReferenceRegion::Nearby,
|
||||
snapshot,
|
||||
);
|
||||
|
||||
for ((_, range), text) in excerpt
|
||||
.parent_declarations
|
||||
.iter()
|
||||
.zip(excerpt_text.parent_signatures.iter())
|
||||
{
|
||||
references.extend(references_in_range(
|
||||
range.clone(),
|
||||
text.as_str(),
|
||||
ReferenceRegion::Breadcrumb,
|
||||
snapshot,
|
||||
));
|
||||
}
|
||||
|
||||
let mut identifier_to_references: HashMap<Identifier, Vec<Reference>> = HashMap::default();
|
||||
for reference in references {
|
||||
identifier_to_references
|
||||
.entry(reference.identifier.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(reference);
|
||||
}
|
||||
identifier_to_references
|
||||
}
|
||||
|
||||
/// Finds all nodes which have a "variable" match from the highlights query within the offset range.
|
||||
pub fn references_in_range(
|
||||
range: Range<usize>,
|
||||
range_text: &str,
|
||||
reference_region: ReferenceRegion,
|
||||
buffer: &BufferSnapshot,
|
||||
) -> Vec<Reference> {
|
||||
let mut matches = buffer
|
||||
.syntax
|
||||
.matches(range.clone(), &buffer.text, |grammar| {
|
||||
grammar
|
||||
.highlights_config
|
||||
.as_ref()
|
||||
.map(|config| &config.query)
|
||||
});
|
||||
|
||||
let mut references = Vec::new();
|
||||
let mut last_added_range = None;
|
||||
while let Some(mat) = matches.peek() {
|
||||
let config = matches.grammars()[mat.grammar_index]
|
||||
.highlights_config
|
||||
.as_ref();
|
||||
|
||||
if let Some(config) = config {
|
||||
for capture in mat.captures {
|
||||
if config.identifier_capture_indices.contains(&capture.index) {
|
||||
let node_range = capture.node.byte_range();
|
||||
|
||||
// sometimes multiple highlight queries match - this deduplicates them
|
||||
if Some(node_range.clone()) == last_added_range {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !range.contains_inclusive(&node_range) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let identifier_text =
|
||||
&range_text[node_range.start - range.start..node_range.end - range.start];
|
||||
|
||||
references.push(Reference {
|
||||
identifier: Identifier {
|
||||
name: identifier_text.into(),
|
||||
language_id: mat.language.id(),
|
||||
},
|
||||
range: node_range.clone(),
|
||||
region: reference_region,
|
||||
});
|
||||
last_added_range = Some(node_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matches.advance();
|
||||
}
|
||||
references
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::{TestAppContext, prelude::*};
|
||||
use indoc::indoc;
|
||||
use language::{BufferSnapshot, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
|
||||
|
||||
use crate::reference::{ReferenceRegion, references_in_range};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_identifier_node_truncated(cx: &mut TestAppContext) {
|
||||
let code = indoc! { r#"
|
||||
fn main() {
|
||||
add(1, 2);
|
||||
}
|
||||
|
||||
fn add(a: i32, b: i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
"# };
|
||||
let buffer = create_buffer(code, cx);
|
||||
|
||||
let range = 0..35;
|
||||
let references = references_in_range(
|
||||
range.clone(),
|
||||
&code[range],
|
||||
ReferenceRegion::Breadcrumb,
|
||||
&buffer,
|
||||
);
|
||||
assert_eq!(references.len(), 2);
|
||||
assert_eq!(references[0].identifier.name.as_ref(), "main");
|
||||
assert_eq!(references[1].identifier.name.as_ref(), "add");
|
||||
}
|
||||
|
||||
fn create_buffer(text: &str, cx: &mut TestAppContext) -> BufferSnapshot {
|
||||
let buffer =
|
||||
cx.new(|cx| language::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_highlights_query(include_str!("../../languages/src/rust/highlights.scm"))
|
||||
.unwrap()
|
||||
.with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,314 +0,0 @@
|
||||
use hashbrown::HashTable;
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
hash::{Hash, Hasher as _},
|
||||
path::Path,
|
||||
sync::LazyLock,
|
||||
};
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
use crate::reference::Reference;
|
||||
|
||||
// TODO: Consider implementing sliding window similarity matching like
|
||||
// https://github.com/sourcegraph/cody-public-snapshot/blob/8e20ac6c1460c08b0db581c0204658112a246eda/vscode/src/completions/context/retrievers/jaccard-similarity/bestJaccardMatch.ts
|
||||
//
|
||||
// That implementation could actually be more efficient - no need to track words in the window that
|
||||
// are not in the query.
|
||||
|
||||
// TODO: Consider a flat sorted Vec<(String, usize)> representation. Intersection can just walk the
|
||||
// two in parallel.
|
||||
|
||||
static IDENTIFIER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\b\w+\b").unwrap());
|
||||
|
||||
/// Multiset of text occurrences for text similarity that only stores hashes and counts.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Occurrences {
|
||||
table: HashTable<OccurrenceEntry>,
|
||||
total_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OccurrenceEntry {
|
||||
hash: u64,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Occurrences {
|
||||
pub fn within_string(text: &str) -> Self {
|
||||
Self::from_identifiers(IDENTIFIER_REGEX.find_iter(text).map(|mat| mat.as_str()))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn within_references(references: &[Reference]) -> Self {
|
||||
Self::from_identifiers(
|
||||
references
|
||||
.iter()
|
||||
.map(|reference| reference.identifier.name.as_ref()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_identifiers(identifiers: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
|
||||
let mut this = Self::default();
|
||||
// TODO: Score matches that match case higher?
|
||||
//
|
||||
// TODO: Also include unsplit identifier?
|
||||
for identifier in identifiers {
|
||||
for identifier_part in split_identifier(identifier.as_ref()) {
|
||||
this.add_hash(fx_hash(&identifier_part.to_lowercase()));
|
||||
}
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
pub fn from_worktree_path(worktree_name: Option<Cow<'_, str>>, rel_path: &RelPath) -> Self {
|
||||
if let Some(worktree_name) = worktree_name {
|
||||
Self::from_identifiers(
|
||||
std::iter::once(worktree_name)
|
||||
.chain(iter_path_without_extension(rel_path.as_std_path())),
|
||||
)
|
||||
} else {
|
||||
Self::from_path(rel_path.as_std_path())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_path(path: &Path) -> Self {
|
||||
Self::from_identifiers(iter_path_without_extension(path))
|
||||
}
|
||||
|
||||
fn add_hash(&mut self, hash: u64) {
|
||||
self.table
|
||||
.entry(
|
||||
hash,
|
||||
|entry: &OccurrenceEntry| entry.hash == hash,
|
||||
|entry| entry.hash,
|
||||
)
|
||||
.and_modify(|entry| entry.count += 1)
|
||||
.or_insert(OccurrenceEntry { hash, count: 1 });
|
||||
self.total_count += 1;
|
||||
}
|
||||
|
||||
fn contains_hash(&self, hash: u64) -> bool {
|
||||
self.get_count(hash) != 0
|
||||
}
|
||||
|
||||
fn get_count(&self, hash: u64) -> usize {
|
||||
self.table
|
||||
.find(hash, |entry| entry.hash == hash)
|
||||
.map(|entry| entry.count)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn iter_path_without_extension(path: &Path) -> impl Iterator<Item = Cow<'_, str>> {
|
||||
let last_component: Option<Cow<'_, str>> = path.file_stem().map(|stem| stem.to_string_lossy());
|
||||
let mut path_components = path.components();
|
||||
path_components.next_back();
|
||||
path_components
|
||||
.map(|component| component.as_os_str().to_string_lossy())
|
||||
.chain(last_component)
|
||||
}
|
||||
|
||||
pub fn fx_hash<T: Hash + ?Sized>(data: &T) -> u64 {
|
||||
let mut hasher = collections::FxHasher::default();
|
||||
data.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
// Splits camelcase / snakecase / kebabcase / pascalcase
|
||||
//
|
||||
// TODO: Make this more efficient / elegant.
|
||||
fn split_identifier(identifier: &str) -> Vec<&str> {
|
||||
let mut parts = Vec::new();
|
||||
let mut start = 0;
|
||||
let chars: Vec<char> = identifier.chars().collect();
|
||||
|
||||
if chars.is_empty() {
|
||||
return parts;
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
let ch = chars[i];
|
||||
|
||||
// Handle explicit delimiters (underscore and hyphen)
|
||||
if ch == '_' || ch == '-' {
|
||||
if i > start {
|
||||
parts.push(&identifier[start..i]);
|
||||
}
|
||||
start = i + 1;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle camelCase and PascalCase transitions
|
||||
if i > 0 && i < chars.len() {
|
||||
let prev_char = chars[i - 1];
|
||||
|
||||
// Transition from lowercase/digit to uppercase
|
||||
if (prev_char.is_lowercase() || prev_char.is_ascii_digit()) && ch.is_uppercase() {
|
||||
parts.push(&identifier[start..i]);
|
||||
start = i;
|
||||
}
|
||||
// Handle sequences like "XMLParser" -> ["XML", "Parser"]
|
||||
else if i + 1 < chars.len()
|
||||
&& ch.is_uppercase()
|
||||
&& chars[i + 1].is_lowercase()
|
||||
&& prev_char.is_uppercase()
|
||||
{
|
||||
parts.push(&identifier[start..i]);
|
||||
start = i;
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Add the last part if there's any remaining
|
||||
if start < identifier.len() {
|
||||
parts.push(&identifier[start..]);
|
||||
}
|
||||
|
||||
// Filter out empty strings
|
||||
parts.into_iter().filter(|s| !s.is_empty()).collect()
|
||||
}
|
||||
|
||||
pub fn jaccard_similarity<'a>(mut set_a: &'a Occurrences, mut set_b: &'a Occurrences) -> f32 {
|
||||
if set_a.table.len() > set_b.table.len() {
|
||||
std::mem::swap(&mut set_a, &mut set_b);
|
||||
}
|
||||
let intersection = set_a
|
||||
.table
|
||||
.iter()
|
||||
.filter(|entry| set_b.contains_hash(entry.hash))
|
||||
.count();
|
||||
let union = set_a.table.len() + set_b.table.len() - intersection;
|
||||
intersection as f32 / union as f32
|
||||
}
|
||||
|
||||
// TODO
|
||||
#[allow(dead_code)]
|
||||
pub fn overlap_coefficient<'a>(mut set_a: &'a Occurrences, mut set_b: &'a Occurrences) -> f32 {
|
||||
if set_a.table.len() > set_b.table.len() {
|
||||
std::mem::swap(&mut set_a, &mut set_b);
|
||||
}
|
||||
let intersection = set_a
|
||||
.table
|
||||
.iter()
|
||||
.filter(|entry| set_b.contains_hash(entry.hash))
|
||||
.count();
|
||||
intersection as f32 / set_a.table.len() as f32
|
||||
}
|
||||
|
||||
// TODO
|
||||
#[allow(dead_code)]
|
||||
pub fn weighted_jaccard_similarity<'a>(
|
||||
mut set_a: &'a Occurrences,
|
||||
mut set_b: &'a Occurrences,
|
||||
) -> f32 {
|
||||
if set_a.table.len() > set_b.table.len() {
|
||||
std::mem::swap(&mut set_a, &mut set_b);
|
||||
}
|
||||
|
||||
let mut numerator = 0;
|
||||
let mut denominator_a = 0;
|
||||
let mut used_count_b = 0;
|
||||
for entry_a in set_a.table.iter() {
|
||||
let count_a = entry_a.count;
|
||||
let count_b = set_b.get_count(entry_a.hash);
|
||||
numerator += count_a.min(count_b);
|
||||
denominator_a += count_a.max(count_b);
|
||||
used_count_b += count_b;
|
||||
}
|
||||
|
||||
let denominator = denominator_a + (set_b.total_count - used_count_b);
|
||||
if denominator == 0 {
|
||||
0.0
|
||||
} else {
|
||||
numerator as f32 / denominator as f32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn weighted_overlap_coefficient<'a>(
|
||||
mut set_a: &'a Occurrences,
|
||||
mut set_b: &'a Occurrences,
|
||||
) -> f32 {
|
||||
if set_a.table.len() > set_b.table.len() {
|
||||
std::mem::swap(&mut set_a, &mut set_b);
|
||||
}
|
||||
|
||||
let mut numerator = 0;
|
||||
for entry_a in set_a.table.iter() {
|
||||
let count_a = entry_a.count;
|
||||
let count_b = set_b.get_count(entry_a.hash);
|
||||
numerator += count_a.min(count_b);
|
||||
}
|
||||
|
||||
let denominator = set_a.total_count.min(set_b.total_count);
|
||||
if denominator == 0 {
|
||||
0.0
|
||||
} else {
|
||||
numerator as f32 / denominator as f32
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_identifier() {
|
||||
assert_eq!(split_identifier("snake_case"), vec!["snake", "case"]);
|
||||
assert_eq!(split_identifier("kebab-case"), vec!["kebab", "case"]);
|
||||
assert_eq!(split_identifier("PascalCase"), vec!["Pascal", "Case"]);
|
||||
assert_eq!(split_identifier("camelCase"), vec!["camel", "Case"]);
|
||||
assert_eq!(split_identifier("XMLParser"), vec!["XML", "Parser"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_similarity_functions() {
|
||||
// 10 identifier parts, 8 unique
|
||||
// Repeats: 2 "outline", 2 "items"
|
||||
let set_a = Occurrences::within_string(
|
||||
"let mut outline_items = query_outline_items(&language, &tree, &source);",
|
||||
);
|
||||
// 14 identifier parts, 11 unique
|
||||
// Repeats: 2 "outline", 2 "language", 2 "tree"
|
||||
let set_b = Occurrences::within_string(
|
||||
"pub fn query_outline_items(language: &Language, tree: &Tree, source: &str) -> Vec<OutlineItem> {",
|
||||
);
|
||||
|
||||
// 6 overlaps: "outline", "items", "query", "language", "tree", "source"
|
||||
// 7 non-overlaps: "let", "mut", "pub", "fn", "vec", "item", "str"
|
||||
assert_eq!(jaccard_similarity(&set_a, &set_b), 6.0 / (6.0 + 7.0));
|
||||
|
||||
// Numerator is one more than before due to both having 2 "outline".
|
||||
// Denominator is the same except for 3 more due to the non-overlapping duplicates
|
||||
assert_eq!(
|
||||
weighted_jaccard_similarity(&set_a, &set_b),
|
||||
7.0 / (7.0 + 7.0 + 3.0)
|
||||
);
|
||||
|
||||
// Numerator is the same as jaccard_similarity. Denominator is the size of the smaller set, 8.
|
||||
assert_eq!(overlap_coefficient(&set_a, &set_b), 6.0 / 8.0);
|
||||
|
||||
// Numerator is the same as weighted_jaccard_similarity. Denominator is the total weight of
|
||||
// the smaller set, 10.
|
||||
assert_eq!(weighted_overlap_coefficient(&set_a, &set_b), 7.0 / 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_path_without_extension() {
|
||||
let mut iter = iter_path_without_extension(Path::new(""));
|
||||
assert_eq!(iter.next(), None);
|
||||
|
||||
let iter = iter_path_without_extension(Path::new("foo"));
|
||||
assert_eq!(iter.collect::<Vec<_>>(), ["foo"]);
|
||||
|
||||
let iter = iter_path_without_extension(Path::new("foo/bar.txt"));
|
||||
assert_eq!(iter.collect::<Vec<_>>(), ["foo", "bar"]);
|
||||
|
||||
let iter = iter_path_without_extension(Path::new("foo/bar/baz.txt"));
|
||||
assert_eq!(iter.collect::<Vec<_>>(), ["foo", "bar", "baz"]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user