diff --git a/Cargo.lock b/Cargo.lock index c4a7112abd41bc7ee3621f0f4d8fa88b026e852f..1952e19e58b5c8829120b5b3f5eb041ac69e4d0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2727,6 +2727,7 @@ dependencies = [ "text", "theme", "tree-sitter", + "tree-sitter-json", "tree-sitter-rust", "unindent", "util", diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 518753fd753418ef144168f7102d72bca8338cd9..0518261f847fa460bbb2d054e7fc65b9019c4686 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -57,5 +57,6 @@ util = { path = "../util", features = ["test-support"] } ctor = "0.1" env_logger = "0.8" rand = "0.8.3" +tree-sitter-json = "0.19.0" tree-sitter-rust = "0.20.0" unindent = "0.1.7" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index eb68b8bbddb1af10cd7538398430600d361a2e15..348ea225cdc008df5e6d9b506ca84c0327be8126 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -486,6 +486,7 @@ impl Buffer { } pub fn set_language(&mut self, language: Option>, cx: &mut ModelContext) { + *self.syntax_tree.lock() = None; self.language = language; self.reparse(cx); } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 0acd4d7d2ffc5a5d9e8db4957da1d04c7430b02c..921a2343d470d3aa624e21eb47f577302549125c 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -276,12 +276,32 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { "arguments: (arguments (identifier)))))))", ) ); +} - fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> String { - buffer.read_with(cx, |buffer, _| { - buffer.syntax_tree().unwrap().root_node().to_sexp() - }) - } +#[gpui::test] +async fn test_resetting_language(cx: &mut gpui::TestAppContext) { + let buffer = cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "{}", cx).with_language(Arc::new(rust_lang()), cx); + buffer.set_sync_parse_timeout(Duration::ZERO); + buffer + }); + + // Wait for the initial text to parse + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + "(source_file (expression_statement (block)))" + ); + + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(Arc::new(json_lang())), cx) + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!(get_tree_sexp(&buffer, &cx), "(document (object))"); } #[gpui::test] @@ -978,6 +998,23 @@ fn rust_lang() -> Language { .unwrap() } +fn json_lang() -> Language { + Language::new( + LanguageConfig { + name: "Json".into(), + path_suffixes: vec!["js".to_string()], + ..Default::default() + }, + Some(tree_sitter_json::language()), + ) +} + +fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> String { + buffer.read_with(cx, |buffer, _| { + buffer.syntax_tree().unwrap().root_node().to_sexp() + }) +} + fn empty(point: Point) -> Range { point..point } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c9683f39d978de3c8d10de0f4abccebcb801b768..b3fe42bbd3da47e128cf227957fe46aa78edb06b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1019,7 +1019,14 @@ impl Project { cx: &mut ModelContext, ) -> Task> { let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx); + let old_path = + File::from_dyn(buffer.read(cx).file()).and_then(|f| Some(f.as_local()?.abs_path(cx))); cx.spawn(|this, mut cx| async move { + if let Some(old_path) = old_path { + this.update(&mut cx, |this, cx| { + this.unregister_buffer_from_language_server(&buffer, old_path, cx); + }); + } let (worktree, path) = worktree_task.await?; worktree .update(&mut cx, |worktree, cx| { @@ -1091,6 +1098,23 @@ impl Project { self.assign_language_to_buffer(buffer, cx); self.register_buffer_with_language_server(buffer, cx); + cx.observe_release(buffer, |this, buffer, cx| { + if let Some(file) = File::from_dyn(buffer.file()) { + if file.is_local() { + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) { + server + .notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(uri.clone()), + }, + ) + .log_err(); + } + } + } + }) + .detach(); Ok(()) } @@ -1143,30 +1167,33 @@ impl Project { self.buffer_snapshots .insert(buffer_id, vec![(0, initial_snapshot)]); } - - cx.observe_release(buffer_handle, |this, buffer, cx| { - if let Some(file) = File::from_dyn(buffer.file()) { - if file.is_local() { - let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); - if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) { - server - .notify::( - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( - uri.clone(), - ), - }, - ) - .log_err(); - } - } - } - }) - .detach(); } } } + fn unregister_buffer_from_language_server( + &mut self, + buffer: &ModelHandle, + old_path: PathBuf, + cx: &mut ModelContext, + ) { + buffer.update(cx, |buffer, cx| { + buffer.update_diagnostics(Default::default(), cx); + self.buffer_snapshots.remove(&buffer.remote_id()); + if let Some((_, language_server)) = self.language_server_for_buffer(buffer, cx) { + language_server + .notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path(old_path).unwrap(), + ), + }, + ) + .log_err(); + } + }); + } + fn on_buffer_event( &mut self, buffer: ModelHandle, @@ -3387,6 +3414,7 @@ impl Project { ) { let snapshot = worktree_handle.read(cx).snapshot(); let mut buffers_to_delete = Vec::new(); + let mut renamed_buffers = Vec::new(); for (buffer_id, buffer) in &self.opened_buffers { if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| { @@ -3426,6 +3454,11 @@ impl Project { } }; + let old_path = old_file.abs_path(cx); + if new_file.abs_path(cx) != old_path { + renamed_buffers.push((cx.handle(), old_path)); + } + if let Some(project_id) = self.remote_id() { self.client .send(proto::UpdateBufferFile { @@ -3446,6 +3479,12 @@ impl Project { for buffer_id in buffers_to_delete { self.opened_buffers.remove(&buffer_id); } + + for (buffer, old_path) in renamed_buffers { + self.unregister_buffer_from_language_server(&buffer, old_path, cx); + self.assign_language_to_buffer(&buffer, cx); + self.register_buffer_with_language_server(&buffer, cx); + } } pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { @@ -4970,7 +5009,7 @@ mod tests { ) .await; - let project = Project::test(fs, cx); + let project = Project::test(fs.clone(), cx); project.update(cx, |project, _| { project.languages.add(Arc::new(rust_language)); project.languages.add(Arc::new(json_language)); @@ -5122,6 +5161,110 @@ mod tests { ) ); + // Renames are reported only to servers matching the buffer's language. + fs.rename( + Path::new("/the-root/test2.rs"), + Path::new("/the-root/test3.rs"), + Default::default(), + ) + .await + .unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test2.rs").unwrap() + ), + ); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test3.rs").unwrap(), + version: 0, + text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), + language_id: Default::default() + }, + ); + + rust_buffer2.update(cx, |buffer, cx| { + buffer.update_diagnostics( + DiagnosticSet::from_sorted_entries( + vec![DiagnosticEntry { + diagnostic: Default::default(), + range: Anchor::MIN..Anchor::MAX, + }], + &buffer.snapshot(), + ), + cx, + ); + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, usize>(0..buffer.len(), false) + .count(), + 1 + ); + }); + + // When the rename changes the extension of the file, the buffer gets closed on the old + // language server and gets opened on the new one. + fs.rename( + Path::new("/the-root/test3.rs"), + Path::new("/the-root/test3.json"), + Default::default(), + ) + .await + .unwrap(); + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test3.rs").unwrap(), + ), + ); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(), + version: 0, + text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), + language_id: Default::default() + }, + ); + // We clear the diagnostics, since the language has changed. + rust_buffer2.read_with(cx, |buffer, _| { + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, usize>(0..buffer.len(), false) + .count(), + 0 + ); + }); + + // The renamed file's version resets after changing language server. + rust_buffer2.update(cx, |buffer, cx| buffer.edit([0..0], "// ", cx)); + assert_eq!( + fake_json_server + .receive_notification::() + .await + .text_document, + lsp::VersionedTextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test3.json").unwrap(), + 1 + ) + ); + // Restart language servers project.update(cx, |project, cx| { project.restart_language_servers_for_buffers( @@ -5139,48 +5282,48 @@ mod tests { let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); let mut fake_json_server = fake_json_servers.next().await.unwrap(); - // Ensure both rust documents are reopened in new rust language server without worrying about order + // Ensure rust document is reopened in new rust language server + assert_eq!( + fake_rust_server + .receive_notification::() + .await + .text_document, + lsp::TextDocumentItem { + uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + version: 1, + text: rust_buffer.read_with(cx, |buffer, _| buffer.text()), + language_id: Default::default() + } + ); + + // Ensure json documents are reopened in new json language server assert_set_eq!( [ - fake_rust_server + fake_json_server .receive_notification::() .await .text_document, - fake_rust_server + fake_json_server .receive_notification::() .await .text_document, ], [ lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), - version: 1, - text: rust_buffer.read_with(cx, |buffer, _| buffer.text()), + uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), + version: 0, + text: json_buffer.read_with(cx, |buffer, _| buffer.text()), language_id: Default::default() }, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test2.rs").unwrap(), + uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(), version: 1, text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), language_id: Default::default() - }, + } ] ); - // Ensure json document is reopened in new json language server - assert_eq!( - fake_json_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), - version: 0, - text: json_buffer.read_with(cx, |buffer, _| buffer.text()), - language_id: Default::default() - } - ); - // Close notifications are reported only to servers matching the buffer's language. cx.update(|_| drop(json_buffer)); let close_message = lsp::DidCloseTextDocumentParams {