Skip to content

Commit 6341e01

Browse files
authored
Merge pull request #872 from posit-dev/feature/static-imports-package
Add symbols imported in packages to workspace
2 parents 1e3b986 + a974de9 commit 6341e01

File tree

8 files changed

+334
-75
lines changed

8 files changed

+334
-75
lines changed

crates/ark/src/lsp/diagnostics.rs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use crate::lsp::encoding::convert_tree_sitter_range_to_lsp_range;
3030
use crate::lsp::indexer;
3131
use crate::lsp::inputs::library::Library;
3232
use crate::lsp::inputs::package::Package;
33+
use crate::lsp::inputs::source_root::SourceRoot;
3334
use crate::lsp::state::WorldState;
3435
use crate::lsp::traits::node::NodeExt;
3536
use crate::lsp::traits::rope::RopeExt;
@@ -62,6 +63,9 @@ pub struct DiagnosticContext<'a> {
6263
// The set of packages that are currently installed.
6364
pub installed_packages: HashSet<String>,
6465

66+
/// Reference to source root, if any.
67+
pub root: &'a Option<SourceRoot>,
68+
6569
/// Reference to the library for looking up package exports.
6670
pub library: &'a Library,
6771

@@ -83,13 +87,14 @@ impl Default for DiagnosticsConfig {
8387
}
8488

8589
impl<'a> DiagnosticContext<'a> {
86-
pub fn new(contents: &'a Rope, library: &'a Library) -> Self {
90+
pub fn new(contents: &'a Rope, root: &'a Option<SourceRoot>, library: &'a Library) -> Self {
8791
Self {
8892
contents,
8993
document_symbols: Vec::new(),
9094
session_symbols: HashSet::new(),
9195
workspace_symbols: HashSet::new(),
9296
installed_packages: HashSet::new(),
97+
root,
9398
library,
9499
library_symbols: BTreeMap::new(),
95100
in_formula: false,
@@ -130,7 +135,11 @@ impl<'a> DiagnosticContext<'a> {
130135
}
131136
}
132137

133-
pub(crate) fn generate_diagnostics(doc: Document, state: WorldState) -> Vec<Diagnostic> {
138+
pub(crate) fn generate_diagnostics(
139+
doc: Document,
140+
state: WorldState,
141+
testthat: bool,
142+
) -> Vec<Diagnostic> {
134143
let mut diagnostics = Vec::new();
135144

136145
if !state.config.diagnostics.enable {
@@ -144,7 +153,7 @@ pub(crate) fn generate_diagnostics(doc: Document, state: WorldState) -> Vec<Diag
144153
return diagnostics;
145154
}
146155

147-
let mut context = DiagnosticContext::new(&doc.contents, &state.library);
156+
let mut context = DiagnosticContext::new(&doc.contents, &state.root, &state.library);
148157

149158
// Add a 'root' context for the document.
150159
context.document_symbols.push(HashMap::new());
@@ -154,9 +163,45 @@ pub(crate) fn generate_diagnostics(doc: Document, state: WorldState) -> Vec<Diag
154163
indexer::IndexEntryData::Function { name, arguments: _ } => {
155164
context.workspace_symbols.insert(name.to_string());
156165
},
166+
indexer::IndexEntryData::Variable { name } => {
167+
context.workspace_symbols.insert(name.to_string());
168+
},
157169
_ => {},
158170
});
159171

172+
// If this is a package, add imported symbols to workspace
173+
if let Some(SourceRoot::Package(root)) = &state.root {
174+
// Add symbols from `importFrom()` directives
175+
for import in &root.namespace.imports {
176+
context.workspace_symbols.insert(import.clone());
177+
}
178+
179+
// Add symbols from `import()` directives
180+
for package_import in &root.namespace.package_imports {
181+
if let Some(pkg) = state.library.get(package_import) {
182+
for export in &pkg.namespace.exports {
183+
context.workspace_symbols.insert(export.clone());
184+
}
185+
}
186+
}
187+
}
188+
189+
// Simple workaround to include testthat exports in test files. I think the
190+
// general principle would be that (a) files in `tests/testthat/` include
191+
// `testthat.R` as a preamble (note that people modify that file e.g. to add
192+
// more `library()` calls), and (b) all helper files are included in a
193+
// test-specific workspace (which is effectively the case currently as we
194+
// don't special-case how workspace inclusion works for packages). We might
195+
// want to provide a mechanism for test packages to declare this sort of
196+
// test files setup.
197+
if testthat {
198+
if let Some(pkg) = state.library.get("testthat") {
199+
for export in &pkg.namespace.exports {
200+
context.workspace_symbols.insert(export.clone());
201+
}
202+
}
203+
}
204+
160205
// Add per-environment session symbols
161206
for scope in state.console_scopes.iter() {
162207
for name in scope.iter() {
@@ -1097,10 +1142,10 @@ mod tests {
10971142

10981143
use harp::eval::RParseEvalOptions;
10991144
use once_cell::sync::Lazy;
1145+
use tower_lsp::lsp_types;
11001146
use tower_lsp::lsp_types::Position;
11011147

11021148
use crate::interface::console_inputs;
1103-
use crate::lsp::diagnostics::generate_diagnostics;
11041149
use crate::lsp::documents::Document;
11051150
use crate::lsp::inputs::library::Library;
11061151
use crate::lsp::inputs::package::Package;
@@ -1113,6 +1158,10 @@ mod tests {
11131158
// Default state that includes installed packages and default scopes.
11141159
static DEFAULT_STATE: Lazy<WorldState> = Lazy::new(|| current_state());
11151160

1161+
fn generate_diagnostics(doc: Document, state: WorldState) -> Vec<lsp_types::Diagnostic> {
1162+
super::generate_diagnostics(doc, state, false)
1163+
}
1164+
11161165
fn current_state() -> WorldState {
11171166
let inputs = console_inputs().unwrap();
11181167

crates/ark/src/lsp/diagnostics_syntax.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ mod tests {
314314
fn text_diagnostics(text: &str) -> Vec<Diagnostic> {
315315
let document = Document::new(text, None);
316316
let library = Library::default();
317-
let context = DiagnosticContext::new(&document.contents, &library);
317+
let context = DiagnosticContext::new(&document.contents, &None, &library);
318318
let diagnostics = syntax_diagnostics(document.ast.root_node(), &context).unwrap();
319319
diagnostics
320320
}

crates/ark/src/lsp/inputs/library.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ impl Library {
6868

6969
fn load_package(&self, name: &str) -> anyhow::Result<Option<Package>> {
7070
for lib_path in self.library_paths.iter() {
71-
match Package::load(&lib_path, name)? {
71+
match Package::load_from_library(&lib_path, name)? {
7272
Some(pkg) => return Ok(Some(pkg)),
7373
None => (),
7474
}

crates/ark/src/lsp/inputs/package.rs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,12 @@ pub struct Package {
2323
}
2424

2525
impl Package {
26-
/// Attempts to load a package from the given path and name.
27-
pub fn load(lib_path: &std::path::Path, name: &str) -> anyhow::Result<Option<Self>> {
28-
let package_path = lib_path.join(name);
29-
26+
/// Load a package from a given path.
27+
pub fn load_from_folder(package_path: &std::path::Path) -> anyhow::Result<Option<Self>> {
3028
let description_path = package_path.join("DESCRIPTION");
3129
let namespace_path = package_path.join("NAMESPACE");
3230

33-
// Only consider libraries that have a folder named after the
34-
// requested package and that contains a description file
31+
// Only consider directories that contain a description file
3532
if !description_path.is_file() {
3633
return Ok(None);
3734
}
@@ -41,24 +38,42 @@ impl Package {
4138
let description_contents = fs::read_to_string(&description_path)?;
4239
let description = Description::parse(&description_contents)?;
4340

44-
if description.name != name {
45-
return Err(anyhow::anyhow!(
46-
"`Package` field in `DESCRIPTION` doesn't match folder name '{name}'"
47-
));
48-
}
49-
5041
let namespace = if namespace_path.is_file() {
5142
let namespace_contents = fs::read_to_string(&namespace_path)?;
5243
Namespace::parse(&namespace_contents)?
5344
} else {
54-
tracing::info!("Package `{name}` doesn't contain a NAMESPACE file, using defaults");
45+
tracing::info!(
46+
"Package `{name}` doesn't contain a NAMESPACE file, using defaults",
47+
name = description.name
48+
);
5549
Namespace::default()
5650
};
5751

5852
Ok(Some(Package {
59-
path: package_path,
53+
path: package_path.to_path_buf(),
6054
description,
6155
namespace,
6256
}))
6357
}
58+
59+
/// Load a package from the given library path and name.
60+
pub fn load_from_library(
61+
lib_path: &std::path::Path,
62+
name: &str,
63+
) -> anyhow::Result<Option<Self>> {
64+
let package_path = lib_path.join(name);
65+
66+
// For library packages, ensure the invariant that the package name
67+
// matches the folder name
68+
if let Some(pkg) = Self::load_from_folder(&package_path)? {
69+
if pkg.description.name != name {
70+
return Err(anyhow::anyhow!(
71+
"`Package` field in `DESCRIPTION` doesn't match folder name '{name}'"
72+
));
73+
}
74+
Ok(Some(pkg))
75+
} else {
76+
Ok(None)
77+
}
78+
}
6479
}

0 commit comments

Comments
 (0)