diff --git a/crates/pine-core/src/library.rs b/crates/pine-core/src/library.rs index b55983e..f7e303b 100644 --- a/crates/pine-core/src/library.rs +++ b/crates/pine-core/src/library.rs @@ -62,6 +62,12 @@ impl DirLoader { self.loaded.borrow().get(path).cloned() } + /// The on-disk file `path` resolves to, without reading it — for locating a + /// library file (e.g. editor go-to-definition). + pub fn resolve_path(&self, path: &str) -> Option { + self.candidates(path).into_iter().find(|c| c.is_file()) + } + /// Every file `path` could name, most specific first. fn candidates(&self, path: &str) -> Vec { let trimmed = path.trim_matches('/'); diff --git a/crates/pine-lsp/src/lib.rs b/crates/pine-lsp/src/lib.rs index ee91bc2..5c1c40e 100644 --- a/crates/pine-lsp/src/lib.rs +++ b/crates/pine-lsp/src/lib.rs @@ -6,7 +6,8 @@ use pine_lang::builtins::{register_namespace_objects, DefaultPineOutput}; use pine_lang::core::PineVersion; use pine_lang::diagnostics::{Diagnostic as PineDiagnostic, Severity}; use pine_lang::interpreter::{BuiltinSignature, Value}; -use pine_lang::sema::{Symbol, SymbolId, SymbolKind, SymbolTable}; +use pine_lang::sema::{FileId, Symbol, SymbolId, SymbolKind, SymbolTable}; +use pine_lang::DirLoader; use tower_lsp_server::lsp_types::*; use tower_lsp_server::{jsonrpc, Client, LanguageServer, LspService, Server, UriExt}; @@ -184,22 +185,22 @@ impl LanguageServer for Backend { ) -> jsonrpc::Result> { let at = params.text_document_position_params; let uri = at.text_document.uri; - let decl = { + let location = { let documents = self.documents.lock().unwrap(); documents.get(&uri).and_then(|doc| { let symbols = doc.symbols.as_ref()?; let id = symbol_at(symbols, &doc.text, at.position)?; let (file, line, column) = symbols.declaration_location(id)?; - // Cross-file (library) declarations are not resolved yet. - (file == SymbolTable::MAIN).then(|| Position::new(line - 1, column - 1)) + let width = symbols.symbol(id).name.chars().count() as u32; + Some(location_at( + file_uri(&uri, symbols, file)?, + line, + column, + width, + )) }) }; - Ok(decl.map(|decl| { - GotoDefinitionResponse::Scalar(Location { - uri, - range: Range::new(decl, decl), - }) - })) + Ok(location.map(GotoDefinitionResponse::Scalar)) } async fn references(&self, params: ReferenceParams) -> jsonrpc::Result>> { @@ -211,23 +212,20 @@ impl LanguageServer for Backend { let symbols = doc.symbols.as_ref()?; let id = symbol_at(symbols, &doc.text, at.position)?; let width = symbols.symbol(id).name.chars().count() as u32; - let mut sites: Vec<(u32, u32)> = Vec::new(); + let mut sites: Vec<(FileId, u32, u32)> = Vec::new(); if params.context.include_declaration { - if let Some((file, line, column)) = symbols.declaration_location(id) { - if file == SymbolTable::MAIN { - sites.push((line, column)); - } - } - } - for (file, line, column) in symbols.references(id) { - if file == SymbolTable::MAIN { - sites.push((line, column)); + if let Some(decl) = symbols.declaration_location(id) { + sites.push(decl); } } + sites.extend(symbols.references(id)); Some( sites .into_iter() - .map(|(line, column)| main_location(&uri, line, column, width)) + .filter_map(|(file, line, column)| { + let uri = file_uri(&uri, symbols, file)?; + Some(location_at(uri, line, column, width)) + }) .collect::>(), ) }) @@ -272,13 +270,24 @@ fn symbol_at( symbols.symbol_at(SymbolTable::MAIN, position.line + 1, start as u32 + 1) } -/// A location in the main document spanning `width` characters from a 1-based -/// `(line, column)`. -fn main_location(uri: &Uri, line: u32, column: u32, width: u32) -> Location { +/// The URI for a symbol-table `file`: the request document for the main file, +/// or the resolved library file otherwise. `None` when a library file can't be +/// located on disk (e.g. an in-memory loader). +fn file_uri(request: &Uri, symbols: &SymbolTable, file: FileId) -> Option { + if file == SymbolTable::MAIN { + return Some(request.clone()); + } + let dir = uri_dir(request)?; + let path = DirLoader::new(vec![dir]).resolve_path(symbols.file_path(file))?; + Uri::from_file_path(path) +} + +/// A location spanning `width` characters from a 1-based `(line, column)`. +fn location_at(uri: Uri, line: u32, column: u32, width: u32) -> Location { let start = Position::new(line - 1, column - 1); let end = Position::new(line - 1, column - 1 + width); Location { - uri: uri.clone(), + uri, range: Range::new(start, end), } } diff --git a/editors/vscode/src/test/suite/extension.test.ts b/editors/vscode/src/test/suite/extension.test.ts index bc6501d..e130de7 100644 --- a/editors/vscode/src/test/suite/extension.test.ts +++ b/editors/vscode/src/test/suite/extension.test.ts @@ -102,6 +102,23 @@ suite("pinecone language server", () => { assert.strictEqual(locations[0].range.start.line, 2); // `double(x) =>` }); + test("goes to a definition in an imported library", async () => { + const uri = fixture("imports.pine"); + await open(uri); + + // `add` in the call `lib.add(1, 2)` on line 6. + const locations = await vscode.commands.executeCommand( + "vscode.executeDefinitionProvider", + uri, + new vscode.Position(5, 8) + ); + assert.ok(locations && locations.length > 0, "expected a definition"); + const loc = locations[0]; + // It resolves into the library file, not the main document. + assert.ok(loc.uri.path.endsWith("mylib.pine"), loc.uri.toString()); + assert.strictEqual(loc.range.start.line, 3); // `export add(a, b) =>` + }); + test("finds references to a function", async () => { const uri = fixture("symbols.pine"); await open(uri); diff --git a/editors/vscode/testFixture/imports.pine b/editors/vscode/testFixture/imports.pine new file mode 100644 index 0000000..4c73d70 --- /dev/null +++ b/editors/vscode/testFixture/imports.pine @@ -0,0 +1,7 @@ +//@version=6 +indicator("imports") + +import mylib as lib + +x = lib.add(1, 2) +plot(x) diff --git a/editors/vscode/testFixture/mylib.pine b/editors/vscode/testFixture/mylib.pine new file mode 100644 index 0000000..011fb90 --- /dev/null +++ b/editors/vscode/testFixture/mylib.pine @@ -0,0 +1,4 @@ +//@version=6 +library("mylib") + +export add(a, b) => a + b