use std::panic::AssertUnwindSafe;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use lsp_types::{Location, Position, Range, Uri};
use rowan::{TextRange, TextSize};
use crate::incremental::Analysis;
use crate::index::model::{DefLocation, Span};
use crate::index::{ModuleIndex, PackageIndex};
use crate::parser::parse;
use crate::resolve::{
ModulePath, Namespace, OccurrenceKey, PackageSource, Resolution, Resolver, module_at,
resolve_submodule,
};
use crate::semantic::{Access, BindingId, BindingKind, LoadKind, QualifiedRead, SemanticModel};
use crate::text::{LineIndex, PositionEncoding};
use super::cross_file;
use super::uri::from_path;
pub fn compute_definition<P: PackageSource>(
uri: &Uri,
text: &str,
position: Position,
encoding: PositionEncoding,
packages: &P,
) -> Vec<Location> {
let model = SemanticModel::build(&parse(text).cst);
let line_index = LineIndex::new(text);
let offset = TextSize::new(line_index.position_to_byte(position, encoding) as u32);
let workspace = super::uri::to_path(uri).and_then(|p| packages.workspace_member(&p));
definition_for(
&model,
packages,
workspace,
uri,
&line_index,
offset,
encoding,
)
}
pub(crate) fn definition_via_db(
snapshot: &Analysis,
uri: &Uri,
path: &Path,
text: &str,
position: Position,
encoding: PositionEncoding,
) -> Vec<Location> {
let line_index = LineIndex::new(text);
let offset = TextSize::new(line_index.position_to_byte(position, encoding) as u32);
let cached = salsa::Cancelled::catch(AssertUnwindSafe(|| {
let file = snapshot.lookup_file(path)?;
if snapshot.file_text(file) != text {
return None;
}
let model = snapshot.semantic_model(file);
if let Some(symbol) = cross_file::workspace_symbol_at(snapshot, path, model, offset)
&& is_workspace_function(snapshot, path, &symbol)
{
let locations = cross_file_definitions(snapshot, &symbol, encoding);
if !locations.is_empty() {
return Some(locations);
}
}
let workspace = snapshot.workspace_member(path);
Some(definition_for(
model,
snapshot,
workspace,
uri,
&line_index,
offset,
encoding,
))
}));
match cached {
Ok(Some(locations)) => locations,
Ok(None) | Err(_) => compute_definition(uri, text, position, encoding, snapshot),
}
}
fn is_workspace_function(snapshot: &Analysis, path: &Path, key: &OccurrenceKey) -> bool {
if key.namespace != Namespace::Value {
return false;
}
let Some((pkg, _)) = snapshot.workspace_member(path) else {
return false;
};
let Some(module) = module_at(&pkg.root, &key.module) else {
return false;
};
module
.functions
.iter()
.any(|f| f.name == key.name && f.owner.is_none())
}
fn cross_file_definitions(
snapshot: &Analysis,
symbol: &OccurrenceKey,
encoding: PositionEncoding,
) -> Vec<Location> {
cross_file::gather_sites(snapshot, symbol, encoding)
.into_iter()
.filter(|site| site.is_def || site.access == Access::Write)
.map(|site| Location {
uri: site.uri,
range: site.range,
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn definition_for<P: PackageSource>(
model: &SemanticModel,
packages: &P,
workspace: Option<(Arc<PackageIndex>, ModulePath)>,
uri: &Uri,
line_index: &LineIndex,
offset: TextSize,
encoding: PositionEncoding,
) -> Vec<Location> {
if let Some(q) = model
.qualified_reads()
.iter()
.find(|q| q.range.contains_inclusive(offset))
{
return qualified_locations(q, packages, encoding).unwrap_or_default();
}
if let Some(ident) = model.ident_at(offset) {
if let Some(bid) = ident.binding {
return binding_locations(model, uri, bid, line_index, encoding);
}
let ns = if ident.is_macro {
Namespace::Macro
} else {
Namespace::Value
};
return free_read_locations(
model,
packages,
workspace,
uri,
&ident.name,
offset,
ns,
line_index,
encoding,
);
}
if let Some(bid) = model.binding_at(offset) {
return binding_locations(model, uri, bid, line_index, encoding);
}
Vec::new()
}
fn qualified_locations<P: PackageSource>(
q: &QualifiedRead,
packages: &P,
encoding: PositionEncoding,
) -> Option<Vec<Location>> {
let (name, module_path) = q.path.split_last()?;
let head = module_path.first()?;
let pkg = packages.package(head)?;
let rest: Vec<&str> = module_path[1..].iter().map(|s| s.as_str()).collect();
let module = resolve_submodule(&pkg.root, &rest)?;
Some(library_locations(packages, &pkg, module, name, encoding))
}
fn binding_locations(
model: &SemanticModel,
uri: &Uri,
bid: BindingId,
line_index: &LineIndex,
encoding: PositionEncoding,
) -> Vec<Location> {
let binding = model.binding(bid);
let mut ranges = vec![binding.def_range];
if matches!(binding.kind, BindingKind::Function | BindingKind::Macro) {
ranges.extend(
model
.idents()
.iter()
.filter(|i| i.binding == Some(bid) && i.access == Access::Write)
.map(|i| i.range),
);
}
ranges.sort_by_key(|r| r.start());
ranges.dedup();
ranges
.into_iter()
.map(|range| self_location(uri, range, line_index, encoding))
.collect()
}
fn self_location(
uri: &Uri,
range: TextRange,
line_index: &LineIndex,
encoding: PositionEncoding,
) -> Location {
Location {
uri: uri.clone(),
range: to_range(range, line_index, encoding),
}
}
#[allow(clippy::too_many_arguments)]
fn free_read_locations<P: PackageSource>(
model: &SemanticModel,
packages: &P,
workspace: Option<(Arc<PackageIndex>, ModulePath)>,
uri: &Uri,
name: &str,
offset: TextSize,
ns: Namespace,
line_index: &LineIndex,
encoding: PositionEncoding,
) -> Vec<Location> {
match Resolver::new(model, packages)
.with_workspace(workspace.clone())
.resolve(name, offset, ns)
{
Resolution::Binding(bid) => binding_locations(model, uri, bid, line_index, encoding),
Resolution::Workspace { module, name } => {
let Some((pkg, _)) = workspace else {
return Vec::new();
};
let Some(host) = module_at(&pkg.root, &module) else {
return Vec::new();
};
library_locations(packages, &pkg, host, &name, encoding)
}
Resolution::System { module, name } => {
let Some(pkg) = packages.package(&module) else {
return Vec::new();
};
library_locations(packages, &pkg, &pkg.root, &name, encoding)
}
Resolution::Using { module, name } => {
library_from_using(model, packages, &module, &name, encoding)
}
Resolution::Unresolved => Vec::new(),
}
}
fn using_def_sites<P: PackageSource>(
model: &SemanticModel,
packages: &P,
module: &str,
name: &str,
) -> Vec<(PathBuf, Span)> {
if let Some(pkg) = packages.package(module) {
let sites = library_def_sites(packages, &pkg, &pkg.root, name);
if !sites.is_empty() {
return sites;
}
}
for load in model.module_loads() {
if load.kind != LoadKind::Using || load.items.is_some() {
continue;
}
let comps = &load.path.components;
if comps.last().map(|c| c.as_str()) != Some(module) {
continue;
}
let Some(first) = comps.first() else { continue };
let Some(pkg) = packages.package(first.as_str()) else {
continue;
};
let rest: Vec<&str> = comps[1..].iter().map(|c| c.as_str()).collect();
if let Some(m) = resolve_submodule(&pkg.root, &rest) {
let sites = library_def_sites(packages, &pkg, m, name);
if !sites.is_empty() {
return sites;
}
}
}
Vec::new()
}
pub(crate) fn using_def_site<P: PackageSource>(
model: &SemanticModel,
packages: &P,
module: &str,
name: &str,
) -> Option<(PathBuf, Span)> {
using_def_sites(model, packages, module, name)
.into_iter()
.next()
}
fn library_from_using<P: PackageSource>(
model: &SemanticModel,
packages: &P,
module: &str,
name: &str,
encoding: PositionEncoding,
) -> Vec<Location> {
site_locations(using_def_sites(model, packages, module, name), encoding)
}
fn library_def_sites<P: PackageSource>(
packages: &P,
pkg: &PackageIndex,
module: &ModuleIndex,
name: &str,
) -> Vec<(PathBuf, Span)> {
let defs = library_def_locations(module, name);
if defs.is_empty() {
return Vec::new();
}
let Some(root) = packages.package_root(&pkg.name) else {
return Vec::new();
};
defs.into_iter()
.map(|def| (root.join(&def.file), def.range))
.collect()
}
pub(crate) fn library_def_site<P: PackageSource>(
packages: &P,
pkg: &PackageIndex,
module: &ModuleIndex,
name: &str,
) -> Option<(PathBuf, Span)> {
library_def_sites(packages, pkg, module, name)
.into_iter()
.next()
}
fn library_locations<P: PackageSource>(
packages: &P,
pkg: &PackageIndex,
module: &ModuleIndex,
name: &str,
encoding: PositionEncoding,
) -> Vec<Location> {
site_locations(library_def_sites(packages, pkg, module, name), encoding)
}
fn site_locations(mut sites: Vec<(PathBuf, Span)>, encoding: PositionEncoding) -> Vec<Location> {
sites.sort_by(|a, b| (&a.0, a.1.start).cmp(&(&b.0, b.1.start)));
sites.dedup();
let mut out = Vec::new();
for chunk in sites.chunk_by(|a, b| a.0 == b.0) {
let abs = &chunk[0].0;
let Ok(text) = std::fs::read_to_string(abs) else {
continue;
};
let Some(uri) = from_path(abs) else {
continue;
};
let line_index = LineIndex::new(&text);
for (_, span) in chunk {
out.push(Location {
uri: uri.clone(),
range: span_to_range(*span, &line_index, encoding),
});
}
}
out
}
fn library_def_locations<'m>(module: &'m ModuleIndex, name: &str) -> Vec<&'m DefLocation> {
if name.starts_with('@') {
return module
.macros
.iter()
.find(|m| m.name == name)
.map(|m| &m.loc)
.into_iter()
.collect();
}
if let Some(f) = module.functions.iter().find(|f| f.name == name) {
return f.methods.iter().map(|m| &m.loc).collect();
}
if let Some(t) = module.types.iter().find(|t| t.name == name) {
return vec![&t.loc];
}
if let Some(c) = module.consts.iter().find(|c| c.name == name) {
return vec![&c.loc];
}
Vec::new()
}
fn to_range(range: TextRange, line_index: &LineIndex, encoding: PositionEncoding) -> Range {
Range {
start: line_index.byte_to_position(range.start().into(), encoding),
end: line_index.byte_to_position(range.end().into(), encoding),
}
}
fn span_to_range(span: Span, line_index: &LineIndex, encoding: PositionEncoding) -> Range {
Range {
start: line_index.byte_to_position(span.start as usize, encoding),
end: line_index.byte_to_position(span.end as usize, encoding),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::index::harvest_package_named;
use super::super::uri::to_path;
#[derive(Default)]
struct TestLib {
packages: BTreeMap<String, Arc<PackageIndex>>,
roots: BTreeMap<String, PathBuf>,
workspace: Option<(Arc<PackageIndex>, ModulePath)>,
}
impl PackageSource for TestLib {
fn package(&self, name: &str) -> Option<Arc<PackageIndex>> {
self.packages.get(name).cloned()
}
fn package_root(&self, name: &str) -> Option<PathBuf> {
self.roots.get(name).cloned()
}
fn workspace_member(&self, _path: &Path) -> Option<(Arc<PackageIndex>, ModulePath)> {
self.workspace.clone()
}
}
struct TempDir {
path: PathBuf,
}
impl TempDir {
fn new() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!("fatou-def-{}-{}", std::process::id(), n));
fs::create_dir_all(&path).unwrap();
Self { path }
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn doc_uri() -> Uri {
Uri::from_str("file:///work/s.jl").unwrap()
}
fn def_at(marked: &str, lib: &impl PackageSource) -> Vec<Location> {
let offset = marked.find('|').expect("a cursor marker");
let src = marked.replacen('|', "", 1);
let line_index = LineIndex::new(&src);
let position = line_index.byte_to_position(offset, PositionEncoding::Utf16);
compute_definition(&doc_uri(), &src, position, PositionEncoding::Utf16, lib)
}
fn single_def_at(marked: &str, lib: &impl PackageSource) -> Option<Location> {
let mut locations = def_at(marked, lib);
match locations.len() {
0 => None,
1 => Some(locations.remove(0)),
n => panic!("expected at most one definition site, got {n}"),
}
}
#[test]
fn local_variable_jumps_to_its_assignment() {
let loc =
single_def_at("function f()\n x = 1\n x|\nend", &TestLib::default()).unwrap();
assert_eq!(loc.uri, doc_uri());
assert_eq!(loc.range.start, Position::new(1, 4));
assert_eq!(loc.range.end, Position::new(1, 5));
}
#[test]
fn call_jumps_to_the_function_definition() {
let loc = single_def_at("greet(a) = a\ngreet|(1)", &TestLib::default()).unwrap();
assert_eq!(loc.uri, doc_uri());
assert_eq!(loc.range.start, Position::new(0, 0));
assert_eq!(loc.range.end, Position::new(0, 5));
}
#[test]
fn parameter_use_jumps_to_the_parameter() {
let loc = single_def_at("function f(abc)\n abc|\nend", &TestLib::default()).unwrap();
assert_eq!(loc.range.start, Position::new(0, 11));
assert_eq!(loc.range.end, Position::new(0, 14));
}
#[test]
fn unresolved_name_has_no_definition() {
assert!(def_at("nope|()", &TestLib::default()).is_empty());
}
#[test]
fn using_export_jumps_into_the_depot_source() {
let tmp = TempDir::new();
let entry = tmp.path.join("src").join("Greetings.jl");
fs::create_dir_all(entry.parent().unwrap()).unwrap();
fs::write(
&entry,
"module Greetings\nexport greet\ngreet(name) = name\nend\n",
)
.unwrap();
let pkg = harvest_package_named(&tmp.path, "Greetings");
let mut lib = TestLib::default();
lib.packages.insert("Greetings".to_string(), Arc::new(pkg));
lib.roots.insert("Greetings".to_string(), tmp.path.clone());
let loc = single_def_at("using Greetings\ngreet|(1)", &lib).unwrap();
assert_eq!(to_path(&loc.uri), Some(entry.clone()));
assert_eq!(loc.range.start, Position::new(2, 0));
assert_eq!(loc.range.end, Position::new(2, 5));
}
#[test]
fn library_without_a_known_root_has_no_definition() {
let tmp = TempDir::new();
let entry = tmp.path.join("src").join("Greetings.jl");
fs::create_dir_all(entry.parent().unwrap()).unwrap();
fs::write(
&entry,
"module Greetings\nexport greet\ngreet(x) = x\nend\n",
)
.unwrap();
let pkg = harvest_package_named(&tmp.path, "Greetings");
let mut lib = TestLib::default();
lib.packages.insert("Greetings".to_string(), Arc::new(pkg));
assert!(def_at("using Greetings\ngreet|(1)", &lib).is_empty());
}
#[test]
fn workspace_sibling_jumps_into_the_other_file() {
let tmp = TempDir::new();
let src = tmp.path.join("src");
fs::create_dir_all(&src).unwrap();
let bar = src.join("bar.jl");
fs::write(
src.join("MyPkg.jl"),
"module MyPkg\ninclude(\"bar.jl\")\nend\n",
)
.unwrap();
fs::write(&bar, "bar(x) = x\n").unwrap();
let pkg = Arc::new(harvest_package_named(&tmp.path, "MyPkg"));
let mut lib = TestLib::default();
lib.packages.insert("MyPkg".to_string(), Arc::clone(&pkg));
lib.roots.insert("MyPkg".to_string(), tmp.path.clone());
lib.workspace = Some((pkg, Vec::new()));
let loc = single_def_at("bar|(1)", &lib).unwrap();
assert_eq!(to_path(&loc.uri), Some(bar));
assert_eq!(loc.range.start, Position::new(0, 0));
assert_eq!(loc.range.end, Position::new(0, 3));
}
#[test]
fn workspace_tier_is_off_without_membership() {
let tmp = TempDir::new();
let src = tmp.path.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(
src.join("MyPkg.jl"),
"module MyPkg\ninclude(\"bar.jl\")\nend\n",
)
.unwrap();
fs::write(src.join("bar.jl"), "bar(x) = x\n").unwrap();
let pkg = Arc::new(harvest_package_named(&tmp.path, "MyPkg"));
let mut lib = TestLib::default();
lib.packages.insert("MyPkg".to_string(), pkg);
lib.roots.insert("MyPkg".to_string(), tmp.path.clone());
assert!(def_at("bar|(1)", &lib).is_empty());
}
#[test]
fn call_returns_every_local_method() {
let locs = def_at(
"f(x::Int) = 1\nf(x::String) = \"s\"\nf|(1)",
&TestLib::default(),
);
assert_eq!(locs.len(), 2, "{locs:?}");
assert_eq!(locs[0].range.start, Position::new(0, 0));
assert_eq!(locs[1].range.start, Position::new(1, 0));
}
#[test]
fn long_and_short_form_methods_are_both_found() {
let locs = def_at(
"function f(x)\n x\nend\nf(x, y) = x\nf|(1)",
&TestLib::default(),
);
assert_eq!(locs.len(), 2, "{locs:?}");
assert_eq!(locs[0].range.start, Position::new(0, 9));
assert_eq!(locs[1].range.start, Position::new(3, 0));
}
#[test]
fn cursor_on_the_first_method_returns_all_methods() {
let locs = def_at("f|(x::Int) = 1\nf(x::String) = \"s\"", &TestLib::default());
assert_eq!(locs.len(), 2, "{locs:?}");
}
#[test]
fn cursor_on_a_later_method_returns_all_methods() {
let locs = def_at("f(x::Int) = 1\nf|(x::String) = \"s\"", &TestLib::default());
assert_eq!(locs.len(), 2, "{locs:?}");
assert_eq!(locs[0].range.start, Position::new(0, 0));
assert_eq!(locs[1].range.start, Position::new(1, 0));
}
#[test]
fn variable_reassignment_keeps_a_single_definition() {
let loc = single_def_at(
"function g()\n x = 1\n x = 2\n x|\nend",
&TestLib::default(),
)
.unwrap();
assert_eq!(loc.range.start, Position::new(1, 4));
}
#[test]
fn nested_local_function_methods_are_all_found() {
let locs = def_at(
"function outer()\n g(x) = 1\n g(x, y) = 2\n g|(1)\nend",
&TestLib::default(),
);
assert_eq!(locs.len(), 2, "{locs:?}");
assert_eq!(locs[0].range.start, Position::new(1, 4));
assert_eq!(locs[1].range.start, Position::new(2, 4));
}
#[test]
fn using_export_returns_all_library_methods() {
let tmp = TempDir::new();
let entry = tmp.path.join("src").join("Greetings.jl");
fs::create_dir_all(entry.parent().unwrap()).unwrap();
fs::write(
&entry,
"module Greetings\nexport greet\ngreet(name) = name\ngreet(a, b) = a\nend\n",
)
.unwrap();
let pkg = harvest_package_named(&tmp.path, "Greetings");
let mut lib = TestLib::default();
lib.packages.insert("Greetings".to_string(), Arc::new(pkg));
lib.roots.insert("Greetings".to_string(), tmp.path.clone());
let locs = def_at("using Greetings\ngreet|(1)", &lib);
assert_eq!(locs.len(), 2, "{locs:?}");
assert!(locs.iter().all(|l| to_path(&l.uri) == Some(entry.clone())));
assert_eq!(locs[0].range.start, Position::new(2, 0));
assert_eq!(locs[1].range.start, Position::new(3, 0));
}
#[test]
fn workspace_sibling_methods_span_member_files() {
let tmp = TempDir::new();
let src = tmp.path.join("src");
fs::create_dir_all(&src).unwrap();
let bar = src.join("bar.jl");
let baz = src.join("baz.jl");
fs::write(
src.join("MyPkg.jl"),
"module MyPkg\ninclude(\"bar.jl\")\ninclude(\"baz.jl\")\nend\n",
)
.unwrap();
fs::write(&bar, "bar(x) = x\n").unwrap();
fs::write(&baz, "bar(x, y) = x\n").unwrap();
let pkg = Arc::new(harvest_package_named(&tmp.path, "MyPkg"));
let mut lib = TestLib::default();
lib.packages.insert("MyPkg".to_string(), Arc::clone(&pkg));
lib.roots.insert("MyPkg".to_string(), tmp.path.clone());
lib.workspace = Some((pkg, Vec::new()));
let locs = def_at("bar|(1)", &lib);
assert_eq!(locs.len(), 2, "{locs:?}");
assert_eq!(to_path(&locs[0].uri), Some(bar));
assert_eq!(to_path(&locs[1].uri), Some(baz));
}
#[test]
fn cross_file_definitions_span_member_files() {
use crate::lsp::cross_file::test_support::{member_path, workspace_db};
use crate::text::PositionEncoding::Utf16;
let a_text = "greet(a) = a\ngreet(a, b) = a\n";
let b_text = "callit() = greet(2)\n";
let (db, _) = workspace_db(&["greet"], &[("a.jl", a_text), ("b.jl", b_text)]);
let snapshot = db.snapshot();
let a_uri = crate::lsp::uri::from_path(&member_path("a.jl")).unwrap();
let b_path = member_path("b.jl");
let b_uri = crate::lsp::uri::from_path(&b_path).unwrap();
let locs = definition_via_db(
&snapshot,
&b_uri,
&b_path,
b_text,
Position::new(0, 11),
Utf16,
);
assert_eq!(locs.len(), 2, "{locs:?}");
assert!(locs.iter().all(|l| l.uri == a_uri));
assert!(locs.iter().any(|l| l.range.start == Position::new(0, 0)));
assert!(locs.iter().any(|l| l.range.start == Position::new(1, 0)));
let a_path = member_path("a.jl");
let locs = definition_via_db(
&snapshot,
&a_uri,
&a_path,
a_text,
Position::new(0, 0),
Utf16,
);
assert_eq!(locs.len(), 2, "{locs:?}");
}
#[test]
fn cross_file_gate_skips_non_function_symbols() {
use crate::lsp::cross_file::test_support::{member_path, workspace_db};
use crate::text::PositionEncoding::Utf16;
let a_text = "x = 1\nx = 2\nx\n";
let (db, _) = workspace_db(&["greet"], &[("a.jl", a_text)]);
let snapshot = db.snapshot();
let a_path = member_path("a.jl");
let a_uri = crate::lsp::uri::from_path(&a_path).unwrap();
let locs = definition_via_db(
&snapshot,
&a_uri,
&a_path,
a_text,
Position::new(2, 0),
Utf16,
);
assert_eq!(locs.len(), 1, "{locs:?}");
assert_eq!(locs[0].range.start, Position::new(0, 0));
}
}