Skip to content

Commit

Permalink
Fix goto-references across packages.
Browse files Browse the repository at this point in the history
  • Loading branch information
rcorre committed Oct 29, 2024
1 parent fa09b38 commit 883244a
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 108 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pbls"
version = "1.0.4"
version = "1.0.5"
edition = "2021"
license = "MIT"
description = "Protobuf Language Server"
Expand Down
183 changes: 109 additions & 74 deletions src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,56 +322,55 @@ impl File {
})
}

pub fn references(self: &Self, item: &GotoContext) -> Vec<tree_sitter::Range> {
match item {
GotoContext::Type(typ) => {
static QUERY: OnceLock<tree_sitter::Query> = OnceLock::new();
let typ = typ.name;
let pkg = self.package();
let query = QUERY.get_or_init(|| {
tree_sitter::Query::new(language(), "(field (type) @name)").unwrap()
});
log::trace!("Searching for references to {typ} in package {pkg:?}");

let mut qc = tree_sitter::QueryCursor::new();
qc.matches(&query, self.tree.root_node(), self.text.as_bytes())
.map(|m| m.captures[0].node)
.inspect(|x| log::trace!("Check {x:?}: {} == {typ}", self.get_text(*x)))
.filter(|node| {
let text = self.get_text(*node);
// first check the fully qualified name
text == typ
|| match pkg {
// If we have a package, try stripping that, in case we're in the same package
Some(pkg) => {
log::trace!(
"Trying to match '{typ}' to '{text}' without package {pkg}"
);
typ.strip_prefix(pkg)
.and_then(|typ| typ.strip_prefix("."))
.map(|target| target == text)
.unwrap_or(false)
}
None => false,
}
})
.map(|node| node.range())
.collect()
}
GotoContext::Import(name) => {
static QUERY: OnceLock<tree_sitter::Query> = OnceLock::new();
let query = QUERY.get_or_init(|| {
tree_sitter::Query::new(language(), "(import (strLit) @name)").unwrap()
});

let mut qc = tree_sitter::QueryCursor::new();
qc.matches(&query, self.tree.root_node(), self.text.as_bytes())
.map(|m| m.captures[0].node)
.filter(|n| self.get_text(*n).trim_matches('"') == *name)
.map(|n| n.range())
.collect()
}
}
pub fn type_references(
self: &Self,
pkg: Option<&str>,
typ: &GotoTypeContext,
) -> Vec<tree_sitter::Range> {
static QUERY: OnceLock<tree_sitter::Query> = OnceLock::new();
let query = QUERY
.get_or_init(|| tree_sitter::Query::new(language(), "(field (type) @name)").unwrap());
let typ = typ.name;
log::trace!("Searching for references to {typ} in package {pkg:?}");

let mut qc = tree_sitter::QueryCursor::new();
qc.matches(&query, self.tree.root_node(), self.text.as_bytes())
.map(|m| m.captures[0].node)
.inspect(|x| log::trace!("Check {x:?}: {} == {typ}", self.get_text(*x)))
.filter(|node| {
let text = self.get_text(*node);
// first check the fully qualified name
text == typ
|| match pkg {
// If we have a package, try stripping that, in case we're in the same package
Some(pkg) => {
log::trace!(
"Trying to match '{typ}' to '{text}' without package {pkg}"
);
text.strip_prefix(pkg)
.and_then(|text| text.strip_prefix("."))
.map(|text| typ == text)
.unwrap_or(false)
}
None => false,
}
})
.map(|node| node.range())
.collect()
}

pub fn import_references(self: &Self, file: &str) -> Vec<tree_sitter::Range> {
static QUERY: OnceLock<tree_sitter::Query> = OnceLock::new();
let query = QUERY.get_or_init(|| {
tree_sitter::Query::new(language(), "(import (strLit) @name)").unwrap()
});

let mut qc = tree_sitter::QueryCursor::new();
qc.matches(&query, self.tree.root_node(), self.text.as_bytes())
.map(|m| m.captures[0].node)
.filter(|n| self.get_text(*n).trim_matches('"') == file)
.map(|n| n.range())
.collect()
}

pub fn type_at(self: &Self, row: usize, col: usize) -> Option<GotoContext> {
Expand Down Expand Up @@ -1000,7 +999,7 @@ mod tests {
}

#[test]
fn test_references() {
fn test_import_references() {
let _ = env_logger::builder().is_test(true).try_init();
let file = File::new(
[
Expand All @@ -1020,7 +1019,7 @@ mod tests {
.unwrap();

assert_eq!(
file.references(&GotoContext::Import("foo.proto")),
file.import_references("foo.proto"),
vec![tree_sitter::Range {
start_byte: 41,
end_byte: 52,
Expand All @@ -1030,7 +1029,7 @@ mod tests {
);

assert_eq!(
file.references(&GotoContext::Import("bar.proto")),
file.import_references("bar.proto"),
vec![tree_sitter::Range {
start_byte: 61,
end_byte: 72,
Expand All @@ -1039,13 +1038,37 @@ mod tests {
}]
);

assert_eq!(file.references(&GotoContext::Import("baz.proto")), vec![]);
assert_eq!(file.import_references("baz.proto"), vec![]);
}

#[test]
fn test_type_references() {
let _ = env_logger::builder().is_test(true).try_init();
let file = File::new(
[
"syntax = \"proto3\";",
"package thing;",
"import \"foo.proto\";",
"import \"bar.proto\";",
"message Foo {",
" Bar b = 1;",
" Biz.Buz bb = 2;",
" buf.Buf buf = 3;",
"}",
"",
]
.join("\n"),
)
.unwrap();

assert_eq!(
file.references(&GotoContext::Type(GotoTypeContext {
name: "thing.Bar",
parent: None
})),
file.type_references(
None,
&GotoTypeContext {
name: "Bar",
parent: None
}
),
vec![tree_sitter::Range {
start_byte: 92,
end_byte: 95,
Expand All @@ -1055,10 +1078,13 @@ mod tests {
);

assert_eq!(
file.references(&GotoContext::Type(GotoTypeContext {
name: "thing.Biz.Buz",
parent: None
})),
file.type_references(
Some("thing"),
&GotoTypeContext {
name: "Biz.Buz",
parent: None
}
),
vec![tree_sitter::Range {
start_byte: 107,
end_byte: 114,
Expand All @@ -1068,10 +1094,13 @@ mod tests {
);

assert_eq!(
file.references(&GotoContext::Type(GotoTypeContext {
name: "buf.Buf",
parent: None
})),
file.type_references(
Some("buf"),
&GotoTypeContext {
name: "Buf",
parent: None
}
),
vec![tree_sitter::Range {
start_byte: 127,
end_byte: 134,
Expand All @@ -1081,18 +1110,24 @@ mod tests {
);

assert_eq!(
file.references(&GotoContext::Type(GotoTypeContext {
name: "Buf",
parent: None
})),
file.type_references(
Some("thing"),
&GotoTypeContext {
name: "Buf",
parent: None
}
),
vec![]
);

assert_eq!(
file.references(&GotoContext::Type(GotoTypeContext {
name: "buf",
parent: None
})),
file.type_references(
Some("thing"),
&GotoTypeContext {
name: "buf",
parent: None
}
),
vec![]
);
}
Expand Down
92 changes: 64 additions & 28 deletions src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ impl Workspace {
.collect())
}

pub fn all_symbols(&mut self, query: &str) -> Result<Vec<SymbolInformation>> {
fn load_all(&mut self) -> Result<()> {
log::debug!("Loading all files");
let paths = self
.proto_paths
.iter()
Expand All @@ -144,9 +145,32 @@ impl Workspace {
.filter_map(|p| p.ok())
.map(|f| f.path())
.filter(|p| p.is_file() && p.extension().map_or(false, |e| e == "proto"))
.map(|p| std::fs::canonicalize(p));
let mut res = vec![];
let mut qc = tree_sitter::QueryCursor::new();
.filter_map(|p| match std::fs::canonicalize(&p) {
Ok(p) => Some(p),
Err(err) => {
log::error!("Failed to open '{p:?}': {err:?}");
None
}
});

for path in paths {
log::debug!("Loading {path:?}");
let uri = Url::from_file_path(&path).or(Err(format!("Invalid path: {path:?}")))?;
if let Some(file) = self.files.get(&uri) {
file
} else {
let text = std::fs::read_to_string(uri.path())?;
let file = file::File::new(text)?;
self.files.insert(uri.clone(), file);
self.files.get(&uri).unwrap()
};
}

Ok(())
}

pub fn all_symbols(&mut self, query: &str) -> Result<Vec<SymbolInformation>> {
self.load_all()?;

let regexes: std::result::Result<Vec<_>, _> = query
.split_whitespace()
Expand All @@ -164,17 +188,9 @@ impl Workspace {
let regexes = regexes?;
log::debug!("Searching workspace symbols with patterns: {regexes:?}");

for path in paths {
let path = path?;
let uri = Url::from_file_path(&path).or(Err(format!("Invalid path: {path:?}")))?;
let file = if let Some(file) = self.files.get(&uri) {
file
} else {
let text = std::fs::read_to_string(uri.path())?;
let file = file::File::new(text)?;
self.files.insert(uri.clone(), file);
self.files.get(&uri).unwrap()
};
let mut res = vec![];
let mut qc = tree_sitter::QueryCursor::new();
for (uri, file) in &self.files {
let symbols = file.symbols(&mut qc);
let syms = symbols
.filter(|s| regexes.iter().all(|r| r.is_match(&s.name)))
Expand Down Expand Up @@ -228,7 +244,6 @@ impl Workspace {
}

// Return the relative paths of proto files under the given dir.

pub fn goto(&self, uri: Url, pos: lsp_types::Position) -> Result<Option<lsp_types::Location>> {
let file = self.get(&uri)?;
let ctx = file.type_at(pos.line.try_into()?, pos.character.try_into()?);
Expand All @@ -247,11 +262,14 @@ impl Workspace {
}

pub fn references(
&self,
&mut self,
params: lsp_types::ReferenceParams,
) -> Result<Option<Vec<lsp_types::Location>>> {
self.load_all()?;

let doc = params.text_document_position;
let file = self.get(&doc.text_document.uri)?;
let uri = &doc.text_document.uri;
let file = self.get(&uri)?;

let Some(item) = file.type_at(
doc.position.line.try_into()?,
Expand All @@ -261,16 +279,34 @@ impl Workspace {
};

let mut res = Vec::new();
for (uri, file) in self.files.iter() {
res.extend(
file.references(&item)
.iter()
.map(|range| lsp_types::Location {
uri: uri.clone(),
range: to_lsp_range(*range),
}),
);
}
match &item {
file::GotoContext::Type(t) => {
let src = self
.find_symbol(uri.clone(), file, &t)?
.ok_or("Symbol not found: {t:?}")?;
let src = self.get(&src.uri)?;
let pkg = src.package();
for (uri, file) in self.files.iter() {
res.extend(file.type_references(pkg, t).iter().map(|range| {
lsp_types::Location {
uri: uri.clone(),
range: to_lsp_range(*range),
}
}));
}
}
file::GotoContext::Import(import) => {
for (uri, file) in self.files.iter() {
res.extend(file.import_references(import).iter().map(|range| {
lsp_types::Location {
uri: uri.clone(),
range: to_lsp_range(*range),
}
}));
}
}
};

Ok(Some(res))
}

Expand Down
Loading

0 comments on commit 883244a

Please sign in to comment.