diff --git a/CHANGELOG.md b/CHANGELOG.md index db04eb99410..e364dc6640c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -175,6 +175,10 @@ now generate a decoder and an encoder, respectively, for `Nil` values. ([Hari Mohan](https://github.com/seafoamteal)) +- The language server now cleans up compilation engines when all project + files are closed. + ([Nonso Chukwurah](https://github.com/TrippleCCC)) + ### Formatter ### Bug fixes diff --git a/language-server/src/messages.rs b/language-server/src/messages.rs index 2d2b7318c74..f7ee40a6ca0 100644 --- a/language-server/src/messages.rs +++ b/language-server/src/messages.rs @@ -91,8 +91,12 @@ impl Request { pub enum Notification { /// A Gleam file has been modified in memory, and the new text is provided. SourceFileChangedInMemory { path: Utf8PathBuf, text: String }, - /// A Gleam file has been saved or closed in the editor. - SourceFileMatchesDisc { path: Utf8PathBuf }, + /// A Gleam file has been opened in the editor. + SourceFileOpened { path: Utf8PathBuf, text: String }, + /// A Gleam file has been closed in the editor. + SourceFileClosed { path: Utf8PathBuf }, + /// A gleam file has been save to disk + SourceFileSaved { path: Utf8PathBuf }, /// gleam.toml has changed. ConfigFileChanged { path: Utf8PathBuf }, /// It's time to compile all open projects. @@ -104,7 +108,7 @@ impl Notification { match notification.method.as_str() { "textDocument/didOpen" => { let params = cast_notification::(notification); - let notification = Notification::SourceFileChangedInMemory { + let notification = Notification::SourceFileOpened { path: super::path(¶ms.text_document.uri), text: params.text_document.text, }; @@ -121,14 +125,14 @@ impl Notification { "textDocument/didSave" => { let params = cast_notification::(notification); - let notification = Notification::SourceFileMatchesDisc { + let notification = Notification::SourceFileSaved { path: super::path(¶ms.text_document.uri), }; Some(Message::Notification(notification)) } "textDocument/didClose" => { let params = cast_notification::(notification); - let notification = Notification::SourceFileMatchesDisc { + let notification = Notification::SourceFileClosed { path: super::path(¶ms.text_document.uri), }; Some(Message::Notification(notification)) diff --git a/language-server/src/server.rs b/language-server/src/server.rs index dea32958f25..b352761bcf5 100644 --- a/language-server/src/server.rs +++ b/language-server/src/server.rs @@ -41,6 +41,22 @@ pub struct LanguageServer<'a, IO> { outside_of_project_feedback: FeedbackBookKeeper, router: Router>, changed_projects: HashSet, + + /// Tracks all files that have ever been opened and their current + /// open state. + /// + /// All files that have been opened are tracked so that upon + /// termination of a complilation engine, the server can send + /// back an empty list of diagnostics for each opened file for + /// a project. We keep track of the open state so we can determine + /// when all files in a project are closed. + /// + /// Files are removed from the map when a compilation engine is + /// terminated. + /// + /// This approach allows the editor to still display diagnostics + /// for files that have been closed. + opened_files: HashMap, io: FileSystemProxy, } @@ -63,6 +79,7 @@ where connection: connection.into(), initialise_params, changed_projects: HashSet::new(), + opened_files: HashMap::new(), outside_of_project_feedback: FeedbackBookKeeper::default(), router, io, @@ -131,18 +148,58 @@ where .expect("channel send LSP response") } + fn attempt_engine_cleanup(&mut self, path: &Utf8PathBuf, feedback: &mut Feedback) { + if let Some(project_path) = self.router.project_path(path) { + let all_files_closed = self + .opened_files + .iter() + .filter(|(path, _)| path.starts_with(&project_path)) + .all(|(_, is_open)| !*is_open); + + if all_files_closed { + self.router.delete_engine_for_path(&project_path); + _ = self.changed_projects.remove(&project_path); + + for (path, _) in self + .opened_files + .extract_if(|path, _| path.starts_with(&project_path)) + .into_iter() + { + feedback.unset_existing_diagnostics(path); + } + } + } + } + fn handle_notification(&mut self, notification: Notification) { let feedback = match notification { Notification::CompilePlease => self.compile_please(), - Notification::SourceFileMatchesDisc { path } => self.discard_in_memory_cache(path), + Notification::SourceFileClosed { path } => self.handle_file_close(&path), + Notification::SourceFileSaved { path } => self.discard_in_memory_cache(&path), Notification::SourceFileChangedInMemory { path, text } => { - self.cache_file_in_memory(path, text) + self.cache_file_in_memory(&path, text) } + Notification::SourceFileOpened { path, text } => self.handle_file_open(path, text), Notification::ConfigFileChanged { path } => self.watched_files_changed(path), }; self.publish_feedback(feedback); } + fn handle_file_close(&mut self, path: &Utf8PathBuf) -> Feedback { + let mut feedback = self.discard_in_memory_cache(path); + if let Some(is_open) = self.opened_files.get_mut(path) { + *is_open = false; + } + self.attempt_engine_cleanup(path, &mut feedback); + feedback + } + + fn handle_file_open(&mut self, path: Utf8PathBuf, text: String) -> Feedback { + let feedback = self.cache_file_in_memory(&path, text); + let _ = self.opened_files.insert(path, true); + feedback + } + fn publish_feedback(&self, feedback: Feedback) { self.publish_diagnostics(feedback.diagnostics); self.publish_messages(feedback.messages); @@ -428,17 +485,17 @@ where self.respond_with_engine(path, |engine| engine.find_references(params)) } - fn cache_file_in_memory(&mut self, path: Utf8PathBuf, text: String) -> Feedback { - self.project_changed(&path); - if let Err(error) = self.io.write_mem_cache(&path, &text) { + fn cache_file_in_memory(&mut self, path: &Utf8PathBuf, text: String) -> Feedback { + self.project_changed(path); + if let Err(error) = self.io.write_mem_cache(path, &text) { return self.outside_of_project_feedback.error(error); } Feedback::none() } - fn discard_in_memory_cache(&mut self, path: Utf8PathBuf) -> Feedback { - self.project_changed(&path); - if let Err(error) = self.io.delete_mem_cache(&path) { + fn discard_in_memory_cache(&mut self, path: &Utf8PathBuf) -> Feedback { + self.project_changed(path); + if let Err(error) = self.io.delete_mem_cache(path) { return self.outside_of_project_feedback.error(error); } Feedback::none()