Skip to content

Commit e4d73a4

Browse files
authored
Add offset-based queries (#1226)
Branched from #1225 Progress towards #1212 Adds `File::resolve_at()` and `File::imports_at()`. To be used mainly to handle LSP requests that need information at a particular cursor position. These methods are not tracked since they take an offset, to avoid growing the cache unnecessarily.
2 parents 52139d4 + 9ba33d5 commit e4d73a4

11 files changed

Lines changed: 939 additions & 105 deletions

File tree

crates/oak_db/src/file_imports.rs

Lines changed: 182 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use std::collections::HashMap;
22

3+
use biome_rowan::TextSize;
4+
use oak_package_metadata::namespace::Namespace;
5+
use oak_semantic::semantic_index::SemanticCall;
36
use oak_semantic::semantic_index::SemanticCallKind;
7+
use oak_semantic::ScopeId;
48

59
use crate::Db;
610
use crate::File;
@@ -18,16 +22,17 @@ pub enum ImportLayer {
1822
/// NAMESPACE `importFrom(pkg, name)` entries. Maps each imported
1923
/// symbol to its source package by name. Translation to `Package`
2024
/// happens at resolution time.
21-
PackageImports(HashMap<String, String>),
22-
/// An attached package's exports. Missing packages are filtered
23-
/// out by `imports`.
24-
PackageExports(Package),
25+
From(HashMap<String, String>),
26+
/// A whole package made available, either via NAMESPACE `import(pkg)`,
27+
/// `library()` / `require()` calls, or the default R search path.
28+
/// Missing packages are filtered out by `imports`.
29+
Package(Package),
2530
}
2631

2732
/// The default R search path: packages always attached at startup, in
2833
/// the order R's `search()` reports them (last attached = searched
2934
/// first, so `stats` is highest-priority and `base` is lowest).
30-
/// Materialised as `PackageExports` layers; packages absent from the
35+
/// Materialised as `Package` layers; packages absent from the
3136
/// workspace and library roots drop out (the LSP fills them in later
3237
/// via `oak_sources`).
3338
const DEFAULT_SEARCH_PATH: [&str; 7] = [
@@ -42,94 +47,213 @@ const DEFAULT_SEARCH_PATH: [&str; 7] = [
4247

4348
#[salsa::tracked]
4449
impl File {
45-
/// The import layers that exist for this file, in priority order.
50+
/// The import layers visible to this file at end-of-file, in R's lookup
51+
/// (LIFO) priority order. Symbols that don't have local bindings (are
52+
/// unbound in the file's semantic index) can be resolved against these
53+
/// imports.
4654
///
47-
/// Offset-independent and stable across cursor moves. Recomputed
48-
/// only when the file's package membership, NAMESPACE, or this
49-
/// file's semantic calls actually change.
55+
/// `library()` calls further down the file come earlier in the returned
56+
/// `Vec`, and collation files later in the package come earlier too. The
57+
/// first hit in a forward search then matches R's runtime semantics (last
58+
/// attached / latest sourced wins).
59+
///
60+
/// Offset-independent and stable across cursor moves. Recomputed only when
61+
/// the file's package membership, NAMESPACE, or this file's semantic calls
62+
/// actually change. See [`File::imports_at`] for the offset-narrowed subset
63+
/// of imports.
5064
#[salsa::tracked(returns(ref))]
5165
pub fn imports(self, db: &dyn Db) -> Vec<ImportLayer> {
5266
match self.package(db) {
5367
Some(package) => package_imports(self, db, package),
5468
None => script_imports(self, db),
5569
}
5670
}
57-
}
5871

59-
fn script_imports(file: File, db: &dyn Db) -> Vec<ImportLayer> {
60-
let mut layers = Vec::new();
61-
let index = file.semantic_index(db);
72+
/// Import layers visible at an `offset` in a file:
73+
///
74+
/// - **Cursor in lazy context**: returns the full lazy view. Lazy
75+
/// contexts like functions are treated as if they run after the
76+
/// file is fully sourced (over-approximation). Any `library()` /
77+
/// collation entry is potentially visible regardless of where it
78+
/// appears relative to the cursor.
79+
///
80+
/// - **Top-level cursor (script)**: only `library()` calls that
81+
/// have occurred before `offset`. Most recently attached comes
82+
/// first.
83+
///
84+
/// - **Top-level cursor (package)**: only collation predecessors
85+
/// of this file. Most recently sourced predecessor comes
86+
/// first. The package imports and base namespace come last.
87+
///
88+
/// Plain method rather than `#[salsa::tracked]`. Tracking would key the
89+
/// cache on `(self, offset)`, creating one entry per cursor position.
90+
/// Skipping the cache is fine because the body just reads already-cached
91+
/// subqueries (`imports`, `semantic_index`) and applies an O(n) filter.
92+
pub fn imports_at(self, db: &dyn Db, offset: TextSize) -> Vec<ImportLayer> {
93+
let index = self.semantic_index(db);
94+
let file_scope = ScopeId::from(0);
95+
let (cursor_scope, _) = index.scope_at(offset);
96+
97+
// Cursor in lazy context. EOF view, same as `imports()`.
98+
if cursor_scope != file_scope {
99+
return self.imports(db).clone();
100+
}
62101

63-
// `library()` / `require()` calls in source order. We keep all of
64-
// them; callers that need offset-based narrowing filter at query
65-
// time.
66-
for call in index.semantic_calls() {
67-
match call.kind() {
68-
SemanticCallKind::Attach { package: name } => {
69-
if let Some(package) = db.package_by_name(name) {
70-
layers.push(ImportLayer::PackageExports(package));
71-
}
72-
},
73-
SemanticCallKind::Source { .. } => {
74-
// `source()` injects into local scope, not the search path,
75-
// so it's not a scope-chain layer.
76-
},
102+
// Top-level cursor: sequential narrowing.
103+
match self.package(db) {
104+
Some(package) => narrow_package_top_level(self, db, package),
105+
None => narrow_script_top_level(self, db, offset),
77106
}
78107
}
108+
}
79109

80-
// Default search path (R always attaches these at startup).
81-
extend_with_default_search_path(db, &mut layers);
110+
fn narrow_script_top_level(file: File, db: &dyn Db, offset: TextSize) -> Vec<ImportLayer> {
111+
let index = file.semantic_index(db);
112+
let file_scope = ScopeId::from(0);
82113

114+
// Keep file-scope `library()` calls that have run by `offset`, in
115+
// LIFO order (latest-attached first).
116+
let mut layers: Vec<_> = index
117+
.semantic_calls()
118+
.iter()
119+
.rev()
120+
.filter(|call| call.scope() == file_scope && call.offset() < offset)
121+
.filter_map(|call| attach_layer(db, call))
122+
.collect();
123+
124+
extend_with_default_search_path(db, &mut layers);
83125
layers
84126
}
85127

86-
fn package_imports(_file: File, db: &dyn Db, package: Package) -> Vec<ImportLayer> {
128+
fn narrow_package_top_level(file: File, db: &dyn Db, package: Package) -> Vec<ImportLayer> {
129+
let files = package.files(db);
130+
let Some(file_pos) = files.iter().position(|f| *f == file) else {
131+
// File claims membership but isn't in the package's `files`.
132+
// Shouldn't happen.
133+
log::warn!(
134+
"File {file} has package back-pointer to {package} but is not in its files",
135+
file = file.url(db),
136+
package = package.name(db),
137+
);
138+
return file.imports(db).clone();
139+
};
140+
87141
let mut layers = Vec::new();
142+
143+
// Predecessors only, in LIFO order (latest-sourced first).
144+
layers.extend(
145+
files[..file_pos]
146+
.iter()
147+
.rev()
148+
.copied()
149+
.map(ImportLayer::File),
150+
);
151+
88152
let namespace = package.namespace(db);
153+
extend_with_namespace_imports(namespace, &mut layers);
154+
extend_with_namespace_package_imports(db, namespace, &mut layers);
155+
156+
extend_with_base(db, &mut layers);
157+
layers
158+
}
89159

90-
// NAMESPACE `importFrom(pkg, name)` directives. Collect them into
91-
// a single layer that maps each imported symbol name to its source
92-
// package.
93-
if !namespace.imports.is_empty() {
94-
let map: HashMap<String, String> = namespace
95-
.imports
160+
fn attach_layer(db: &dyn Db, call: &SemanticCall) -> Option<ImportLayer> {
161+
match call.kind() {
162+
SemanticCallKind::Attach { package: name } => {
163+
db.package_by_name(name).map(ImportLayer::Package)
164+
},
165+
SemanticCallKind::Source { .. } => {
166+
// `source()` injects into local scope, not the search path,
167+
// so it's not a scope-chain layer.
168+
None
169+
},
170+
}
171+
}
172+
173+
fn script_imports(file: File, db: &dyn Db) -> Vec<ImportLayer> {
174+
let index = file.semantic_index(db);
175+
176+
// Reverse: R searches LIFO, so latest-attached comes first.
177+
let mut layers: Vec<_> = index
178+
.semantic_calls()
179+
.iter()
180+
.rev()
181+
.filter_map(|call| attach_layer(db, call))
182+
.collect();
183+
184+
extend_with_default_search_path(db, &mut layers);
185+
layers
186+
}
187+
188+
fn package_imports(file: File, db: &dyn Db, package: Package) -> Vec<ImportLayer> {
189+
let mut layers = Vec::new();
190+
191+
// All package files except self, in LIFO order (latest-sourced first).
192+
// Self is excluded: a file's own top-level bindings come from `exports`,
193+
// and including self here would create a cycle in `resolve` for unbound
194+
// names.
195+
//
196+
// TODO(scan): once `oak_scan` orders `Package.files` by the DESCRIPTION
197+
// `Collate` field, this iteration becomes collation-ordered directly.
198+
// Today `Package.files` is whatever order the scanner produces.
199+
layers.extend(
200+
package
201+
.files(db)
96202
.iter()
97-
.map(|imp| (imp.name.clone(), imp.package.clone()))
98-
.collect();
99-
layers.push(ImportLayer::PackageImports(map));
203+
.rev()
204+
.copied()
205+
.filter(|f| *f != file)
206+
.map(ImportLayer::File),
207+
);
208+
209+
let namespace = package.namespace(db);
210+
extend_with_namespace_imports(namespace, &mut layers);
211+
extend_with_namespace_package_imports(db, namespace, &mut layers);
212+
213+
extend_with_base(db, &mut layers);
214+
layers
215+
}
216+
217+
/// Push the `From` layer if the namespace has any `importFrom` entries.
218+
/// Collects them into a single map from name to source package.
219+
fn extend_with_namespace_imports(namespace: &Namespace, layers: &mut Vec<ImportLayer>) {
220+
if namespace.imports.is_empty() {
221+
return;
100222
}
223+
let map: HashMap<String, String> = namespace
224+
.imports
225+
.iter()
226+
.map(|imp| (imp.name.clone(), imp.package.clone()))
227+
.collect();
228+
layers.push(ImportLayer::From(map));
229+
}
101230

102-
// NAMESPACE `import(pkg)` directives (bulk package imports).
231+
/// Push one `Package` layer per `import(pkg)` directive in the namespace
232+
/// (bulk package imports). Missing packages are silently dropped.
233+
fn extend_with_namespace_package_imports(
234+
db: &dyn Db,
235+
namespace: &Namespace,
236+
layers: &mut Vec<ImportLayer>,
237+
) {
103238
for pkg_name in &namespace.package_imports {
104239
if let Some(pkg) = db.package_by_name(pkg_name) {
105-
layers.push(ImportLayer::PackageExports(pkg));
240+
layers.push(ImportLayer::Package(pkg));
106241
}
107242
}
243+
}
108244

109-
// All files in the package, in `Package.files` order. Query-time
110-
// narrowing filters to predecessors of a given file (R's collation
111-
// order) when only those are visible.
112-
//
113-
// TODO(scan): once `oak_scan` orders `Package.files` by the
114-
// DESCRIPTION `Collate` field, this iteration becomes
115-
// collation-ordered directly. Today `Package.files` is whatever
116-
// order the scanner produces.
117-
for &pkg_file in package.files(db) {
118-
layers.push(ImportLayer::File(pkg_file));
119-
}
120-
121-
// `base` is always implicitly available inside a package.
245+
/// Push the `base` package as a `Package` layer. `base` is always
246+
/// implicitly available inside a package.
247+
fn extend_with_base(db: &dyn Db, layers: &mut Vec<ImportLayer>) {
122248
if let Some(base) = db.package_by_name("base") {
123-
layers.push(ImportLayer::PackageExports(base));
249+
layers.push(ImportLayer::Package(base));
124250
}
125-
126-
layers
127251
}
128252

129253
fn extend_with_default_search_path(db: &dyn Db, layers: &mut Vec<ImportLayer>) {
130254
for pkg_name in DEFAULT_SEARCH_PATH {
131255
if let Some(pkg) = db.package_by_name(pkg_name) {
132-
layers.push(ImportLayer::PackageExports(pkg));
256+
layers.push(ImportLayer::Package(pkg));
133257
}
134258
}
135259
}

0 commit comments

Comments
 (0)