Skip to content

Commit dd8dbf1

Browse files
committed
lsp is fast on OA project
1 parent e645286 commit dd8dbf1

7 files changed

Lines changed: 745 additions & 232 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Thumbs.db
2424

2525

2626
# lsp vscode client
27-
2827
/editors/code/node_modules
2928
/editors/code/out
29+
/editors/code/.vscode-test
30+
/editors/code/package-lock.json

src/build/cache.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ impl ModuleCache {
123123
Self::default()
124124
}
125125

126+
/// Returns true if the cache has any module entries (i.e. a prior build populated it).
127+
pub fn has_entries(&self) -> bool {
128+
!self.entries.is_empty()
129+
}
130+
126131
/// Compute a content hash for a source string.
127132
pub fn content_hash(source: &str) -> u64 {
128133
let mut hasher = std::collections::hash_map::DefaultHasher::new();

src/js_ffi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ pub fn validate_foreign_module(
326326
.cloned()
327327
.collect();
328328

329-
let unused: Vec<String> = info
329+
let _unused: Vec<String> = info
330330
.es_exports
331331
.iter()
332332
.filter(|name| !import_set.contains(name.as_str()))

src/lsp/handlers/completion.rs

Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ impl Backend {
5353
// Find insert position for new imports (after last import, or after module header)
5454
let import_insert_line = find_import_insert_line(&source, &module);
5555

56-
let registry = self.registry.read().await;
56+
let comp_index = self.completion_index.read().await;
5757
let mut items = Vec::new();
5858
let mut seen = HashSet::new();
5959

@@ -85,52 +85,53 @@ impl Backend {
8585
}
8686

8787
// 2. Already-imported names (higher priority than unimported)
88-
// 3. All exported values from all modules in the registry
89-
for (mod_path, mod_exports) in registry.iter_all() {
90-
let mod_name = interner::resolve_module_name(mod_path);
91-
if mod_name == current_module_name {
88+
// 3. All exported names from all modules via lightweight completion index
89+
for (mod_name, mod_entries) in &comp_index.entries {
90+
if mod_name == &current_module_name {
9291
continue;
9392
}
9493

95-
for (qi, scheme) in &mod_exports.values {
96-
let name = match interner::resolve(qi.name) {
97-
Some(n) => n.to_string(),
98-
None => continue,
99-
};
100-
if !name.starts_with(&prefix) {
94+
for entry in mod_entries {
95+
if !entry.name.starts_with(&prefix) {
10196
continue;
10297
}
103-
if seen.contains(&name) {
98+
if seen.contains(&entry.name) {
10499
continue;
105100
}
106-
seen.insert(name.clone());
101+
seen.insert(entry.name.clone());
107102

108-
let type_str = format!("{}", scheme.ty);
109-
let is_imported = already_imported.contains(&name);
110-
let is_constructor = name.starts_with(|c: char| c.is_uppercase());
103+
let is_imported = already_imported.contains(&entry.name);
104+
let is_constructor = matches!(entry.kind, crate::lsp::CompletionEntryKind::Constructor);
105+
106+
let kind = match entry.kind {
107+
crate::lsp::CompletionEntryKind::Value => CompletionItemKind::FUNCTION,
108+
crate::lsp::CompletionEntryKind::Constructor => CompletionItemKind::CONSTRUCTOR,
109+
crate::lsp::CompletionEntryKind::Type => CompletionItemKind::CLASS,
110+
crate::lsp::CompletionEntryKind::Class => CompletionItemKind::INTERFACE,
111+
};
111112

112-
let kind = if is_constructor {
113-
CompletionItemKind::CONSTRUCTOR
113+
let detail = if entry.type_string.is_empty() {
114+
Some(mod_name.clone())
114115
} else {
115-
CompletionItemKind::FUNCTION
116+
Some(format!("{mod_name} :: {}", entry.type_string))
116117
};
117118

118119
// Imported items sort before unimported
119120
let sort_prefix = if is_imported { "1" } else { "2" };
120121

121122
let mut item = CompletionItem {
122-
label: name.clone(),
123+
label: entry.name.clone(),
123124
kind: Some(kind),
124-
detail: Some(format!("{mod_name} :: {type_str}")),
125+
detail,
125126
sort_text: Some(format!("{sort_prefix}{}", items.len())),
126127
..Default::default()
127128
};
128129

129130
// Auto-import: add additional_text_edits if not already imported
130131
if !is_imported {
131132
if let Some(edit) = build_import_edit(
132-
&mod_name,
133-
&name,
133+
mod_name,
134+
&entry.name,
134135
is_constructor,
135136
&module,
136137
&source,
@@ -142,29 +143,8 @@ impl Backend {
142143

143144
items.push(item);
144145
}
145-
146-
// Also add type constructors
147-
for (type_qi, ctor_names) in &mod_exports.data_constructors {
148-
for ctor_qi in ctor_names {
149-
let ctor_name = match interner::resolve(ctor_qi.name) {
150-
Some(n) => n.to_string(),
151-
None => continue,
152-
};
153-
if !ctor_name.starts_with(&prefix) {
154-
continue;
155-
}
156-
if seen.contains(&ctor_name) {
157-
continue;
158-
}
159-
// Only add if the constructor has a value entry (it's exported)
160-
if !mod_exports.values.contains_key(ctor_qi) {
161-
continue;
162-
}
163-
// Already handled in the values loop above
164-
}
165-
let _ = type_qi;
166-
}
167146
}
147+
drop(comp_index);
168148

169149
Ok(Some(CompletionResponse::List(CompletionList {
170150
is_incomplete: items.len() > 100,

src/lsp/handlers/diagnostics.rs

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ use std::fmt::Display;
22

33
use tower_lsp::lsp_types::*;
44

5+
use crate::cst::Module;
56
use crate::interner;
67
use crate::build::cache::ModuleCache;
8+
use crate::typechecker::registry::ModuleRegistry;
79

810
use super::super::{Backend, FileState};
911

@@ -14,7 +16,34 @@ impl Backend {
1416
.await;
1517
}
1618

19+
/// Ensure all modules imported by `module` have their exports loaded into the registry.
20+
/// Loads missing exports lazily from the ModuleCache (which reads from disk on demand).
21+
async fn ensure_imports_loaded(&self, module: &Module, registry: &mut ModuleRegistry) {
22+
for import_decl in &module.imports {
23+
let import_parts = &import_decl.module.parts;
24+
25+
// Skip if already in registry
26+
if registry.lookup(import_parts).is_some() {
27+
continue;
28+
}
29+
30+
let import_name = interner::resolve_module_name(import_parts);
31+
32+
// Try to load from module cache (lazy disk load)
33+
let exports = {
34+
let mut cache = self.module_cache.write().await;
35+
cache.get_exports(&import_name).cloned()
36+
};
37+
38+
if let Some(exports) = exports {
39+
registry.register(import_parts, exports);
40+
log::debug!("Lazy-loaded exports for {import_name}");
41+
}
42+
}
43+
}
44+
1745
pub(crate) async fn on_change(&self, uri: Url, source: String) {
46+
let on_change_start = std::time::Instant::now();
1847
{
1948
let mut files = self.files.write().await;
2049
files.insert(
@@ -31,6 +60,7 @@ impl Backend {
3160
return;
3261
}
3362

63+
let t = std::time::Instant::now();
3464
let module = match crate::parser::parse(&source) {
3565
Ok(module) => {
3666
let module_name = format!("{}", module.name.value);
@@ -58,13 +88,21 @@ impl Backend {
5888
return;
5989
}
6090
};
91+
self.info(format!("[on_change] parse: {:.2?}", t.elapsed())).await;
6192

6293
let module_name = interner::resolve_module_name(&module.name.value.parts);
6394
let module_parts: Vec<interner::Symbol> = module.name.value.parts.clone();
6495

65-
// Type-check against the registry (use stacker to extend stack for deep recursion)
96+
// Ensure imported modules' exports are in the registry (lazy load from cache)
97+
let t = std::time::Instant::now();
6698
let mut registry = self.registry.write().await;
99+
self.ensure_imports_loaded(&module, &mut registry).await;
100+
self.info(format!("[on_change] ensure_imports_loaded: {:.2?}", t.elapsed())).await;
101+
102+
// Type-check against the registry
103+
let t = std::time::Instant::now();
67104
let check_result = crate::typechecker::check_module_with_registry(&module, &registry);
105+
self.info(format!("[on_change] typecheck {module_name}: {:.2?}", t.elapsed())).await;
68106

69107
// Update registry with new exports
70108
registry.register(&module_parts, check_result.exports.clone());
@@ -76,10 +114,6 @@ impl Backend {
76114
.collect();
77115
let mut cache = self.module_cache.write().await;
78116
cache.update(module_name.clone(), source_hash, check_result.exports, import_names);
79-
cache.build_reverse_deps();
80-
81-
// Find transitive dependents that need re-checking
82-
let dependents = cache.transitive_dependents(&module_name);
83117
drop(cache);
84118

85119
// Publish diagnostics for the changed module
@@ -88,63 +122,7 @@ impl Backend {
88122
.publish_diagnostics(uri, diagnostics, None)
89123
.await;
90124

91-
// Update source map
92-
{
93-
let mfmap = self.module_file_map.read().await;
94-
let mut smap = self.source_map.write().await;
95-
// Update the changed module's source in source_map
96-
if let Some(file_uri) = mfmap.get(&module_name) {
97-
smap.insert(file_uri.clone(), source);
98-
}
99-
}
100-
101-
// Cascade: re-typecheck dependents
102-
if !dependents.is_empty() {
103-
log::debug!("Cascade rebuild: {} dependents of {}", dependents.len(), module_name);
104-
105-
let mfmap = self.module_file_map.read().await;
106-
let smap = self.source_map.read().await;
107-
108-
for dep_name in &dependents {
109-
let dep_uri_str = match mfmap.get(dep_name) {
110-
Some(u) => u.clone(),
111-
None => continue,
112-
};
113-
let dep_source = match smap.get(&dep_uri_str) {
114-
Some(s) => s.clone(),
115-
None => continue,
116-
};
117-
let dep_uri = match Url::parse(&dep_uri_str) {
118-
Ok(u) => u,
119-
Err(_) => continue,
120-
};
121-
122-
let dep_module = match crate::parser::parse(&dep_source) {
123-
Ok(m) => m,
124-
Err(_) => continue,
125-
};
126-
127-
let dep_result = crate::typechecker::check_module_with_registry(&dep_module, &registry);
128-
129-
// Update registry with dependent's exports
130-
let dep_parts: Vec<interner::Symbol> = dep_module.name.value.parts.clone();
131-
registry.register(&dep_parts, dep_result.exports.clone());
132-
133-
// Update cache for dependent
134-
let dep_hash = ModuleCache::content_hash(&dep_source);
135-
let dep_imports: Vec<String> = dep_module.imports.iter()
136-
.map(|imp| interner::resolve_module_name(&imp.module.parts))
137-
.collect();
138-
let mut cache = self.module_cache.write().await;
139-
cache.update(dep_name.clone(), dep_hash, dep_result.exports, dep_imports);
140-
drop(cache);
141-
142-
let dep_diagnostics = type_errors_to_diagnostics(&dep_result.errors, &dep_source);
143-
self.client
144-
.publish_diagnostics(dep_uri, dep_diagnostics, None)
145-
.await;
146-
}
147-
}
125+
self.info(format!("[on_change] total: {:.2?}", on_change_start.elapsed())).await;
148126
}
149127
}
150128

0 commit comments

Comments
 (0)