11use std:: collections:: HashMap ;
22
3+ use biome_rowan:: TextSize ;
4+ use oak_package_metadata:: namespace:: Namespace ;
5+ use oak_semantic:: semantic_index:: SemanticCall ;
36use oak_semantic:: semantic_index:: SemanticCallKind ;
7+ use oak_semantic:: ScopeId ;
48
59use crate :: Db ;
610use 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`).
3338const DEFAULT_SEARCH_PATH : [ & str ; 7 ] = [
@@ -42,94 +47,213 @@ const DEFAULT_SEARCH_PATH: [&str; 7] = [
4247
4348#[ salsa:: tracked]
4449impl 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
129253fn 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