Skip to content

Commit b7d6244

Browse files
committed
typecheck on open/init
1 parent bcdc25a commit b7d6244

2 files changed

Lines changed: 127 additions & 3 deletions

File tree

src/lsp/handlers/diagnostics.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,27 @@ impl Backend {
124124

125125
self.info(format!("[on_change] total: {:.2?}", on_change_start.elapsed())).await;
126126
}
127+
128+
/// Re-typecheck all files that are currently open in the editor.
129+
/// Called after initialization completes to process files opened during loading.
130+
pub(crate) async fn typecheck_open_files(&self) {
131+
let open_files: Vec<(String, String)> = {
132+
let files = self.files.read().await;
133+
files.iter().map(|(uri, fs)| (uri.clone(), fs.source.clone())).collect()
134+
};
135+
if open_files.is_empty() {
136+
return;
137+
}
138+
self.info(format!("[lsp] typechecking {} open file(s) after init", open_files.len())).await;
139+
for (uri_str, source) in open_files {
140+
if let Ok(uri) = Url::parse(&uri_str) {
141+
self.on_change(uri, source).await;
142+
}
143+
}
144+
}
127145
}
128146

129-
fn type_errors_to_diagnostics(errors: &[crate::typechecker::error::TypeError], source: &str) -> Vec<Diagnostic> {
147+
pub(crate) fn type_errors_to_diagnostics(errors: &[crate::typechecker::error::TypeError], source: &str) -> Vec<Diagnostic> {
130148
errors
131149
.iter()
132150
.map(|err| {
@@ -156,7 +174,7 @@ fn type_errors_to_diagnostics(errors: &[crate::typechecker::error::TypeError], s
156174
.collect()
157175
}
158176

159-
fn error_to_range(err: &crate::diagnostics::CompilerError, source: &str) -> Range {
177+
pub(crate) fn error_to_range(err: &crate::diagnostics::CompilerError, source: &str) -> Range {
160178
match err.get_span() {
161179
Some(span) => match span.to_pos(source) {
162180
Some((start, end)) => Range {

src/lsp/handlers/load_sources.rs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
use std::collections::HashMap;
22
use std::sync::atomic::Ordering;
3+
use std::sync::Arc;
34

45
use rayon::prelude::*;
6+
use tokio::sync::RwLock;
57
use tower_lsp::lsp_types::*;
8+
use tower_lsp::Client;
69

710
use crate::build::cache;
11+
use crate::build::cache::ModuleCache;
812
use crate::build::BuildOptions;
913
use crate::cst::{self, Decl};
1014
use crate::interner;
1115
use crate::lsp::utils::find_definition::DefinitionIndex;
12-
use crate::lsp::{CompletionEntry, CompletionEntryKind, CompletionIndex};
16+
use crate::lsp::{CompletionEntry, CompletionEntryKind, CompletionIndex, FileState};
17+
use crate::typechecker::registry::ModuleRegistry;
1318

1419
use super::super::{Backend, LOAD_STATE_CACHE_LOADED, LOAD_STATE_READY};
1520

@@ -95,6 +100,7 @@ impl Backend {
95100
if all_snapshots_loaded {
96101
self.load_state.store(LOAD_STATE_READY, Ordering::SeqCst);
97102
self.info(format!("[timing] Ready from cache in {:.2?} total", total_start.elapsed())).await;
103+
self.typecheck_open_files().await;
98104
return;
99105
}
100106

@@ -145,6 +151,8 @@ impl Backend {
145151
let load_state = self.load_state.clone();
146152
let cache_dir = self.cache_dir.clone();
147153
let progress_token = token.clone();
154+
let files = self.files.clone();
155+
let registry = self.registry.clone();
148156

149157
let rt_handle = tokio::runtime::Handle::current();
150158
std::thread::Builder::new()
@@ -355,6 +363,9 @@ impl Backend {
355363
.await;
356364
});
357365
info(format!("[timing] Phase B (index build) total: {:.2?}", build_start.elapsed()));
366+
367+
// Typecheck any files that were opened during loading
368+
typecheck_open_files(&files, &registry, &module_cache, &client, &rt);
358369
})
359370
.expect("failed to spawn load-sources thread");
360371
}
@@ -393,6 +404,7 @@ impl Backend {
393404
let load_state = self.load_state.clone();
394405
let cache_dir = self.cache_dir.clone();
395406
let progress_token = token.clone();
407+
let files = self.files.clone();
396408

397409
let rt_handle = tokio::runtime::Handle::current();
398410
std::thread::Builder::new()
@@ -640,11 +652,105 @@ impl Backend {
640652
.await;
641653
});
642654
info(format!("[timing] Phase B (full build) total: {:.2?}", build_start.elapsed()));
655+
656+
// Typecheck any files that were opened during loading
657+
typecheck_open_files(&files, &registry, &module_cache, &client, &rt);
643658
})
644659
.expect("failed to spawn load-sources thread");
645660
}
646661
}
647662

663+
/// Typecheck any files that were opened in the editor during loading.
664+
/// Called from spawned threads after setting READY.
665+
fn typecheck_open_files(
666+
files: &Arc<RwLock<HashMap<String, FileState>>>,
667+
registry: &Arc<RwLock<ModuleRegistry>>,
668+
module_cache: &Arc<RwLock<ModuleCache>>,
669+
client: &Client,
670+
rt: &tokio::runtime::Handle,
671+
) {
672+
let open_files: Vec<(String, String)> = rt.block_on(async {
673+
let f = files.read().await;
674+
f.iter().map(|(uri, fs)| (uri.clone(), fs.source.clone())).collect()
675+
});
676+
if open_files.is_empty() {
677+
return;
678+
}
679+
rt.block_on(client.log_message(
680+
MessageType::INFO,
681+
format!("[lsp] typechecking {} open file(s) after init", open_files.len()),
682+
));
683+
for (uri_str, source) in open_files {
684+
let uri = match Url::parse(&uri_str) {
685+
Ok(u) => u,
686+
Err(_) => continue,
687+
};
688+
let module = match crate::parser::parse(&source) {
689+
Ok(m) => m,
690+
Err(err) => {
691+
let range = super::diagnostics::error_to_range(&err, &source);
692+
let diagnostics = vec![Diagnostic {
693+
range,
694+
severity: Some(DiagnosticSeverity::ERROR),
695+
code: Some(NumberOrString::String(err.code())),
696+
source: Some("pfc".to_string()),
697+
message: err.get_message(),
698+
..Default::default()
699+
}];
700+
rt.block_on(client.publish_diagnostics(uri, diagnostics, None));
701+
continue;
702+
}
703+
};
704+
705+
let module_name = interner::resolve_module_name(&module.name.value.parts);
706+
let module_parts: Vec<crate::interner::Symbol> = module.name.value.parts.clone();
707+
708+
rt.block_on(async {
709+
// Update file state with module name
710+
{
711+
let mut f = files.write().await;
712+
if let Some(fs) = f.get_mut(&uri_str) {
713+
fs.module_name = Some(module_name.clone());
714+
}
715+
}
716+
717+
let mut reg = registry.write().await;
718+
719+
// Lazy-load imports from cache
720+
for import_decl in &module.imports {
721+
let import_parts = &import_decl.module.parts;
722+
if reg.lookup(import_parts).is_some() {
723+
continue;
724+
}
725+
let import_name = interner::resolve_module_name(import_parts);
726+
let exports = {
727+
let mut mc = module_cache.write().await;
728+
mc.get_exports(&import_name).cloned()
729+
};
730+
if let Some(exports) = exports {
731+
reg.register(import_parts, exports);
732+
}
733+
}
734+
735+
let check_result = crate::typechecker::check_module_with_registry(&module, &reg);
736+
reg.register(&module_parts, check_result.exports.clone());
737+
738+
let source_hash = ModuleCache::content_hash(&source);
739+
let import_names: Vec<String> = module.imports.iter()
740+
.map(|imp| interner::resolve_module_name(&imp.module.parts))
741+
.collect();
742+
let mut mc = module_cache.write().await;
743+
if check_result.errors.is_empty() {
744+
mc.update(module_name.clone(), source_hash, check_result.exports, import_names);
745+
}
746+
drop(mc);
747+
748+
let diagnostics = super::diagnostics::type_errors_to_diagnostics(&check_result.errors, &source);
749+
client.publish_diagnostics(uri, diagnostics, None).await;
750+
});
751+
}
752+
}
753+
648754
/// Extract completion entries from a module's CST declarations and source text.
649755
fn extract_completion_entries(module: &cst::Module, source: &str) -> Vec<CompletionEntry> {
650756
let mut entries = Vec::new();

0 commit comments

Comments
 (0)