From 4d456d384706f1bf037c0383e3c15d4c307d23ef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Mar 2022 18:01:29 -0700 Subject: [PATCH 01/24] Remove duplication in build_language_registry --- crates/language/src/language.rs | 4 +- crates/zed/src/language.rs | 125 +++++++++++++++++--------------- 2 files changed, 67 insertions(+), 62 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 3fbc00c72d350c975222008cf9dc03015020ca26..bdc2e120df47a1af022202164f1f0b5c033498da 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -465,8 +465,8 @@ impl Language { Ok(self) } - pub fn with_lsp_adapter(mut self, lsp_adapter: impl LspAdapter) -> Self { - self.adapter = Some(Arc::new(lsp_adapter)); + pub fn with_lsp_adapter(mut self, lsp_adapter: Arc) -> Self { + self.adapter = Some(lsp_adapter); self } diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 0d69ebee690a02a7dff73593c538dd978f996835..166ec2a8397f016740d16603b1811513521dfd9c 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -534,71 +534,68 @@ impl LspAdapter for JsonLspAdapter { pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { let languages = LanguageRegistry::new(login_shell_env_loaded); - languages.add(Arc::new(c())); - languages.add(Arc::new(json())); - languages.add(Arc::new(rust())); - languages.add(Arc::new(markdown())); + for (name, grammar, lsp_adapter) in [ + ( + "c", + tree_sitter_c::language(), + Some(Arc::new(CLspAdapter) as Arc), + ), + ( + "json", + tree_sitter_json::language(), + Some(Arc::new(JsonLspAdapter)), + ), + ( + "markdown", + tree_sitter_markdown::language(), + None, // + ), + ( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(RustLspAdapter)), + ), + ] { + languages.add(Arc::new(language(name, grammar, lsp_adapter))); + } languages } -fn rust() -> Language { - let grammar = tree_sitter_rust::language(); - let config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("rust/highlights.scm").as_ref()) - .unwrap() - .with_brackets_query(load_query("rust/brackets.scm").as_ref()) - .unwrap() - .with_indents_query(load_query("rust/indents.scm").as_ref()) - .unwrap() - .with_outline_query(load_query("rust/outline.scm").as_ref()) - .unwrap() - .with_lsp_adapter(RustLspAdapter) -} - -fn c() -> Language { - let grammar = tree_sitter_c::language(); - let config = toml::from_slice(&LanguageDir::get("c/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("c/highlights.scm").as_ref()) - .unwrap() - .with_brackets_query(load_query("c/brackets.scm").as_ref()) - .unwrap() - .with_indents_query(load_query("c/indents.scm").as_ref()) - .unwrap() - .with_outline_query(load_query("c/outline.scm").as_ref()) - .unwrap() - .with_lsp_adapter(CLspAdapter) -} - -fn json() -> Language { - let grammar = tree_sitter_json::language(); - let config = toml::from_slice(&LanguageDir::get("json/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("json/highlights.scm").as_ref()) - .unwrap() - .with_brackets_query(load_query("json/brackets.scm").as_ref()) - .unwrap() - .with_indents_query(load_query("json/indents.scm").as_ref()) - .unwrap() - .with_outline_query(load_query("json/outline.scm").as_ref()) - .unwrap() - .with_lsp_adapter(JsonLspAdapter) -} - -fn markdown() -> Language { - let grammar = tree_sitter_markdown::language(); - let config = toml::from_slice(&LanguageDir::get("markdown/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("markdown/highlights.scm").as_ref()) - .unwrap() +fn language( + name: &str, + grammar: tree_sitter::Language, + lsp_adapter: Option>, +) -> Language { + let config = toml::from_slice( + &LanguageDir::get(&format!("{}/config.toml", name)) + .unwrap() + .data, + ) + .unwrap(); + let mut language = Language::new(config, Some(grammar)); + if let Some(query) = load_query(&format!("{}/highlights.scm", name)) { + language = language.with_highlights_query(query.as_ref()).unwrap(); + } + if let Some(query) = load_query(&format!("{}/brackets.scm", name)) { + language = language.with_brackets_query(query.as_ref()).unwrap(); + } + if let Some(query) = load_query(&format!("{}/indents.scm", name)) { + language = language.with_indents_query(query.as_ref()).unwrap(); + } + if let Some(query) = load_query(&format!("{}/outline.scm", name)) { + language = language.with_outline_query(query.as_ref()).unwrap(); + } + if let Some(lsp_adapter) = lsp_adapter { + language = language.with_lsp_adapter(lsp_adapter) + } + language } -fn load_query(path: &str) -> Cow<'static, str> { - match LanguageDir::get(path).unwrap().data { +fn load_query(path: &str) -> Option> { + LanguageDir::get(path).map(|item| match item.data { Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - } + }) } #[cfg(test)] @@ -651,7 +648,11 @@ mod tests { #[test] fn test_rust_label_for_completion() { - let language = rust(); + let language = language( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(RustLspAdapter)), + ); let grammar = language.grammar().unwrap(); let theme = SyntaxTheme::new(vec![ ("type".into(), Color::green().into()), @@ -726,7 +727,11 @@ mod tests { #[test] fn test_rust_label_for_symbol() { - let language = rust(); + let language = language( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(RustLspAdapter)), + ); let grammar = language.grammar().unwrap(); let theme = SyntaxTheme::new(vec![ ("type".into(), Color::green().into()), From a8600e76a3de34968ed27c984ed32c4f17427f46 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Mar 2022 18:14:49 -0700 Subject: [PATCH 02/24] Make language's language server config non-optional --- crates/editor/src/editor.rs | 4 ++-- crates/language/src/language.rs | 24 +++++++----------------- crates/language/src/tests.rs | 1 - crates/project/src/project.rs | 24 +++++++++--------------- crates/server/src/rpc.rs | 22 +++++++++++----------- crates/zed/languages/c/config.toml | 3 --- crates/zed/languages/json/config.toml | 3 --- crates/zed/src/language.rs | 5 +++++ 8 files changed, 34 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f861b6ac60b1f4fe2c1b8ca83df26fed4abf9d6f..dac7adfdf823f2d18e16d0b6d1a27a4f7c321173 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8889,7 +8889,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -8979,7 +8979,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index bdc2e120df47a1af022202164f1f0b5c033498da..5b917c0fdf1e6141e7991b89d8a7f55a1455b4f6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -51,7 +51,7 @@ lazy_static! { brackets: Default::default(), autoclose_before: Default::default(), line_comment: None, - language_server: None, + language_server: Default::default(), }, None, )); @@ -113,7 +113,8 @@ pub struct LanguageConfig { #[serde(default)] pub autoclose_before: String, pub line_comment: Option, - pub language_server: Option, + #[serde(default)] + pub language_server: LanguageServerConfig, } impl Default for LanguageConfig { @@ -251,20 +252,12 @@ impl LanguageRegistry { cx: &mut MutableAppContext, ) -> Option>> { #[cfg(any(test, feature = "test-support"))] - if language - .config - .language_server - .as_ref() - .and_then(|config| config.fake_config.as_ref()) - .is_some() - { + if language.config.language_server.fake_config.is_some() { let language = language.clone(); return Some(cx.spawn(|mut cx| async move { let fake_config = language .config .language_server - .as_ref() - .unwrap() .fake_config .as_ref() .unwrap(); @@ -478,18 +471,15 @@ impl Language { self.config.line_comment.as_deref() } - pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet> { - self.config - .language_server - .as_ref() - .map(|config| &config.disk_based_diagnostic_sources) + pub fn disk_based_diagnostic_sources(&self) -> &HashSet { + &self.config.language_server.disk_based_diagnostic_sources } pub fn disk_based_diagnostics_progress_token(&self) -> Option<&String> { self.config .language_server + .disk_based_diagnostics_progress_token .as_ref() - .and_then(|config| config.disk_based_diagnostics_progress_token.as_ref()) } pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 98ecf63a4692f6419707a6bc95889ddfb2cbb29f..8af4ff247ebd7f62e4c42da7345e14efff78e570 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -869,7 +869,6 @@ fn rust_lang() -> Language { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: None, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 404d867069ec383c24dac33f550ac63306468122..1a41475909d2a0a395ae5fcc7f37541e54cf25fc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1599,14 +1599,8 @@ impl Project { ), ); } - self.update_diagnostics( - params, - language - .disk_based_diagnostic_sources() - .unwrap_or(&Default::default()), - cx, - ) - .log_err(); + self.update_diagnostics(params, language.disk_based_diagnostic_sources(), cx) + .log_err(); if disk_diagnostics_token.is_none() { self.disk_based_diagnostics_finished(cx); self.broadcast_language_server_update( @@ -4697,7 +4691,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(rust_lsp_config), + language_server: rust_lsp_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -4706,7 +4700,7 @@ mod tests { LanguageConfig { name: "JSON".into(), path_suffixes: vec!["json".to_string()], - language_server: Some(json_lsp_config), + language_server: json_lsp_config, ..Default::default() }, None, @@ -4906,7 +4900,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -5023,7 +5017,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(lsp_config), + language_server: lsp_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -5394,7 +5388,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(lsp_config), + language_server: lsp_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -5744,7 +5738,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -6680,7 +6674,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 374aaf6a7ec6820022c1802757cdabf3b51aa946..b329839de2b80912ff48299a69407ef5d8e42c4f 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -2050,7 +2050,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2279,7 +2279,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2474,7 +2474,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2589,7 +2589,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2732,7 +2732,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -2972,7 +2972,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -3119,7 +3119,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -3252,7 +3252,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -3349,7 +3349,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -3585,7 +3585,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), @@ -5456,7 +5456,7 @@ mod tests { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), + language_server: language_server_config, ..Default::default() }, None, diff --git a/crates/zed/languages/c/config.toml b/crates/zed/languages/c/config.toml index aeee919ac6158d9c424fb72d6c086e432dcb14f7..b7e1f0744308b202065b6052fa9994700edb5bcd 100644 --- a/crates/zed/languages/c/config.toml +++ b/crates/zed/languages/c/config.toml @@ -9,6 +9,3 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false }, { start = "/*", end = " */", close = true, newline = false }, ] - -[language_server] -disk_based_diagnostic_sources = [] \ No newline at end of file diff --git a/crates/zed/languages/json/config.toml b/crates/zed/languages/json/config.toml index 27d1193b0d75fb9eb126a5d635d1813b11b83299..ad87dcf63322f321d025a1480e77d6e9bfdb11f7 100644 --- a/crates/zed/languages/json/config.toml +++ b/crates/zed/languages/json/config.toml @@ -6,6 +6,3 @@ brackets = [ { start = "[", end = "]", close = true, newline = true }, { start = "\"", end = "\"", close = true, newline = false }, ] - -[language_server] -disk_based_diagnostic_sources = [] \ No newline at end of file diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 166ec2a8397f016740d16603b1811513521dfd9c..fbf413c644a98cc92503b196f5184bdcb974ab7a 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -555,6 +555,11 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi tree_sitter_rust::language(), Some(Arc::new(RustLspAdapter)), ), + ( + "typescript", + tree_sitter_typescript::language_typescript(), + None, // + ), ] { languages.add(Arc::new(language(name, grammar, lsp_adapter))); } From dd1c88afa5c703ced5bb36205415d2501ce4cb23 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Mar 2022 18:18:49 -0700 Subject: [PATCH 03/24] Add basic TypeScript and TSX support Co-Authored-By: Keith Simmons --- Cargo.lock | 11 + crates/zed/Cargo.toml | 1 + crates/zed/languages/tsx/brackets.scm | 1 + crates/zed/languages/tsx/config.toml | 12 + crates/zed/languages/tsx/highlights-jsx.scm | 0 crates/zed/languages/tsx/highlights.scm | 1 + crates/zed/languages/tsx/indents.scm | 1 + crates/zed/languages/tsx/outline.scm | 1 + crates/zed/languages/typescript/brackets.scm | 5 + crates/zed/languages/typescript/config.toml | 12 + .../zed/languages/typescript/highlights.scm | 219 ++++++++++++++++++ crates/zed/languages/typescript/indents.scm | 15 ++ crates/zed/languages/typescript/outline.scm | 55 +++++ crates/zed/src/language.rs | 52 +++-- 14 files changed, 373 insertions(+), 13 deletions(-) create mode 120000 crates/zed/languages/tsx/brackets.scm create mode 100644 crates/zed/languages/tsx/config.toml create mode 100644 crates/zed/languages/tsx/highlights-jsx.scm create mode 120000 crates/zed/languages/tsx/highlights.scm create mode 120000 crates/zed/languages/tsx/indents.scm create mode 120000 crates/zed/languages/tsx/outline.scm create mode 100644 crates/zed/languages/typescript/brackets.scm create mode 100644 crates/zed/languages/typescript/config.toml create mode 100644 crates/zed/languages/typescript/highlights.scm create mode 100644 crates/zed/languages/typescript/indents.scm create mode 100644 crates/zed/languages/typescript/outline.scm diff --git a/Cargo.lock b/Cargo.lock index 9c3692396461c298a0c73160fb177ee8f8c45850..a90a6d2adfe225c08ab4881bb8242ad5875bc865 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5415,6 +5415,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-typescript" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8ed0ecb931cdff13c6a13f45ccd615156e2779d9ffb0395864e05505e6e86d" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "ttf-parser" version = "0.9.0" @@ -6025,6 +6035,7 @@ dependencies = [ "tree-sitter-json", "tree-sitter-markdown", "tree-sitter-rust", + "tree-sitter-typescript", "unindent", "url", "util", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fc9946b778bc8c74254b49b6fd872e402b515f36..869a6a999926bb98f176d2d03e2315238f1e9c90 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -98,6 +98,7 @@ tree-sitter-c = "0.20.1" tree-sitter-json = "0.19.0" tree-sitter-rust = "0.20.1" tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } +tree-sitter-typescript = "0.20.1" url = "2.2" [dev-dependencies] diff --git a/crates/zed/languages/tsx/brackets.scm b/crates/zed/languages/tsx/brackets.scm new file mode 120000 index 0000000000000000000000000000000000000000..e6835c943b05c54ca6ecccc0b3bbd7673f668788 --- /dev/null +++ b/crates/zed/languages/tsx/brackets.scm @@ -0,0 +1 @@ +../typescript/brackets.scm \ No newline at end of file diff --git a/crates/zed/languages/tsx/config.toml b/crates/zed/languages/tsx/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..62717266df9e0bd56d83b336cfbf28aa2b01ee4b --- /dev/null +++ b/crates/zed/languages/tsx/config.toml @@ -0,0 +1,12 @@ +name = "TSX" +path_suffixes = ["tsx"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true }, + { start = "\"", end = "\"", close = true, newline = false }, + { start = "/*", end = " */", close = true, newline = false }, +] diff --git a/crates/zed/languages/tsx/highlights-jsx.scm b/crates/zed/languages/tsx/highlights-jsx.scm new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/zed/languages/tsx/highlights.scm b/crates/zed/languages/tsx/highlights.scm new file mode 120000 index 0000000000000000000000000000000000000000..226302a5d1605c7110145345be31d8e0cd96818a --- /dev/null +++ b/crates/zed/languages/tsx/highlights.scm @@ -0,0 +1 @@ +../typescript/highlights.scm \ No newline at end of file diff --git a/crates/zed/languages/tsx/indents.scm b/crates/zed/languages/tsx/indents.scm new file mode 120000 index 0000000000000000000000000000000000000000..502c2a060af208e476e793db6b0d69060f0a5377 --- /dev/null +++ b/crates/zed/languages/tsx/indents.scm @@ -0,0 +1 @@ +../typescript/indents.scm \ No newline at end of file diff --git a/crates/zed/languages/tsx/outline.scm b/crates/zed/languages/tsx/outline.scm new file mode 120000 index 0000000000000000000000000000000000000000..a0df409fda15ec9b384fc7659b5c56a6797f9034 --- /dev/null +++ b/crates/zed/languages/tsx/outline.scm @@ -0,0 +1 @@ +../typescript/outline.scm \ No newline at end of file diff --git a/crates/zed/languages/typescript/brackets.scm b/crates/zed/languages/typescript/brackets.scm new file mode 100644 index 0000000000000000000000000000000000000000..63395f81d84e6452c631a9e582e2d697cba445ef --- /dev/null +++ b/crates/zed/languages/typescript/brackets.scm @@ -0,0 +1,5 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/languages/typescript/config.toml b/crates/zed/languages/typescript/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..5d491d2d329e89fe2a97ff8121c2d30c861b61d5 --- /dev/null +++ b/crates/zed/languages/typescript/config.toml @@ -0,0 +1,12 @@ +name = "TypeScript" +path_suffixes = ["ts"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true }, + { start = "\"", end = "\"", close = true, newline = false }, + { start = "/*", end = " */", close = true, newline = false }, +] diff --git a/crates/zed/languages/typescript/highlights.scm b/crates/zed/languages/typescript/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..cb4e82b33d8b04da41e90c6926499669fee33b60 --- /dev/null +++ b/crates/zed/languages/typescript/highlights.scm @@ -0,0 +1,219 @@ +; Variables + +(identifier) @variable + +; Properties + +(property_identifier) @property + +; Function and method calls + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Function and method definitions + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +(pair + key: (property_identifier) @function.method + value: [(function) (arrow_function)]) + +(assignment_expression + left: (member_expression + property: (property_identifier) @function.method) + right: [(function) (arrow_function)]) + +(variable_declarator + name: (identifier) @function + value: [(function) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function) (arrow_function)]) + +; Special identifiers + +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +([ + (identifier) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @constant + (#match? @constant "^[A-Z_][A-Z\\d_]+$")) + +; Literals + +(this) @variable.builtin +(super) @variable.builtin + +[ + (true) + (false) + (null) + (undefined) +] @constant.builtin + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.special +(number) @number + +; Tokens + +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) @embedded + +[ + ";" + "?." + "." + "," +] @punctuation.delimiter + +[ + "-" + "--" + "-=" + "+" + "++" + "+=" + "*" + "*=" + "**" + "**=" + "/" + "/=" + "%" + "%=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "===" + "!" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "~" + "^" + "&" + "|" + "^=" + "&=" + "|=" + "&&" + "||" + "??" + "&&=" + "||=" + "??=" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "default" + "delete" + "do" + "else" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "import" + "in" + "instanceof" + "let" + "new" + "of" + "return" + "set" + "static" + "switch" + "target" + "throw" + "try" + "typeof" + "var" + "void" + "while" + "with" + "yield" +] @keyword + +; Types + +(type_identifier) @type +(predefined_type) @type.builtin + +((identifier) @type + (#match? @type "^[A-Z]")) + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) + +; Keywords + +[ "abstract" + "declare" + "enum" + "export" + "implements" + "interface" + "keyof" + "namespace" + "private" + "protected" + "public" + "type" + "readonly" + "override" +] @keyword \ No newline at end of file diff --git a/crates/zed/languages/typescript/indents.scm b/crates/zed/languages/typescript/indents.scm new file mode 100644 index 0000000000000000000000000000000000000000..107e6ff8e03b633f408676243c24d0d9707a2a26 --- /dev/null +++ b/crates/zed/languages/typescript/indents.scm @@ -0,0 +1,15 @@ +[ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed/languages/typescript/outline.scm b/crates/zed/languages/typescript/outline.scm new file mode 100644 index 0000000000000000000000000000000000000000..f8691fa41d9f64bc71e8a7ada2e6d64d62268a3a --- /dev/null +++ b/crates/zed/languages/typescript/outline.scm @@ -0,0 +1,55 @@ +(internal_module + "namespace" @context + name: (_) @name) @item + +(enum_declaration + "enum" @context + name: (_) @name) @item + +(function_declaration + "async"? @context + "function" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(interface_declaration + "interface" @context + name: (_) @name) @item + +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (_) @name) @item)) + +(class_declaration + "class" @context + name: (_) @name) @item + +(method_definition + [ + "get" + "set" + "async" + "*" + "readonly" + "static" + (override_modifier) + (accessibility_modifier) + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(public_field_definition + [ + "declare" + "readonly" + "abstract" + "static" + (accessibility_modifier) + ]* @context + name: (_) @name) @item diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index fbf413c644a98cc92503b196f5184bdcb974ab7a..50540bc82cb5a6059b4fc2a02a1a3f33e3d741fc 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -555,6 +555,11 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi tree_sitter_rust::language(), Some(Arc::new(RustLspAdapter)), ), + ( + "tsx", + tree_sitter_typescript::language_tsx(), + None, // + ), ( "typescript", tree_sitter_typescript::language_typescript(), @@ -578,17 +583,26 @@ fn language( ) .unwrap(); let mut language = Language::new(config, Some(grammar)); - if let Some(query) = load_query(&format!("{}/highlights.scm", name)) { - language = language.with_highlights_query(query.as_ref()).unwrap(); + + if let Some(query) = load_query(name, "/highlights") { + language = language + .with_highlights_query(query.as_ref()) + .expect("failed to evaluate highlights query"); } - if let Some(query) = load_query(&format!("{}/brackets.scm", name)) { - language = language.with_brackets_query(query.as_ref()).unwrap(); + if let Some(query) = load_query(name, "/brackets") { + language = language + .with_brackets_query(query.as_ref()) + .expect("failed to load brackets query"); } - if let Some(query) = load_query(&format!("{}/indents.scm", name)) { - language = language.with_indents_query(query.as_ref()).unwrap(); + if let Some(query) = load_query(name, "/indents") { + language = language + .with_indents_query(query.as_ref()) + .expect("failed to load indents query"); } - if let Some(query) = load_query(&format!("{}/outline.scm", name)) { - language = language.with_outline_query(query.as_ref()).unwrap(); + if let Some(query) = load_query(name, "/outline") { + language = language + .with_outline_query(query.as_ref()) + .expect("failed to load outline query"); } if let Some(lsp_adapter) = lsp_adapter { language = language.with_lsp_adapter(lsp_adapter) @@ -596,11 +610,23 @@ fn language( language } -fn load_query(path: &str) -> Option> { - LanguageDir::get(path).map(|item| match item.data { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - }) +fn load_query(name: &str, filename_prefix: &str) -> Option> { + let mut result = None; + for path in LanguageDir::iter() { + if let Some(remainder) = path.strip_prefix(name) { + if remainder.starts_with(filename_prefix) { + let contents = match LanguageDir::get(path.as_ref()).unwrap().data { + Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), + Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), + }; + match &mut result { + None => result = Some(contents), + Some(r) => r.to_mut().push_str(contents.as_ref()), + } + } + } + } + result } #[cfg(test)] From d466768eed098a2185d68b6d5b19daa57175a6fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 29 Mar 2022 11:06:08 -0700 Subject: [PATCH 04/24] WIP --- crates/zed/src/languages.rs | 125 ++++++ crates/zed/src/languages/c.rs | 136 +++++++ crates/zed/{ => src}/languages/c/brackets.scm | 0 crates/zed/{ => src}/languages/c/config.toml | 0 .../zed/{ => src}/languages/c/highlights.scm | 0 crates/zed/{ => src}/languages/c/indents.scm | 0 crates/zed/{ => src}/languages/c/outline.scm | 0 crates/zed/src/languages/json.rs | 131 +++++++ .../zed/{ => src}/languages/json/brackets.scm | 0 .../zed/{ => src}/languages/json/config.toml | 0 .../{ => src}/languages/json/highlights.scm | 0 .../zed/{ => src}/languages/json/indents.scm | 0 .../zed/{ => src}/languages/json/outline.scm | 0 .../{ => src}/languages/markdown/config.toml | 0 .../languages/markdown/highlights.scm | 0 .../src/{language.rs => languages/rust.rs} | 370 +----------------- .../zed/{ => src}/languages/rust/brackets.scm | 0 .../zed/{ => src}/languages/rust/config.toml | 0 .../{ => src}/languages/rust/highlights.scm | 0 .../zed/{ => src}/languages/rust/indents.scm | 0 .../zed/{ => src}/languages/rust/outline.scm | 0 .../zed/{ => src}/languages/tsx/brackets.scm | 0 .../zed/{ => src}/languages/tsx/config.toml | 0 .../languages/tsx/highlights-jsx.scm | 0 .../{ => src}/languages/tsx/highlights.scm | 0 .../zed/{ => src}/languages/tsx/indents.scm | 0 .../zed/{ => src}/languages/tsx/outline.scm | 0 crates/zed/src/languages/typescript.rs | 121 ++++++ .../languages/typescript/brackets.scm | 0 .../languages/typescript/config.toml | 0 .../languages/typescript/highlights.scm | 0 .../languages/typescript/indents.scm | 0 .../languages/typescript/outline.scm | 0 crates/zed/src/main.rs | 6 +- crates/zed/src/zed.rs | 6 +- 35 files changed, 523 insertions(+), 372 deletions(-) create mode 100644 crates/zed/src/languages.rs create mode 100644 crates/zed/src/languages/c.rs rename crates/zed/{ => src}/languages/c/brackets.scm (100%) rename crates/zed/{ => src}/languages/c/config.toml (100%) rename crates/zed/{ => src}/languages/c/highlights.scm (100%) rename crates/zed/{ => src}/languages/c/indents.scm (100%) rename crates/zed/{ => src}/languages/c/outline.scm (100%) create mode 100644 crates/zed/src/languages/json.rs rename crates/zed/{ => src}/languages/json/brackets.scm (100%) rename crates/zed/{ => src}/languages/json/config.toml (100%) rename crates/zed/{ => src}/languages/json/highlights.scm (100%) rename crates/zed/{ => src}/languages/json/indents.scm (100%) rename crates/zed/{ => src}/languages/json/outline.scm (100%) rename crates/zed/{ => src}/languages/markdown/config.toml (100%) rename crates/zed/{ => src}/languages/markdown/highlights.scm (100%) rename crates/zed/src/{language.rs => languages/rust.rs} (57%) rename crates/zed/{ => src}/languages/rust/brackets.scm (100%) rename crates/zed/{ => src}/languages/rust/config.toml (100%) rename crates/zed/{ => src}/languages/rust/highlights.scm (100%) rename crates/zed/{ => src}/languages/rust/indents.scm (100%) rename crates/zed/{ => src}/languages/rust/outline.scm (100%) rename crates/zed/{ => src}/languages/tsx/brackets.scm (100%) rename crates/zed/{ => src}/languages/tsx/config.toml (100%) rename crates/zed/{ => src}/languages/tsx/highlights-jsx.scm (100%) rename crates/zed/{ => src}/languages/tsx/highlights.scm (100%) rename crates/zed/{ => src}/languages/tsx/indents.scm (100%) rename crates/zed/{ => src}/languages/tsx/outline.scm (100%) create mode 100644 crates/zed/src/languages/typescript.rs rename crates/zed/{ => src}/languages/typescript/brackets.scm (100%) rename crates/zed/{ => src}/languages/typescript/config.toml (100%) rename crates/zed/{ => src}/languages/typescript/highlights.scm (100%) rename crates/zed/{ => src}/languages/typescript/indents.scm (100%) rename crates/zed/{ => src}/languages/typescript/outline.scm (100%) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs new file mode 100644 index 0000000000000000000000000000000000000000..607f4c30bb756935ce315a0378ce4caf1eb16b71 --- /dev/null +++ b/crates/zed/src/languages.rs @@ -0,0 +1,125 @@ +use client::http::{self, HttpClient, Method}; +use gpui::Task; +pub use language::*; +use rust_embed::RustEmbed; +use serde::Deserialize; +use std::{borrow::Cow, str, sync::Arc}; + +mod c; +mod json; +mod rust; +mod typescript; + +#[derive(RustEmbed)] +#[folder = "src/languages"] +#[exclude = "*.rs"] +struct LanguageDir; + +#[derive(Deserialize)] +struct GithubRelease { + name: String, + assets: Vec, +} + +#[derive(Deserialize)] +struct GithubReleaseAsset { + name: String, + browser_download_url: http::Url, +} + +pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { + let languages = LanguageRegistry::new(login_shell_env_loaded); + for (name, grammar, lsp_adapter) in [ + ( + "c", + tree_sitter_c::language(), + Some(Arc::new(c::CLspAdapter) as Arc), + ), + ( + "json", + tree_sitter_json::language(), + Some(Arc::new(json::JsonLspAdapter)), + ), + ( + "markdown", + tree_sitter_markdown::language(), + None, // + ), + ( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(rust::RustLspAdapter)), + ), + ( + "tsx", + tree_sitter_typescript::language_tsx(), + None, // + ), + ( + "typescript", + tree_sitter_typescript::language_typescript(), + Some(Arc::new(typescript::TypeScriptLspAdapter)), + ), + ] { + languages.add(Arc::new(language(name, grammar, lsp_adapter))); + } + languages +} + +fn language( + name: &str, + grammar: tree_sitter::Language, + lsp_adapter: Option>, +) -> Language { + let config = toml::from_slice( + &LanguageDir::get(&format!("{}/config.toml", name)) + .unwrap() + .data, + ) + .unwrap(); + let mut language = Language::new(config, Some(grammar)); + + if let Some(query) = load_query(name, "/highlights") { + language = language + .with_highlights_query(query.as_ref()) + .expect("failed to evaluate highlights query"); + } + if let Some(query) = load_query(name, "/brackets") { + language = language + .with_brackets_query(query.as_ref()) + .expect("failed to load brackets query"); + } + if let Some(query) = load_query(name, "/indents") { + language = language + .with_indents_query(query.as_ref()) + .expect("failed to load indents query"); + } + if let Some(query) = load_query(name, "/outline") { + language = language + .with_outline_query(query.as_ref()) + .expect("failed to load outline query"); + } + if let Some(lsp_adapter) = lsp_adapter { + language = language.with_lsp_adapter(lsp_adapter) + } + language +} + +fn load_query(name: &str, filename_prefix: &str) -> Option> { + let mut result = None; + for path in LanguageDir::iter() { + if let Some(remainder) = path.strip_prefix(name) { + if remainder.starts_with(filename_prefix) { + let contents = match LanguageDir::get(path.as_ref()).unwrap().data { + Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), + Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), + }; + match &mut result { + None => result = Some(contents), + Some(r) => r.to_mut().push_str(contents.as_ref()), + } + } + } + } + result +} diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs new file mode 100644 index 0000000000000000000000000000000000000000..9ce3eab2f772d86fdcc7f7a717020480660a5ed5 --- /dev/null +++ b/crates/zed/src/languages/c.rs @@ -0,0 +1,136 @@ +use anyhow::{anyhow, Result}; +use client::http::{self, HttpClient, Method}; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +pub use language::*; +use smol::fs::{self, File}; +use std::{path::PathBuf, str, sync::Arc}; +use util::{ResultExt, TryFutureExt}; + +use super::GithubRelease; + +pub struct CLspAdapter; + +impl super::LspAdapter for CLspAdapter { + fn name(&self) -> &'static str { + "clangd" + } + + fn fetch_latest_server_version( + &self, + http: Arc, + ) -> BoxFuture<'static, Result> { + async move { + let release = http + .send( + surf::RequestBuilder::new( + Method::Get, + http::Url::parse( + "https://api.github.com/repos/clangd/clangd/releases/latest", + ) + .unwrap(), + ) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) + .await + .map_err(|err| anyhow!("error fetching latest release: {}", err))? + .body_json::() + .await + .map_err(|err| anyhow!("error parsing latest release: {}", err))?; + let asset_name = format!("clangd-mac-{}.zip", release.name); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; + Ok(LspBinaryVersion { + name: release.name, + url: Some(asset.browser_download_url.clone()), + }) + } + .boxed() + } + + fn fetch_server_binary( + &self, + version: LspBinaryVersion, + http: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + async move { + let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); + let version_dir = container_dir.join(format!("clangd_{}", version.name)); + let binary_path = version_dir.join("bin/clangd"); + + if fs::metadata(&binary_path).await.is_err() { + let response = http + .send( + surf::RequestBuilder::new(Method::Get, version.url.unwrap()) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let mut file = File::create(&zip_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response, &mut file).await?; + + let unzip_status = smol::process::Command::new("unzip") + .current_dir(&container_dir) + .arg(&zip_path) + .output() + .await? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip clangd archive"))?; + } + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + async move { + let mut last_clangd_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_clangd_dir = Some(entry.path()); + } + } + let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let clangd_bin = clangd_dir.join("bin/clangd"); + if clangd_bin.exists() { + Ok(clangd_bin) + } else { + Err(anyhow!( + "missing clangd binary in directory {:?}", + clangd_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} +} diff --git a/crates/zed/languages/c/brackets.scm b/crates/zed/src/languages/c/brackets.scm similarity index 100% rename from crates/zed/languages/c/brackets.scm rename to crates/zed/src/languages/c/brackets.scm diff --git a/crates/zed/languages/c/config.toml b/crates/zed/src/languages/c/config.toml similarity index 100% rename from crates/zed/languages/c/config.toml rename to crates/zed/src/languages/c/config.toml diff --git a/crates/zed/languages/c/highlights.scm b/crates/zed/src/languages/c/highlights.scm similarity index 100% rename from crates/zed/languages/c/highlights.scm rename to crates/zed/src/languages/c/highlights.scm diff --git a/crates/zed/languages/c/indents.scm b/crates/zed/src/languages/c/indents.scm similarity index 100% rename from crates/zed/languages/c/indents.scm rename to crates/zed/src/languages/c/indents.scm diff --git a/crates/zed/languages/c/outline.scm b/crates/zed/src/languages/c/outline.scm similarity index 100% rename from crates/zed/languages/c/outline.scm rename to crates/zed/src/languages/c/outline.scm diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs new file mode 100644 index 0000000000000000000000000000000000000000..bb7744714f415a4db7ee1df25cb0f65027a371b7 --- /dev/null +++ b/crates/zed/src/languages/json.rs @@ -0,0 +1,131 @@ +use anyhow::{anyhow, Context, Result}; +use client::http::HttpClient; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use language::{LspAdapter, LspBinaryVersion}; +use serde::Deserialize; +use serde_json::json; +use smol::fs; +use std::{path::PathBuf, sync::Arc}; +use util::ResultExt; + +pub struct JsonLspAdapter; + +impl JsonLspAdapter { + const BIN_PATH: &'static str = + "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; +} + +impl LspAdapter for JsonLspAdapter { + fn name(&self) -> &'static str { + "vscode-json-languageserver" + } + + fn server_args(&self) -> &[&str] { + &["--stdio"] + } + + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result> { + async move { + #[derive(Deserialize)] + struct NpmInfo { + versions: Vec, + } + + let output = smol::process::Command::new("npm") + .args(["info", "vscode-json-languageserver", "--json"]) + .output() + .await?; + if !output.status.success() { + Err(anyhow!("failed to execute npm info"))?; + } + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + + Ok(LspBinaryVersion { + name: info + .versions + .pop() + .ok_or_else(|| anyhow!("no versions found in npm info"))?, + url: Default::default(), + }) + } + .boxed() + } + + fn fetch_server_binary( + &self, + version: LspBinaryVersion, + _: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + async move { + let version_dir = container_dir.join(&version.name); + fs::create_dir_all(&version_dir) + .await + .context("failed to create version directory")?; + let binary_path = version_dir.join(Self::BIN_PATH); + + if fs::metadata(&binary_path).await.is_err() { + let output = smol::process::Command::new("npm") + .current_dir(&version_dir) + .arg("install") + .arg(format!("vscode-json-languageserver@{}", version.name)) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!("failed to install vscode-json-languageserver"))?; + } + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let bin_path = last_version_dir.join(Self::BIN_PATH); + if bin_path.exists() { + Ok(bin_path) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + + fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} diff --git a/crates/zed/languages/json/brackets.scm b/crates/zed/src/languages/json/brackets.scm similarity index 100% rename from crates/zed/languages/json/brackets.scm rename to crates/zed/src/languages/json/brackets.scm diff --git a/crates/zed/languages/json/config.toml b/crates/zed/src/languages/json/config.toml similarity index 100% rename from crates/zed/languages/json/config.toml rename to crates/zed/src/languages/json/config.toml diff --git a/crates/zed/languages/json/highlights.scm b/crates/zed/src/languages/json/highlights.scm similarity index 100% rename from crates/zed/languages/json/highlights.scm rename to crates/zed/src/languages/json/highlights.scm diff --git a/crates/zed/languages/json/indents.scm b/crates/zed/src/languages/json/indents.scm similarity index 100% rename from crates/zed/languages/json/indents.scm rename to crates/zed/src/languages/json/indents.scm diff --git a/crates/zed/languages/json/outline.scm b/crates/zed/src/languages/json/outline.scm similarity index 100% rename from crates/zed/languages/json/outline.scm rename to crates/zed/src/languages/json/outline.scm diff --git a/crates/zed/languages/markdown/config.toml b/crates/zed/src/languages/markdown/config.toml similarity index 100% rename from crates/zed/languages/markdown/config.toml rename to crates/zed/src/languages/markdown/config.toml diff --git a/crates/zed/languages/markdown/highlights.scm b/crates/zed/src/languages/markdown/highlights.scm similarity index 100% rename from crates/zed/languages/markdown/highlights.scm rename to crates/zed/src/languages/markdown/highlights.scm diff --git a/crates/zed/src/language.rs b/crates/zed/src/languages/rust.rs similarity index 57% rename from crates/zed/src/language.rs rename to crates/zed/src/languages/rust.rs index 50540bc82cb5a6059b4fc2a02a1a3f33e3d741fc..2d4c6b3d3673c1117abbe86fcfc48044f283cf05 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/languages/rust.rs @@ -1,37 +1,17 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use client::http::{self, HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; -use gpui::Task; pub use language::*; use lazy_static::lazy_static; use regex::Regex; -use rust_embed::RustEmbed; -use serde::Deserialize; -use serde_json::json; use smol::fs::{self, File}; use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; use util::{ResultExt, TryFutureExt}; -#[derive(RustEmbed)] -#[folder = "languages"] -struct LanguageDir; +use super::GithubRelease; -struct RustLspAdapter; -struct CLspAdapter; -struct JsonLspAdapter; - -#[derive(Deserialize)] -struct GithubRelease { - name: String, - assets: Vec, -} - -#[derive(Deserialize)] -struct GithubReleaseAsset { - name: String, - browser_download_url: http::Url, -} +pub struct RustLspAdapter; impl LspAdapter for RustLspAdapter { fn name(&self) -> &'static str { @@ -287,353 +267,11 @@ impl LspAdapter for RustLspAdapter { } } -impl LspAdapter for CLspAdapter { - fn name(&self) -> &'static str { - "clangd" - } - - fn fetch_latest_server_version( - &self, - http: Arc, - ) -> BoxFuture<'static, Result> { - async move { - let release = http - .send( - surf::RequestBuilder::new( - Method::Get, - http::Url::parse( - "https://api.github.com/repos/clangd/clangd/releases/latest", - ) - .unwrap(), - ) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error fetching latest release: {}", err))? - .body_json::() - .await - .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let asset_name = format!("clangd-mac-{}.zip", release.name); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(LspBinaryVersion { - name: release.name, - url: Some(asset.browser_download_url.clone()), - }) - } - .boxed() - } - - fn fetch_server_binary( - &self, - version: LspBinaryVersion, - http: Arc, - container_dir: PathBuf, - ) -> BoxFuture<'static, Result> { - async move { - let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); - let version_dir = container_dir.join(format!("clangd_{}", version.name)); - let binary_path = version_dir.join("bin/clangd"); - - if fs::metadata(&binary_path).await.is_err() { - let response = http - .send( - surf::RequestBuilder::new(Method::Get, version.url.unwrap()) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error downloading release: {}", err))?; - let mut file = File::create(&zip_path).await?; - if !response.status().is_success() { - Err(anyhow!( - "download failed with status {}", - response.status().to_string() - ))?; - } - futures::io::copy(response, &mut file).await?; - - let unzip_status = smol::process::Command::new("unzip") - .current_dir(&container_dir) - .arg(&zip_path) - .output() - .await? - .status; - if !unzip_status.success() { - Err(anyhow!("failed to unzip clangd archive"))?; - } - - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } - - Ok(binary_path) - } - .boxed() - } - - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { - async move { - let mut last_clangd_dir = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_dir() { - last_clangd_dir = Some(entry.path()); - } - } - let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let clangd_bin = clangd_dir.join("bin/clangd"); - if clangd_bin.exists() { - Ok(clangd_bin) - } else { - Err(anyhow!( - "missing clangd binary in directory {:?}", - clangd_dir - )) - } - } - .log_err() - .boxed() - } - - fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} -} - -impl JsonLspAdapter { - const BIN_PATH: &'static str = - "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; -} - -impl LspAdapter for JsonLspAdapter { - fn name(&self) -> &'static str { - "vscode-json-languageserver" - } - - fn server_args(&self) -> &[&str] { - &["--stdio"] - } - - fn fetch_latest_server_version( - &self, - _: Arc, - ) -> BoxFuture<'static, Result> { - async move { - #[derive(Deserialize)] - struct NpmInfo { - versions: Vec, - } - - let output = smol::process::Command::new("npm") - .args(["info", "vscode-json-languageserver", "--json"]) - .output() - .await?; - if !output.status.success() { - Err(anyhow!("failed to execute npm info"))?; - } - let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; - - Ok(LspBinaryVersion { - name: info - .versions - .pop() - .ok_or_else(|| anyhow!("no versions found in npm info"))?, - url: Default::default(), - }) - } - .boxed() - } - - fn fetch_server_binary( - &self, - version: LspBinaryVersion, - _: Arc, - container_dir: PathBuf, - ) -> BoxFuture<'static, Result> { - async move { - let version_dir = container_dir.join(&version.name); - fs::create_dir_all(&version_dir) - .await - .context("failed to create version directory")?; - let binary_path = version_dir.join(Self::BIN_PATH); - - if fs::metadata(&binary_path).await.is_err() { - let output = smol::process::Command::new("npm") - .current_dir(&version_dir) - .arg("install") - .arg(format!("vscode-json-languageserver@{}", version.name)) - .output() - .await - .context("failed to run npm install")?; - if !output.status.success() { - Err(anyhow!("failed to install vscode-json-languageserver"))?; - } - - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } - - Ok(binary_path) - } - .boxed() - } - - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { - async move { - let mut last_version_dir = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_dir() { - last_version_dir = Some(entry.path()); - } - } - let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let bin_path = last_version_dir.join(Self::BIN_PATH); - if bin_path.exists() { - Ok(bin_path) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - last_version_dir - )) - } - } - .log_err() - .boxed() - } - - fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} - - fn initialization_options(&self) -> Option { - Some(json!({ - "provideFormatter": true - })) - } -} - -pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { - let languages = LanguageRegistry::new(login_shell_env_loaded); - for (name, grammar, lsp_adapter) in [ - ( - "c", - tree_sitter_c::language(), - Some(Arc::new(CLspAdapter) as Arc), - ), - ( - "json", - tree_sitter_json::language(), - Some(Arc::new(JsonLspAdapter)), - ), - ( - "markdown", - tree_sitter_markdown::language(), - None, // - ), - ( - "rust", - tree_sitter_rust::language(), - Some(Arc::new(RustLspAdapter)), - ), - ( - "tsx", - tree_sitter_typescript::language_tsx(), - None, // - ), - ( - "typescript", - tree_sitter_typescript::language_typescript(), - None, // - ), - ] { - languages.add(Arc::new(language(name, grammar, lsp_adapter))); - } - languages -} - -fn language( - name: &str, - grammar: tree_sitter::Language, - lsp_adapter: Option>, -) -> Language { - let config = toml::from_slice( - &LanguageDir::get(&format!("{}/config.toml", name)) - .unwrap() - .data, - ) - .unwrap(); - let mut language = Language::new(config, Some(grammar)); - - if let Some(query) = load_query(name, "/highlights") { - language = language - .with_highlights_query(query.as_ref()) - .expect("failed to evaluate highlights query"); - } - if let Some(query) = load_query(name, "/brackets") { - language = language - .with_brackets_query(query.as_ref()) - .expect("failed to load brackets query"); - } - if let Some(query) = load_query(name, "/indents") { - language = language - .with_indents_query(query.as_ref()) - .expect("failed to load indents query"); - } - if let Some(query) = load_query(name, "/outline") { - language = language - .with_outline_query(query.as_ref()) - .expect("failed to load outline query"); - } - if let Some(lsp_adapter) = lsp_adapter { - language = language.with_lsp_adapter(lsp_adapter) - } - language -} - -fn load_query(name: &str, filename_prefix: &str) -> Option> { - let mut result = None; - for path in LanguageDir::iter() { - if let Some(remainder) = path.strip_prefix(name) { - if remainder.starts_with(filename_prefix) { - let contents = match LanguageDir::get(path.as_ref()).unwrap().data { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - }; - match &mut result { - None => result = Some(contents), - Some(r) => r.to_mut().push_str(contents.as_ref()), - } - } - } - } - result -} - #[cfg(test)] mod tests { use super::*; + use crate::languages::{language, LspAdapter}; use gpui::color::Color; - use language::LspAdapter; use theme::SyntaxTheme; #[test] diff --git a/crates/zed/languages/rust/brackets.scm b/crates/zed/src/languages/rust/brackets.scm similarity index 100% rename from crates/zed/languages/rust/brackets.scm rename to crates/zed/src/languages/rust/brackets.scm diff --git a/crates/zed/languages/rust/config.toml b/crates/zed/src/languages/rust/config.toml similarity index 100% rename from crates/zed/languages/rust/config.toml rename to crates/zed/src/languages/rust/config.toml diff --git a/crates/zed/languages/rust/highlights.scm b/crates/zed/src/languages/rust/highlights.scm similarity index 100% rename from crates/zed/languages/rust/highlights.scm rename to crates/zed/src/languages/rust/highlights.scm diff --git a/crates/zed/languages/rust/indents.scm b/crates/zed/src/languages/rust/indents.scm similarity index 100% rename from crates/zed/languages/rust/indents.scm rename to crates/zed/src/languages/rust/indents.scm diff --git a/crates/zed/languages/rust/outline.scm b/crates/zed/src/languages/rust/outline.scm similarity index 100% rename from crates/zed/languages/rust/outline.scm rename to crates/zed/src/languages/rust/outline.scm diff --git a/crates/zed/languages/tsx/brackets.scm b/crates/zed/src/languages/tsx/brackets.scm similarity index 100% rename from crates/zed/languages/tsx/brackets.scm rename to crates/zed/src/languages/tsx/brackets.scm diff --git a/crates/zed/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml similarity index 100% rename from crates/zed/languages/tsx/config.toml rename to crates/zed/src/languages/tsx/config.toml diff --git a/crates/zed/languages/tsx/highlights-jsx.scm b/crates/zed/src/languages/tsx/highlights-jsx.scm similarity index 100% rename from crates/zed/languages/tsx/highlights-jsx.scm rename to crates/zed/src/languages/tsx/highlights-jsx.scm diff --git a/crates/zed/languages/tsx/highlights.scm b/crates/zed/src/languages/tsx/highlights.scm similarity index 100% rename from crates/zed/languages/tsx/highlights.scm rename to crates/zed/src/languages/tsx/highlights.scm diff --git a/crates/zed/languages/tsx/indents.scm b/crates/zed/src/languages/tsx/indents.scm similarity index 100% rename from crates/zed/languages/tsx/indents.scm rename to crates/zed/src/languages/tsx/indents.scm diff --git a/crates/zed/languages/tsx/outline.scm b/crates/zed/src/languages/tsx/outline.scm similarity index 100% rename from crates/zed/languages/tsx/outline.scm rename to crates/zed/src/languages/tsx/outline.scm diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs new file mode 100644 index 0000000000000000000000000000000000000000..59b7d225f9cb4dc1850e3a42280011dd37d7454d --- /dev/null +++ b/crates/zed/src/languages/typescript.rs @@ -0,0 +1,121 @@ +pub struct TypeScriptLspAdapter; + +impl TypeScriptLspAdapter { + const BIN_PATH: &'static str = + "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; +} + +impl super::LspAdapter for TypeScriptLspAdapter { + fn name(&self) -> &'static str { + "typescript-language-server" + } + + fn server_args(&self) -> &[&str] { + &["--stdio"] + } + + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result> { + async move { + #[derive(Deserialize)] + struct NpmInfo { + versions: Vec, + } + + let output = smol::process::Command::new("npm") + .args(["info", "vscode-json-languageserver", "--json"]) + .output() + .await?; + if !output.status.success() { + Err(anyhow!("failed to execute npm info"))?; + } + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + + Ok(LspBinaryVersion { + name: info + .versions + .pop() + .ok_or_else(|| anyhow!("no versions found in npm info"))?, + url: Default::default(), + }) + } + .boxed() + } + + fn fetch_server_binary( + &self, + version: LspBinaryVersion, + _: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + async move { + let version_dir = container_dir.join(&version.name); + fs::create_dir_all(&version_dir) + .await + .context("failed to create version directory")?; + let binary_path = version_dir.join(Self::BIN_PATH); + + if fs::metadata(&binary_path).await.is_err() { + let output = smol::process::Command::new("npm") + .current_dir(&version_dir) + .arg("install") + .arg(format!("vscode-json-languageserver@{}", version.name)) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!("failed to install vscode-json-languageserver"))?; + } + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let bin_path = last_version_dir.join(Self::BIN_PATH); + if bin_path.exists() { + Ok(bin_path) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + + fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} diff --git a/crates/zed/languages/typescript/brackets.scm b/crates/zed/src/languages/typescript/brackets.scm similarity index 100% rename from crates/zed/languages/typescript/brackets.scm rename to crates/zed/src/languages/typescript/brackets.scm diff --git a/crates/zed/languages/typescript/config.toml b/crates/zed/src/languages/typescript/config.toml similarity index 100% rename from crates/zed/languages/typescript/config.toml rename to crates/zed/src/languages/typescript/config.toml diff --git a/crates/zed/languages/typescript/highlights.scm b/crates/zed/src/languages/typescript/highlights.scm similarity index 100% rename from crates/zed/languages/typescript/highlights.scm rename to crates/zed/src/languages/typescript/highlights.scm diff --git a/crates/zed/languages/typescript/indents.scm b/crates/zed/src/languages/typescript/indents.scm similarity index 100% rename from crates/zed/languages/typescript/indents.scm rename to crates/zed/src/languages/typescript/indents.scm diff --git a/crates/zed/languages/typescript/outline.scm b/crates/zed/src/languages/typescript/outline.scm similarity index 100% rename from crates/zed/languages/typescript/outline.scm rename to crates/zed/src/languages/typescript/outline.scm diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 63721346c399ca60ea4f701f330ad78fb61d63e0..49efc9ade2af2749cca42cc20362a1c174dea833 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -19,7 +19,7 @@ use workspace::{ AppState, OpenNew, OpenParams, OpenPaths, Settings, }; use zed::{ - self, assets::Assets, build_window_options, build_workspace, fs::RealFs, language, menus, + self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus, }; fn main() { @@ -34,7 +34,7 @@ fn main() { let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme) .unwrap() .with_overrides( - language::PLAIN_TEXT.name(), + languages::PLAIN_TEXT.name(), settings::LanguageOverride { soft_wrap: Some(settings::SoftWrap::PreferredLineLength), ..Default::default() @@ -60,7 +60,7 @@ fn main() { app.run(move |cx| { let http = http::client(); let client = client::Client::new(http.clone()); - let mut languages = language::build_language_registry(login_shell_env_loaded); + let mut languages = languages::build_language_registry(login_shell_env_loaded); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let channel_list = cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1302d54067810653140367b40966e869edcf8372..25aa011c9b5ec801a79f8e7a5720904e862a6b4a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,5 +1,5 @@ pub mod assets; -pub mod language; +pub mod languages; pub mod menus; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -557,7 +557,7 @@ mod tests { assert_eq!(editor.title(cx), "untitled"); assert!(Arc::ptr_eq( editor.language(cx).unwrap(), - &language::PLAIN_TEXT + &languages::PLAIN_TEXT )); editor.handle_input(&editor::Input("hi".into()), cx); assert!(editor.is_dirty(cx)); @@ -647,7 +647,7 @@ mod tests { editor.update(cx, |editor, cx| { assert!(Arc::ptr_eq( editor.language(cx).unwrap(), - &language::PLAIN_TEXT + &languages::PLAIN_TEXT )); editor.handle_input(&editor::Input("hi".into()), cx); assert!(editor.is_dirty(cx.as_ref())); From 0e1d371a67b1483e4ab7a426b12938cb5b0abdde Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 29 Mar 2022 13:42:21 -0700 Subject: [PATCH 05/24] Add typescript language server Currently not tested for tsx files Co-authored-by: Max Brunsfeld --- crates/language/src/language.rs | 9 ++-- crates/project/src/project.rs | 8 +-- crates/zed/src/languages.rs | 4 +- crates/zed/src/languages/c.rs | 15 +++--- crates/zed/src/languages/json.rs | 23 ++++----- crates/zed/src/languages/rust.rs | 15 +++--- crates/zed/src/languages/typescript.rs | 69 +++++++++++++++++++------- 7 files changed, 90 insertions(+), 53 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 5b917c0fdf1e6141e7991b89d8a7f55a1455b4f6..6e400c4a6fa2e5a5f35ee84ee9339cba190a2741 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -20,6 +20,7 @@ use parking_lot::{Mutex, RwLock}; use serde::Deserialize; use serde_json::Value; use std::{ + any::Any, cell::RefCell, ops::Range, path::{Path, PathBuf}, @@ -61,9 +62,9 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } -pub struct LspBinaryVersion { +pub struct GitHubLspBinaryVersion { pub name: String, - pub url: Option, + pub url: http::Url, } pub trait LspAdapter: 'static + Send + Sync { @@ -71,10 +72,10 @@ pub trait LspAdapter: 'static + Send + Sync { fn fetch_latest_server_version( &self, http: Arc, - ) -> BoxFuture<'static, Result>; + ) -> BoxFuture<'static, Result>>; fn fetch_server_binary( &self, - version: LspBinaryVersion, + version: Box, http: Arc, container_dir: PathBuf, ) -> BoxFuture<'static, Result>; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1a41475909d2a0a395ae5fcc7f37541e54cf25fc..f4d5e9ee11e855cbfe43e8a05dd6ad9a06027faf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2278,11 +2278,12 @@ impl Project { Ok(completions .into_iter() .filter_map(|lsp_completion| { - let (old_range, new_text) = match lsp_completion.text_edit.as_ref()? { - lsp::CompletionTextEdit::Edit(edit) => { + let (old_range, new_text) = match lsp_completion.text_edit.as_ref() { + Some(lsp::CompletionTextEdit::Edit(edit)) => { (range_from_lsp(edit.range), edit.new_text.clone()) } - lsp::CompletionTextEdit::InsertAndReplace(_) => { + None => (position..position, lsp_completion.label.clone()), + Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { log::info!("unsupported insert/replace completion"); return None; } @@ -2307,6 +2308,7 @@ impl Project { lsp_completion, }) } else { + log::info!("completion out of expected range"); None } }) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 607f4c30bb756935ce315a0378ce4caf1eb16b71..cc22247025a393cb104ee4317bc76a47155988d3 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,4 +1,4 @@ -use client::http::{self, HttpClient, Method}; +use client::http; use gpui::Task; pub use language::*; use rust_embed::RustEmbed; @@ -53,7 +53,7 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi ( "tsx", tree_sitter_typescript::language_tsx(), - None, // + Some(Arc::new(typescript::TypeScriptLspAdapter)), ), ( "typescript", diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 9ce3eab2f772d86fdcc7f7a717020480660a5ed5..cf4e2199676614de174a73647dbe67b5dfe67c97 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -3,7 +3,7 @@ use client::http::{self, HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; pub use language::*; use smol::fs::{self, File}; -use std::{path::PathBuf, str, sync::Arc}; +use std::{any::Any, path::PathBuf, str, sync::Arc}; use util::{ResultExt, TryFutureExt}; use super::GithubRelease; @@ -18,7 +18,7 @@ impl super::LspAdapter for CLspAdapter { fn fetch_latest_server_version( &self, http: Arc, - ) -> BoxFuture<'static, Result> { + ) -> BoxFuture<'static, Result>> { async move { let release = http .send( @@ -43,20 +43,21 @@ impl super::LspAdapter for CLspAdapter { .iter() .find(|asset| asset.name == asset_name) .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(LspBinaryVersion { + Ok(Box::new(GitHubLspBinaryVersion { name: release.name, - url: Some(asset.browser_download_url.clone()), - }) + url: asset.browser_download_url.clone(), + }) as Box<_>) } .boxed() } fn fetch_server_binary( &self, - version: LspBinaryVersion, + version: Box, http: Arc, container_dir: PathBuf, ) -> BoxFuture<'static, Result> { + let version = version.downcast::().unwrap(); async move { let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); let version_dir = container_dir.join(format!("clangd_{}", version.name)); @@ -65,7 +66,7 @@ impl super::LspAdapter for CLspAdapter { if fs::metadata(&binary_path).await.is_err() { let response = http .send( - surf::RequestBuilder::new(Method::Get, version.url.unwrap()) + surf::RequestBuilder::new(Method::Get, version.url) .middleware(surf::middleware::Redirect::default()) .build(), ) diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index bb7744714f415a4db7ee1df25cb0f65027a371b7..c1d0db2ed6d220773094a78c43d2df18c8317c51 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,12 +1,12 @@ use anyhow::{anyhow, Context, Result}; use client::http::HttpClient; use futures::{future::BoxFuture, FutureExt, StreamExt}; -use language::{LspAdapter, LspBinaryVersion}; +use language::LspAdapter; use serde::Deserialize; use serde_json::json; use smol::fs; -use std::{path::PathBuf, sync::Arc}; -use util::ResultExt; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::{ResultExt, TryFutureExt}; pub struct JsonLspAdapter; @@ -27,7 +27,7 @@ impl LspAdapter for JsonLspAdapter { fn fetch_latest_server_version( &self, _: Arc, - ) -> BoxFuture<'static, Result> { + ) -> BoxFuture<'static, Result>> { async move { #[derive(Deserialize)] struct NpmInfo { @@ -43,25 +43,24 @@ impl LspAdapter for JsonLspAdapter { } let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; - Ok(LspBinaryVersion { - name: info - .versions + Ok(Box::new( + info.versions .pop() .ok_or_else(|| anyhow!("no versions found in npm info"))?, - url: Default::default(), - }) + ) as Box<_>) } .boxed() } fn fetch_server_binary( &self, - version: LspBinaryVersion, + version: Box, _: Arc, container_dir: PathBuf, ) -> BoxFuture<'static, Result> { + let version = version.downcast::().unwrap(); async move { - let version_dir = container_dir.join(&version.name); + let version_dir = container_dir.join(version.as_str()); fs::create_dir_all(&version_dir) .await .context("failed to create version directory")?; @@ -71,7 +70,7 @@ impl LspAdapter for JsonLspAdapter { let output = smol::process::Command::new("npm") .current_dir(&version_dir) .arg("install") - .arg(format!("vscode-json-languageserver@{}", version.name)) + .arg(format!("vscode-json-languageserver@{}", version)) .output() .await .context("failed to run npm install")?; diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 2d4c6b3d3673c1117abbe86fcfc48044f283cf05..8b06c5cbd7c784752cd91fecded506a40f6f5423 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -6,7 +6,7 @@ pub use language::*; use lazy_static::lazy_static; use regex::Regex; use smol::fs::{self, File}; -use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; use util::{ResultExt, TryFutureExt}; use super::GithubRelease; @@ -21,7 +21,7 @@ impl LspAdapter for RustLspAdapter { fn fetch_latest_server_version( &self, http: Arc, - ) -> BoxFuture<'static, Result> { + ) -> BoxFuture<'static, Result>> { async move { let release = http .send( @@ -46,27 +46,28 @@ impl LspAdapter for RustLspAdapter { .iter() .find(|asset| asset.name == asset_name) .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(LspBinaryVersion { + Ok(Box::new(GitHubLspBinaryVersion { name: release.name, - url: Some(asset.browser_download_url.clone()), - }) + url: asset.browser_download_url.clone(), + }) as Box<_>) } .boxed() } fn fetch_server_binary( &self, - version: LspBinaryVersion, + version: Box, http: Arc, container_dir: PathBuf, ) -> BoxFuture<'static, Result> { async move { + let version = version.downcast::().unwrap(); let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name)); if fs::metadata(&destination_path).await.is_err() { let response = http .send( - surf::RequestBuilder::new(Method::Get, version.url.unwrap()) + surf::RequestBuilder::new(Method::Get, version.url) .middleware(surf::middleware::Redirect::default()) .build(), ) diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 59b7d225f9cb4dc1850e3a42280011dd37d7454d..f7547fdd4e651a03bc53028c591a6aa16b60878a 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -1,57 +1,86 @@ +use anyhow::{anyhow, Context, Result}; +use client::http::HttpClient; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use language::LspAdapter; +use serde::Deserialize; +use serde_json::json; +use smol::fs; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::{ResultExt, TryFutureExt}; + pub struct TypeScriptLspAdapter; impl TypeScriptLspAdapter { - const BIN_PATH: &'static str = - "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; + const BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js"; +} + +struct Versions { + typescript_version: String, + server_version: String, } -impl super::LspAdapter for TypeScriptLspAdapter { +impl LspAdapter for TypeScriptLspAdapter { fn name(&self) -> &'static str { "typescript-language-server" } fn server_args(&self) -> &[&str] { - &["--stdio"] + &["--stdio", "--tsserver-path", "node_modules/typescript/lib"] } fn fetch_latest_server_version( &self, _: Arc, - ) -> BoxFuture<'static, Result> { + ) -> BoxFuture<'static, Result>> { async move { #[derive(Deserialize)] struct NpmInfo { versions: Vec, } - let output = smol::process::Command::new("npm") - .args(["info", "vscode-json-languageserver", "--json"]) + let typescript_output = smol::process::Command::new("npm") + .args(["info", "typescript", "--json"]) + .output() + .await?; + if !typescript_output.status.success() { + Err(anyhow!("failed to execute npm info"))?; + } + let mut typescript_info: NpmInfo = serde_json::from_slice(&typescript_output.stdout)?; + + let server_output = smol::process::Command::new("npm") + .args(["info", "typescript-language-server", "--json"]) .output() .await?; - if !output.status.success() { + if !server_output.status.success() { Err(anyhow!("failed to execute npm info"))?; } - let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + let mut server_info: NpmInfo = serde_json::from_slice(&server_output.stdout)?; - Ok(LspBinaryVersion { - name: info + Ok(Box::new(Versions { + typescript_version: typescript_info .versions .pop() - .ok_or_else(|| anyhow!("no versions found in npm info"))?, - url: Default::default(), - }) + .ok_or_else(|| anyhow!("no versions found in typescript npm info"))?, + server_version: server_info.versions.pop().ok_or_else(|| { + anyhow!("no versions found in typescript language server npm info") + })?, + }) as Box<_>) } .boxed() } fn fetch_server_binary( &self, - version: LspBinaryVersion, + versions: Box, _: Arc, container_dir: PathBuf, ) -> BoxFuture<'static, Result> { + let versions = versions.downcast::().unwrap(); async move { - let version_dir = container_dir.join(&version.name); + let version_dir = container_dir.join(&format!( + "typescript-{}:server-{}", + versions.typescript_version, versions.server_version + )); fs::create_dir_all(&version_dir) .await .context("failed to create version directory")?; @@ -61,12 +90,16 @@ impl super::LspAdapter for TypeScriptLspAdapter { let output = smol::process::Command::new("npm") .current_dir(&version_dir) .arg("install") - .arg(format!("vscode-json-languageserver@{}", version.name)) + .arg(format!("typescript@{}", versions.typescript_version)) + .arg(format!( + "typescript-language-server@{}", + versions.server_version + )) .output() .await .context("failed to run npm install")?; if !output.status.success() { - Err(anyhow!("failed to install vscode-json-languageserver"))?; + Err(anyhow!("failed to install typescript-language-server"))?; } if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { From 158d9879652b4cd24fb2b381b1e938b1543b1892 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 29 Mar 2022 16:57:18 -0700 Subject: [PATCH 06/24] Start work on allowing language servers to support multiple languages --- crates/language/src/language.rs | 32 ++++-- crates/project/src/lsp_command.rs | 23 ++-- crates/project/src/project.rs | 141 +++++++++++++------------ crates/rpc/proto/zed.proto | 2 +- crates/zed/src/languages/c.rs | 6 +- crates/zed/src/languages/json.rs | 6 +- crates/zed/src/languages/rust.rs | 4 +- crates/zed/src/languages/typescript.rs | 6 +- 8 files changed, 118 insertions(+), 102 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 6e400c4a6fa2e5a5f35ee84ee9339cba190a2741..8d573574fda99bf1f2c862794bba6049107ba0b4 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -8,7 +8,7 @@ mod tests; use anyhow::{anyhow, Context, Result}; use client::http::{self, HttpClient}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use futures::{ future::{BoxFuture, Shared}, FutureExt, TryFutureExt, @@ -67,8 +67,11 @@ pub struct GitHubLspBinaryVersion { pub url: http::Url, } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct LanguageServerName(pub Arc); + pub trait LspAdapter: 'static + Send + Sync { - fn name(&self) -> &'static str; + fn name(&self) -> LanguageServerName; fn fetch_latest_server_version( &self, http: Arc, @@ -159,7 +162,6 @@ pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, pub(crate) adapter: Option>, - lsp_binary_path: Mutex>>>>>, } pub struct Grammar { @@ -186,6 +188,12 @@ pub struct LanguageRegistry { lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, login_shell_env_loaded: Shared>, + lsp_binary_paths: Mutex< + HashMap< + LanguageServerName, + Shared>>>, + >, + >, } impl LanguageRegistry { @@ -197,6 +205,7 @@ impl LanguageRegistry { lsp_binary_statuses_tx, lsp_binary_statuses_rx, login_shell_env_loaded: login_shell_env_loaded.shared(), + lsp_binary_paths: Default::default(), } } @@ -246,7 +255,7 @@ impl LanguageRegistry { } pub fn start_language_server( - &self, + self: &Arc, language: Arc, root_path: Arc, http_client: Arc, @@ -291,16 +300,18 @@ impl LanguageRegistry { .ok_or_else(|| anyhow!("language server download directory has not been assigned")) .log_err()?; + let this = self.clone(); let adapter = language.adapter.clone()?; let background = cx.background().clone(); let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone(); Some(cx.background().spawn(async move { login_shell_env_loaded.await; - let server_binary_path = language - .lsp_binary_path + let server_binary_path = this + .lsp_binary_paths .lock() - .get_or_insert_with(|| { + .entry(adapter.name()) + .or_insert_with(|| { get_server_binary_path( adapter.clone(), language.clone(), @@ -342,7 +353,7 @@ async fn get_server_binary_path( download_dir: Arc, statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { - let container_dir = download_dir.join(adapter.name()); + let container_dir = download_dir.join(adapter.name().0.as_ref()); if !container_dir.exists() { smol::fs::create_dir_all(&container_dir) .await @@ -415,10 +426,13 @@ impl Language { }) }), adapter: None, - lsp_binary_path: Default::default(), } } + pub fn lsp_adapter(&self) -> Option> { + self.adapter.clone() + } + pub fn with_highlights_query(mut self, source: &str) -> Result { let grammar = self .grammar diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4867ada7cb0f86b124cc7c11f5a568df83236eb7..b6f007659dad840b3ee9145131fd522498f5524d 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -223,22 +223,19 @@ impl LspCommand for PerformRename { mut cx: AsyncAppContext, ) -> Result { if let Some(edit) = message { - let language_server = project + let (lsp_adapter, lsp_server) = project .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; - let language = buffer - .read_with(&cx, |buffer, _| buffer.language().cloned()) - .ok_or_else(|| anyhow!("no language for buffer"))?; Project::deserialize_workspace_edit( project, edit, self.push_to_history, - language.name(), - language_server, + lsp_adapter, + lsp_server, &mut cx, ) .await @@ -343,16 +340,13 @@ impl LspCommand for GetDefinition { mut cx: AsyncAppContext, ) -> Result> { let mut definitions = Vec::new(); - let language_server = project + let (lsp_adapter, language_server) = project .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; - let language = buffer - .read_with(&cx, |buffer, _| buffer.language().cloned()) - .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(message) = message { let mut unresolved_locations = Vec::new(); @@ -377,7 +371,7 @@ impl LspCommand for GetDefinition { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( target_uri, - language.name(), + lsp_adapter.clone(), language_server.clone(), cx, ) @@ -521,16 +515,13 @@ impl LspCommand for GetReferences { mut cx: AsyncAppContext, ) -> Result> { let mut references = Vec::new(); - let language_server = project + let (lsp_adapter, language_server) = project .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; - let language = buffer - .read_with(&cx, |buffer, _| buffer.language().cloned()) - .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(locations) = locations { for lsp_location in locations { @@ -538,7 +529,7 @@ impl LspCommand for GetReferences { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( lsp_location.uri, - language.name(), + lsp_adapter.clone(), language_server.clone(), cx, ) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f4d5e9ee11e855cbfe43e8a05dd6ad9a06027faf..6f550d10206e1f90475937e34ed5940a6b78531b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -18,8 +18,8 @@ use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, - LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition, - ToOffset, ToPointUtf16, Transaction, + LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt, Operation, Patch, PointUtf16, + TextBufferSnapshot, ToLspPosition, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -57,8 +57,10 @@ pub struct Project { worktrees: Vec, active_entry: Option, languages: Arc, - language_servers: HashMap<(WorktreeId, Arc), Arc>, - started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, + language_servers: + HashMap<(WorktreeId, LanguageServerName), (Arc, Arc)>, + started_language_servers: + HashMap<(WorktreeId, LanguageServerName), Task>>>, language_server_statuses: BTreeMap, language_server_settings: Arc>, next_language_server_id: usize, @@ -185,7 +187,7 @@ pub struct DocumentHighlight { pub struct Symbol { pub source_worktree_id: WorktreeId, pub worktree_id: WorktreeId, - pub language_name: String, + pub language_server_name: LanguageServerName, pub path: PathBuf, pub label: CodeLabel, pub name: String, @@ -957,8 +959,8 @@ impl Project { fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, - lang_name: Arc, - lang_server: Arc, + lsp_adapter: Arc, + lsp_server: Arc, cx: &mut ModelContext, ) -> Task>> { cx.spawn(|this, mut cx| async move { @@ -976,8 +978,10 @@ impl Project { }) .await?; this.update(&mut cx, |this, cx| { - this.language_servers - .insert((worktree.read(cx).id(), lang_name), lang_server); + this.language_servers.insert( + (worktree.read(cx).id(), lsp_adapter.name()), + (lsp_adapter, lsp_server), + ); }); (worktree, PathBuf::new()) }; @@ -1120,7 +1124,7 @@ impl Project { } } - if let Some(server) = language_server { + if let Some((_, server)) = language_server { server .notify::( lsp::DidOpenTextDocumentParams { @@ -1153,7 +1157,7 @@ impl Project { 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) { + if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) { server .notify::( lsp::DidCloseTextDocumentParams { @@ -1189,7 +1193,7 @@ impl Project { cx.background().spawn(request).detach_and_log_err(cx); } BufferEvent::Edited { .. } => { - let language_server = self + let (_, language_server) = self .language_server_for_buffer(buffer.read(cx), cx)? .clone(); let buffer = buffer.read(cx); @@ -1262,11 +1266,11 @@ impl Project { fn language_servers_for_worktree( &self, worktree_id: WorktreeId, - ) -> impl Iterator)> { + ) -> impl Iterator, Arc)> { self.language_servers.iter().filter_map( - move |((language_server_worktree_id, language_name), server)| { + move |((language_server_worktree_id, _), server)| { if *language_server_worktree_id == worktree_id { - Some((language_name.as_ref(), server)) + Some(server) } else { None } @@ -1302,7 +1306,12 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - let key = (worktree_id, language.name()); + let adapter = if let Some(adapter) = language.lsp_adapter() { + adapter + } else { + return; + }; + let key = (worktree_id, adapter.name()); self.started_language_servers .entry(key.clone()) .or_insert_with(|| { @@ -1416,7 +1425,7 @@ impl Project { let language_server = language_server.initialize().await.log_err()?; this.update(&mut cx, |this, cx| { this.language_servers - .insert(key.clone(), language_server.clone()); + .insert(key.clone(), (adapter, language_server.clone())); this.language_server_statuses.insert( server_id, LanguageServerStatus { @@ -1459,7 +1468,10 @@ impl Project { } else { continue; }; - if (file.worktree.read(cx).id(), language.name()) != key { + if file.worktree.read(cx).id() != key.0 + || language.lsp_adapter().map(|a| a.name()) + != Some(key.1.clone()) + { continue; } @@ -1675,7 +1687,7 @@ impl Project { } pub fn set_language_server_settings(&mut self, settings: serde_json::Value) { - for server in self.language_servers.values() { + for (_, server) in self.language_servers.values() { server .notify::( lsp::DidChangeConfigurationParams { @@ -1925,7 +1937,7 @@ impl Project { let buffer = buffer_handle.read(cx); if let Some(file) = File::from_dyn(buffer.file()) { if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) { - if let Some(server) = self.language_server_for_buffer(buffer, cx) { + if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) { local_buffers.push((buffer_handle, buffer_abs_path, server.clone())); } } else { @@ -2062,25 +2074,24 @@ impl Project { pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { if self.is_local() { let mut language_servers = HashMap::default(); - for ((worktree_id, language_name), language_server) in self.language_servers.iter() { - if let Some((worktree, language)) = self + for ((worktree_id, _), (lsp_adapter, language_server)) in self.language_servers.iter() { + if let Some(worktree) = self .worktree_for_id(*worktree_id, cx) .and_then(|worktree| worktree.read(cx).as_local()) - .zip(self.languages.get_language(language_name)) { language_servers .entry(Arc::as_ptr(language_server)) .or_insert(( + lsp_adapter.clone(), language_server.clone(), *worktree_id, worktree.abs_path().clone(), - language.clone(), )); } } let mut requests = Vec::new(); - for (language_server, _, _, _) in language_servers.values() { + for (_, language_server, _, _) in language_servers.values() { requests.push(language_server.request::( lsp::WorkspaceSymbolParams { query: query.to_string(), @@ -2095,7 +2106,7 @@ impl Project { let mut symbols = Vec::new(); if let Some(this) = this.upgrade(&cx) { this.read_with(&cx, |this, cx| { - for ((_, source_worktree_id, worktree_abs_path, language), lsp_symbols) in + for ((adapter, _, source_worktree_id, worktree_abs_path), lsp_symbols) in language_servers.into_values().zip(responses) { symbols.extend(lsp_symbols.into_iter().flatten().filter_map( @@ -2112,8 +2123,13 @@ impl Project { path = relativize_path(&worktree_abs_path, &abs_path); } - let label = language - .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind) + let label = this + .languages + .select_language(&path) + .and_then(|language| { + language + .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind) + }) .unwrap_or_else(|| { CodeLabel::plain(lsp_symbol.name.clone(), None) }); @@ -2122,7 +2138,7 @@ impl Project { Some(Symbol { source_worktree_id, worktree_id, - language_name: language.name().to_string(), + language_server_name: adapter.name(), name: lsp_symbol.name, kind: lsp_symbol.kind, label, @@ -2169,9 +2185,9 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { if self.is_local() { - let language_server = if let Some(server) = self.language_servers.get(&( + let (lsp_adapter, language_server) = if let Some(server) = self.language_servers.get(&( symbol.source_worktree_id, - Arc::from(symbol.language_name.as_str()), + symbol.language_server_name.clone(), )) { server.clone() } else { @@ -2196,12 +2212,7 @@ impl Project { return Task::ready(Err(anyhow!("invalid symbol path"))); }; - self.open_local_buffer_via_lsp( - symbol_uri, - Arc::from(symbol.language_name.as_str()), - language_server, - cx, - ) + self.open_local_buffer_via_lsp(symbol_uri, lsp_adapter, language_server, cx) } else if let Some(project_id) = self.remote_id() { let request = self.client.request(proto::OpenBufferForSymbol { project_id, @@ -2242,7 +2253,7 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_server = + let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(source_buffer, cx) { server.clone() } else { @@ -2356,7 +2367,8 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + { server.clone() } else { return Task::ready(Ok(Default::default())); @@ -2447,7 +2459,8 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + { server.clone() } else { return Task::ready(Ok(Default::default())); @@ -2534,16 +2547,12 @@ impl Project { ) -> Task> { if self.is_local() { let buffer = buffer_handle.read(cx); - let lang_name = if let Some(lang) = buffer.language() { - lang.name() - } else { - return Task::ready(Ok(Default::default())); - }; - let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { - server.clone() - } else { - return Task::ready(Ok(Default::default())); - }; + let (lsp_adapter, lang_server) = + if let Some(server) = self.language_server_for_buffer(buffer, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; let range = action.range.to_point_utf16(buffer); cx.spawn(|this, mut cx| async move { @@ -2580,7 +2589,7 @@ impl Project { this, edit, push_to_history, - lang_name, + lsp_adapter, lang_server, &mut cx, ) @@ -2616,7 +2625,7 @@ impl Project { this: ModelHandle, edit: lsp::WorkspaceEdit, push_to_history: bool, - language_name: Arc, + lsp_adapter: Arc, language_server: Arc, cx: &mut AsyncAppContext, ) -> Result { @@ -2693,7 +2702,7 @@ impl Project { .update(cx, |this, cx| { this.open_local_buffer_via_lsp( op.text_document.uri, - language_name.clone(), + lsp_adapter.clone(), language_server.clone(), cx, ) @@ -2988,7 +2997,7 @@ impl Project { let buffer = buffer_handle.read(cx); if self.is_local() { let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let Some((file, language_server)) = + if let Some((file, (_, language_server))) = file.zip(self.language_server_for_buffer(buffer, cx).cloned()) { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); @@ -4086,9 +4095,8 @@ impl Project { } fn deserialize_symbol(&self, serialized_symbol: proto::Symbol) -> Result { - let language = self - .languages - .get_language(&serialized_symbol.language_name); + let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id); + let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id); let start = serialized_symbol .start .ok_or_else(|| anyhow!("invalid start"))?; @@ -4096,15 +4104,17 @@ impl Project { .end .ok_or_else(|| anyhow!("invalid end"))?; let kind = unsafe { mem::transmute(serialized_symbol.kind) }; + let path = PathBuf::from(serialized_symbol.path); + let language = self.languages.select_language(&path); Ok(Symbol { - source_worktree_id: WorktreeId::from_proto(serialized_symbol.source_worktree_id), - worktree_id: WorktreeId::from_proto(serialized_symbol.worktree_id), - language_name: serialized_symbol.language_name.clone(), + source_worktree_id, + worktree_id, + language_server_name: LanguageServerName(serialized_symbol.language_server_name.into()), label: language .and_then(|language| language.label_for_symbol(&serialized_symbol.name, kind)) .unwrap_or_else(|| CodeLabel::plain(serialized_symbol.name.clone(), None)), name: serialized_symbol.name, - path: PathBuf::from(serialized_symbol.path), + path, range: PointUtf16::new(start.row, start.column)..PointUtf16::new(end.row, end.column), kind, signature: serialized_symbol @@ -4349,10 +4359,11 @@ impl Project { &self, buffer: &Buffer, cx: &AppContext, - ) -> Option<&Arc> { + ) -> Option<&(Arc, Arc)> { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let worktree_id = file.worktree_id(cx); - self.language_servers.get(&(worktree_id, language.name())) + self.language_servers + .get(&(worktree_id, language.lsp_adapter()?.name())) } else { None } @@ -4466,7 +4477,7 @@ impl Entity for Project { let shutdown_futures = self .language_servers .drain() - .filter_map(|(_, server)| server.shutdown()) + .filter_map(|(_, (_, server))| server.shutdown()) .collect::>(); Some( async move { @@ -4537,7 +4548,7 @@ fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { proto::Symbol { source_worktree_id: symbol.source_worktree_id.to_proto(), worktree_id: symbol.worktree_id.to_proto(), - language_name: symbol.language_name.clone(), + language_server_name: symbol.language_server_name.0.to_string(), name: symbol.name.clone(), kind: unsafe { mem::transmute(symbol.kind) }, path: symbol.path.to_string_lossy().to_string(), diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 9d25e66190b14bc7d4624885ec7da3dc57acb61b..cef0e60f0fe3bea6d90341954e6be3f923187a29 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -229,7 +229,7 @@ message GetProjectSymbolsResponse { message Symbol { uint64 source_worktree_id = 1; uint64 worktree_id = 2; - string language_name = 3; + string language_server_name = 3; string name = 4; int32 kind = 5; string path = 6; diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index cf4e2199676614de174a73647dbe67b5dfe67c97..56994db4251d995d347cb248279e52e3272d84d5 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -3,7 +3,7 @@ use client::http::{self, HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; pub use language::*; use smol::fs::{self, File}; -use std::{any::Any, path::PathBuf, str, sync::Arc}; +use std::{any::Any, path::PathBuf, sync::Arc}; use util::{ResultExt, TryFutureExt}; use super::GithubRelease; @@ -11,8 +11,8 @@ use super::GithubRelease; pub struct CLspAdapter; impl super::LspAdapter for CLspAdapter { - fn name(&self) -> &'static str { - "clangd" + fn name(&self) -> LanguageServerName { + LanguageServerName("clangd".into()) } fn fetch_latest_server_version( diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index c1d0db2ed6d220773094a78c43d2df18c8317c51..4069413f11129f826add608995bf5c792db709d1 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Context, Result}; use client::http::HttpClient; use futures::{future::BoxFuture, FutureExt, StreamExt}; -use language::LspAdapter; +use language::{LanguageServerName, LspAdapter}; use serde::Deserialize; use serde_json::json; use smol::fs; @@ -16,8 +16,8 @@ impl JsonLspAdapter { } impl LspAdapter for JsonLspAdapter { - fn name(&self) -> &'static str { - "vscode-json-languageserver" + fn name(&self) -> LanguageServerName { + LanguageServerName("vscode-json-languageserver".into()) } fn server_args(&self) -> &[&str] { diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 8b06c5cbd7c784752cd91fecded506a40f6f5423..4034e5effa32b216cfcd22dc3f91c5aa1ce3cb69 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -14,8 +14,8 @@ use super::GithubRelease; pub struct RustLspAdapter; impl LspAdapter for RustLspAdapter { - fn name(&self) -> &'static str { - "rust-analyzer" + fn name(&self) -> LanguageServerName { + LanguageServerName("rust-analyzer".into()) } fn fetch_latest_server_version( diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index f7547fdd4e651a03bc53028c591a6aa16b60878a..4d7af34c38cf2c35ed030136c3f75395efeb9d2a 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Context, Result}; use client::http::HttpClient; use futures::{future::BoxFuture, FutureExt, StreamExt}; -use language::LspAdapter; +use language::{LanguageServerName, LspAdapter}; use serde::Deserialize; use serde_json::json; use smol::fs; @@ -20,8 +20,8 @@ struct Versions { } impl LspAdapter for TypeScriptLspAdapter { - fn name(&self) -> &'static str { - "typescript-language-server" + fn name(&self) -> LanguageServerName { + LanguageServerName("typescript-language-server".into()) } fn server_args(&self) -> &[&str] { From ebc711f9f500bd5c10d774659e6d3d0b6f078c27 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 29 Mar 2022 17:54:29 -0700 Subject: [PATCH 07/24] Restructure fake language server setup Replace FakeLanguageServerConfig with FakeLanguageServerAdapter --- crates/editor/src/editor.rs | 51 ++- crates/language/src/language.rs | 101 +++-- crates/project/src/project.rs | 124 +++--- crates/server/src/rpc.rs | 715 ++++++++++++++++---------------- 4 files changed, 496 insertions(+), 495 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dac7adfdf823f2d18e16d0b6d1a27a4f7c321173..b9428fc1f5acbe5d1bf1a852cb2796320722e515 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6438,13 +6438,12 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { - use super::*; use gpui::{ geometry::rect::RectF, platform::{WindowBounds, WindowOptions}, }; - use language::{LanguageConfig, LanguageServerConfig}; + use language::{FakeLspAdapter, LanguageConfig}; use lsp::FakeLanguageServer; use project::FakeFs; use smol::stream::StreamExt; @@ -8880,26 +8879,27 @@ mod tests { cx.foreground().forbid_parking(); cx.update(populate_settings); - let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - language_server_config.set_fake_capabilities(lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }); let fs = FakeFs::new(cx.background().clone()); fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages().add(language)); + project.update(cx, |project, _| project.languages().add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -8913,6 +8913,8 @@ mod tests { .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) .await .unwrap(); + + cx.foreground().start_waiting(); let mut fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -8934,6 +8936,7 @@ mod tests { }) .next() .await; + cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), @@ -8955,6 +8958,7 @@ mod tests { }); let save = cx.update(|cx| editor.save(project.clone(), cx)); cx.foreground().advance_clock(items::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), @@ -8967,23 +8971,24 @@ mod tests { async fn test_completion(cx: &mut gpui::TestAppContext) { cx.update(populate_settings); - let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - language_server_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - ..Default::default() - }), - ..Default::default() - }); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }); let text = " one @@ -8996,7 +9001,7 @@ mod tests { fs.insert_file("/file.rs", text).await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages().add(language)); + project.update(cx, |project, _| project.languages().add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8d573574fda99bf1f2c862794bba6049107ba0b4..a98f1a9845c28f4fed6198a270a1f4a20e74b7c1 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -138,16 +138,13 @@ impl Default for LanguageConfig { pub struct LanguageServerConfig { pub disk_based_diagnostic_sources: HashSet, pub disk_based_diagnostics_progress_token: Option, - #[cfg(any(test, feature = "test-support"))] - #[serde(skip)] - fake_config: Option, } #[cfg(any(test, feature = "test-support"))] -struct FakeLanguageServerConfig { - servers_tx: mpsc::UnboundedSender, - capabilities: lsp::ServerCapabilities, - initializer: Option>, +pub struct FakeLspAdapter { + pub name: &'static str, + pub capabilities: lsp::ServerCapabilities, + pub initializer: Option>, } #[derive(Clone, Debug, Deserialize)] @@ -162,6 +159,12 @@ pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, pub(crate) adapter: Option>, + + #[cfg(any(test, feature = "test-support"))] + fake_adapter: Option<( + mpsc::UnboundedSender, + Arc, + )>, } pub struct Grammar { @@ -262,26 +265,22 @@ impl LanguageRegistry { cx: &mut MutableAppContext, ) -> Option>> { #[cfg(any(test, feature = "test-support"))] - if language.config.language_server.fake_config.is_some() { + if language.fake_adapter.is_some() { let language = language.clone(); return Some(cx.spawn(|mut cx| async move { - let fake_config = language - .config - .language_server - .fake_config - .as_ref() - .unwrap(); + let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap(); let (server, mut fake_server) = cx.update(|cx| { lsp::LanguageServer::fake_with_capabilities( - fake_config.capabilities.clone(), + fake_adapter.capabilities.clone(), cx, ) }); - if let Some(initializer) = &fake_config.initializer { + + if let Some(initializer) = &fake_adapter.initializer { initializer(&mut fake_server); } - let servers_tx = fake_config.servers_tx.clone(); + let servers_tx = servers_tx.clone(); cx.background() .spawn(async move { fake_server @@ -426,6 +425,9 @@ impl Language { }) }), adapter: None, + + #[cfg(any(test, feature = "test-support"))] + fake_adapter: None, } } @@ -478,6 +480,18 @@ impl Language { self } + #[cfg(any(test, feature = "test-support"))] + pub fn set_fake_lsp_adapter( + &mut self, + fake_lsp_adapter: FakeLspAdapter, + ) -> mpsc::UnboundedReceiver { + let (servers_tx, servers_rx) = mpsc::unbounded(); + let adapter = Arc::new(fake_lsp_adapter); + self.fake_adapter = Some((servers_tx, adapter.clone())); + self.adapter = Some(adapter); + servers_rx + } + pub fn name(&self) -> Arc { self.config.name.clone() } @@ -601,33 +615,42 @@ impl CodeLabel { } #[cfg(any(test, feature = "test-support"))] -impl LanguageServerConfig { - pub fn fake() -> (Self, mpsc::UnboundedReceiver) { - let (servers_tx, servers_rx) = mpsc::unbounded(); - ( - Self { - fake_config: Some(FakeLanguageServerConfig { - servers_tx, - capabilities: lsp::LanguageServer::full_capabilities(), - initializer: None, - }), - disk_based_diagnostics_progress_token: Some("fakeServer/check".to_string()), - ..Default::default() - }, - servers_rx, - ) +impl Default for FakeLspAdapter { + fn default() -> Self { + Self { + name: "the-fake-language-server", + capabilities: lsp::LanguageServer::full_capabilities(), + initializer: None, + } } +} - pub fn set_fake_capabilities(&mut self, capabilities: lsp::ServerCapabilities) { - self.fake_config.as_mut().unwrap().capabilities = capabilities; +impl LspAdapter for FakeLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName(self.name.into()) } - pub fn set_fake_initializer( - &mut self, - initializer: impl 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer), - ) { - self.fake_config.as_mut().unwrap().initializer = Some(Box::new(initializer)); + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result>> { + unreachable!(); } + + fn fetch_server_binary( + &self, + _: Box, + _: Arc, + _: PathBuf, + ) -> BoxFuture<'static, Result> { + unreachable!(); + } + + fn cached_server_binary(&self, _: PathBuf) -> BoxFuture<'static, Option> { + unreachable!(); + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} } impl ToLspPosition for PointUtf16 { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6f550d10206e1f90475937e34ed5940a6b78531b..1dd0f907f702db274cf580e6f2be829c79fc2e1e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4606,8 +4606,8 @@ mod tests { use futures::StreamExt; use gpui::test::subscribe; use language::{ - tree_sitter_rust, Diagnostic, LanguageConfig, LanguageServerConfig, OffsetRangeExt, Point, - ToPoint, + tree_sitter_rust, Diagnostic, FakeLspAdapter, LanguageConfig, LanguageServerConfig, + OffsetRangeExt, Point, ToPoint, }; use lsp::Url; use serde_json::json; @@ -4683,41 +4683,44 @@ mod tests { async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (mut rust_lsp_config, mut fake_rust_servers) = LanguageServerConfig::fake(); - let (mut json_lsp_config, mut fake_json_servers) = LanguageServerConfig::fake(); - rust_lsp_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), "::".to_string()]), - ..Default::default() - }), - ..Default::default() - }); - json_lsp_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![":".to_string()]), - ..Default::default() - }), - ..Default::default() - }); - - let rust_language = Arc::new(Language::new( + let mut rust_language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: rust_lsp_config, ..Default::default() }, Some(tree_sitter_rust::language()), - )); - let json_language = Arc::new(Language::new( + ); + let mut json_language = Language::new( LanguageConfig { name: "JSON".into(), path_suffixes: vec!["json".to_string()], - language_server: json_lsp_config, ..Default::default() }, None, - )); + ); + let mut fake_rust_servers = rust_language.set_fake_lsp_adapter(FakeLspAdapter { + name: "the-rust-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), "::".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }); + let mut fake_json_servers = json_language.set_fake_lsp_adapter(FakeLspAdapter { + name: "the-json-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -4733,8 +4736,8 @@ mod tests { let project = Project::test(fs, cx); project.update(cx, |project, _| { - project.languages.add(rust_language); - project.languages.add(json_language); + project.languages.add(Arc::new(rust_language)); + project.languages.add(Arc::new(json_language)); }); let worktree_id = project @@ -4903,21 +4906,20 @@ mod tests { async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let progress_token = language_server_config - .disk_based_diagnostics_progress_token - .clone() - .unwrap(); - - let language = Arc::new(Language::new( + let progress_token = "the-progress-token".to_string(); + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, + language_server: LanguageServerConfig { + disk_based_diagnostics_progress_token: Some(progress_token.clone()), + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -4930,7 +4932,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages.add(language)); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let (tree, _) = project .update(cx, |project, cx| { @@ -5022,19 +5024,20 @@ mod tests { async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); - lsp_config - .disk_based_diagnostic_sources - .insert("disk".to_string()); - let language = Arc::new(Language::new( + // let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: lsp_config, + language_server: LanguageServerConfig { + disk_based_diagnostic_sources: ["disk".to_string()].into_iter().collect(), + ..Default::default() + }, ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let text = " fn a() { A } @@ -5047,7 +5050,7 @@ mod tests { fs.insert_tree("/dir", json!({ "a.rs": text })).await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages.add(language)); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -5396,16 +5399,15 @@ mod tests { async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (lsp_config, mut fake_servers) = LanguageServerConfig::fake(); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: lsp_config, ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let text = " fn a() { @@ -5430,7 +5432,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages.add(language)); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -5746,16 +5748,15 @@ mod tests { #[gpui::test] async fn test_definition(cx: &mut gpui::TestAppContext) { - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -5768,9 +5769,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); - }); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let (tree, _) = project .update(cx, |project, cx| { @@ -6682,16 +6681,15 @@ mod tests { async fn test_rename(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -6704,9 +6702,7 @@ mod tests { .await; let project = Project::test(fs.clone(), cx); - project.update(cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); - }); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let (tree, _) = project .update(cx, |project, cx| { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index b329839de2b80912ff48299a69407ef5d8e42c4f..0f8720c673b731e1f75c63650f46ea0ac7f1aa33 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1088,10 +1088,10 @@ mod tests { }; use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle}; use language::{ - tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, - LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition, + tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, + LanguageRegistry, OffsetRangeExt, Point, ToLspPosition, }; - use lsp; + use lsp::{self, FakeLanguageServer}; use parking_lot::Mutex; use postage::barrier; use project::{ @@ -2039,22 +2039,20 @@ mod tests { cx_b: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2261,29 +2259,29 @@ mod tests { cx_b: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. - let (mut language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - language_server_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], ..Default::default() - }), + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + ..Default::default() + }, ..Default::default() }); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2463,22 +2461,20 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2563,7 +2559,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root-1", @@ -2582,18 +2578,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2705,7 +2699,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root-1", @@ -2725,18 +2719,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2967,16 +2959,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - lang_registry.add(Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3092,7 +3084,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/code", @@ -3112,18 +3104,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3231,7 +3221,7 @@ mod tests { mut rng: StdRng, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root", @@ -3244,19 +3234,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3337,23 +3324,21 @@ mod tests { cx_b: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3573,23 +3558,21 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -4838,7 +4821,7 @@ mod tests { let rng = Arc::new(Mutex::new(rng)); let guest_lang_registry = Arc::new(LanguageRegistry::test()); - let (language_server_config, _fake_language_servers) = LanguageServerConfig::fake(); + let host_language_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -4852,6 +4835,7 @@ mod tests { let operations = Rc::new(Cell::new(0)); let mut server = TestServer::start(cx.foreground(), cx.background()).await; let mut clients = Vec::new(); + let files = Arc::new(Mutex::new(Vec::new())); let mut next_entity_id = 100000; let mut host_cx = TestAppContext::new( @@ -4868,7 +4852,7 @@ mod tests { Project::local( host.client.clone(), host.user_store.clone(), - Arc::new(LanguageRegistry::test()), + host_language_registry.clone(), fs.clone(), cx, ) @@ -4891,9 +4875,138 @@ mod tests { .await .unwrap(); + // Set up fake language servers. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let _fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + name: "the-fake-language-server", + capabilities: lsp::LanguageServer::full_capabilities(), + initializer: Some(Box::new({ + let rng = rng.clone(); + let files = files.clone(); + let project = host_project.downgrade(); + move |fake_server: &mut FakeLanguageServer| { + fake_server.handle_request::( + |_, _| async move { + Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "the-new-text".to_string(), + })), + ..Default::default() + }])) + }, + ); + + fake_server.handle_request::( + |_, _| async move { + Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "the-code-action".to_string(), + ..Default::default() + }, + )]) + }, + ); + + fake_server.handle_request::( + |params, _| async move { + Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + params.position, + params.position, + ))) + }, + ); + + fake_server.handle_request::({ + let files = files.clone(); + let rng = rng.clone(); + move |_, _| { + let files = files.clone(); + let rng = rng.clone(); + async move { + let files = files.lock(); + let mut rng = rng.lock(); + let count = rng.gen_range::(1..3); + let files = (0..count) + .map(|_| files.choose(&mut *rng).unwrap()) + .collect::>(); + log::info!("LSP: Returning definitions in files {:?}", &files); + Some(lsp::GotoDefinitionResponse::Array( + files + .into_iter() + .map(|file| lsp::Location { + uri: lsp::Url::from_file_path(file).unwrap(), + range: Default::default(), + }) + .collect(), + )) + } + } + }); + + fake_server.handle_request::({ + let rng = rng.clone(); + let project = project.clone(); + move |params, mut cx| { + let highlights = if let Some(project) = project.upgrade(&cx) { + project.update(&mut cx, |project, cx| { + let path = params + .text_document_position_params + .text_document + .uri + .to_file_path() + .unwrap(); + let (worktree, relative_path) = + project.find_local_worktree(&path, cx)?; + let project_path = + ProjectPath::from((worktree.read(cx).id(), relative_path)); + let buffer = + project.get_open_buffer(&project_path, cx)?.read(cx); + + let mut highlights = Vec::new(); + let highlight_count = rng.lock().gen_range(1..=5); + let mut prev_end = 0; + for _ in 0..highlight_count { + let range = + buffer.random_byte_range(prev_end, &mut *rng.lock()); + let start = buffer + .offset_to_point_utf16(range.start) + .to_lsp_position(); + let end = buffer + .offset_to_point_utf16(range.end) + .to_lsp_position(); + highlights.push(lsp::DocumentHighlight { + range: lsp::Range::new(start, end), + kind: Some(lsp::DocumentHighlightKind::READ), + }); + prev_end = range.end; + } + Some(highlights) + }) + } else { + None + }; + async move { highlights } + } + }); + } + })), + }); + host_language_registry.add(Arc::new(language)); + clients.push(cx.foreground().spawn(host.simulate_host( host_project, - language_server_config, + files, operations.clone(), max_operations, rng.clone(), @@ -5324,264 +5437,128 @@ mod tests { }) } - fn simulate_host( + async fn simulate_host( mut self, project: ModelHandle, - mut language_server_config: LanguageServerConfig, + files: Arc>>, operations: Rc>, max_operations: usize, rng: Arc>, mut cx: TestAppContext, - ) -> impl Future { - let files: Arc>> = Default::default(); - - // Set up a fake language server. - language_server_config.set_fake_initializer({ - let rng = rng.clone(); - let files = files.clone(); - let project = project.downgrade(); - move |fake_server| { - fake_server.handle_request::( - |_, _| async move { - Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem { - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 0), - ), - new_text: "the-new-text".to_string(), - })), - ..Default::default() - }])) - }, - ); - - fake_server.handle_request::( - |_, _| async move { - Some(vec![lsp::CodeActionOrCommand::CodeAction( - lsp::CodeAction { - title: "the-code-action".to_string(), - ..Default::default() - }, - )]) - }, - ); - - fake_server.handle_request::( - |params, _| async move { - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( - params.position, - params.position, - ))) - }, - ); + ) -> (Self, TestAppContext) { + let fs = project.read_with(&cx, |project, _| project.fs().clone()); + while operations.get() < max_operations { + operations.set(operations.get() + 1); - fake_server.handle_request::({ - let files = files.clone(); - let rng = rng.clone(); - move |_, _| { - let files = files.clone(); - let rng = rng.clone(); - async move { - let files = files.lock(); - let mut rng = rng.lock(); - let count = rng.gen_range::(1..3); - let files = (0..count) - .map(|_| files.choose(&mut *rng).unwrap()) - .collect::>(); - log::info!("LSP: Returning definitions in files {:?}", &files); - Some(lsp::GotoDefinitionResponse::Array( - files - .into_iter() - .map(|file| lsp::Location { - uri: lsp::Url::from_file_path(file).unwrap(), - range: Default::default(), - }) - .collect(), - )) + let distribution = rng.lock().gen_range::(0..100); + match distribution { + 0..=20 if !files.lock().is_empty() => { + let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let mut path = path.as_path(); + while let Some(parent_path) = path.parent() { + path = parent_path; + if rng.lock().gen() { + break; } } - }); - fake_server.handle_request::({ - let rng = rng.clone(); - let project = project.clone(); - move |params, mut cx| { - let highlights = if let Some(project) = project.upgrade(&cx) { - project.update(&mut cx, |project, cx| { - let path = params - .text_document_position_params - .text_document - .uri - .to_file_path() - .unwrap(); - let (worktree, relative_path) = - project.find_local_worktree(&path, cx)?; - let project_path = - ProjectPath::from((worktree.read(cx).id(), relative_path)); - let buffer = - project.get_open_buffer(&project_path, cx)?.read(cx); - - let mut highlights = Vec::new(); - let highlight_count = rng.lock().gen_range(1..=5); - let mut prev_end = 0; - for _ in 0..highlight_count { - let range = - buffer.random_byte_range(prev_end, &mut *rng.lock()); - let start = buffer - .offset_to_point_utf16(range.start) - .to_lsp_position(); - let end = buffer - .offset_to_point_utf16(range.end) - .to_lsp_position(); - highlights.push(lsp::DocumentHighlight { - range: lsp::Range::new(start, end), - kind: Some(lsp::DocumentHighlightKind::READ), - }); - prev_end = range.end; - } - Some(highlights) - }) - } else { - None - }; - async move { highlights } + log::info!("Host: find/create local worktree {:?}", path); + let find_or_create_worktree = project.update(&mut cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }); + let find_or_create_worktree = async move { + find_or_create_worktree.await.unwrap(); + }; + if rng.lock().gen() { + cx.background().spawn(find_or_create_worktree).detach(); + } else { + find_or_create_worktree.await; } - }); - } - }); - - project.update(&mut cx, |project, _| { - project.languages().add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: language_server_config, - ..Default::default() - }, - None, - ))); - }); - - async move { - let fs = project.read_with(&cx, |project, _| project.fs().clone()); - while operations.get() < max_operations { - operations.set(operations.get() + 1); - - let distribution = rng.lock().gen_range::(0..100); - match distribution { - 0..=20 if !files.lock().is_empty() => { - let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); - let mut path = path.as_path(); - while let Some(parent_path) = path.parent() { - path = parent_path; - if rng.lock().gen() { - break; - } - } + } + 10..=80 if !files.lock().is_empty() => { + let buffer = if self.buffers.is_empty() || rng.lock().gen() { + let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let (worktree, path) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree(file.clone(), true, cx) + }) + .await + .unwrap(); + let project_path = + worktree.read_with(&cx, |worktree, _| (worktree.id(), path)); + log::info!( + "Host: opening path {:?}, worktree {}, relative_path {:?}", + file, + project_path.0, + project_path.1 + ); + let buffer = project + .update(&mut cx, |project, cx| { + project.open_buffer(project_path, cx) + }) + .await + .unwrap(); + self.buffers.insert(buffer.clone()); + buffer + } else { + self.buffers + .iter() + .choose(&mut *rng.lock()) + .unwrap() + .clone() + }; - log::info!("Host: find/create local worktree {:?}", path); - let find_or_create_worktree = project.update(&mut cx, |project, cx| { - project.find_or_create_local_worktree(path, true, cx) + if rng.lock().gen_bool(0.1) { + cx.update(|cx| { + log::info!( + "Host: dropping buffer {:?}", + buffer.read(cx).file().unwrap().full_path(cx) + ); + self.buffers.remove(&buffer); + drop(buffer); }); - let find_or_create_worktree = async move { - find_or_create_worktree.await.unwrap(); - }; - if rng.lock().gen() { - cx.background().spawn(find_or_create_worktree).detach(); - } else { - find_or_create_worktree.await; - } - } - 10..=80 if !files.lock().is_empty() => { - let buffer = if self.buffers.is_empty() || rng.lock().gen() { - let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); - let (worktree, path) = project - .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree( - file.clone(), - true, - cx, - ) - }) - .await - .unwrap(); - let project_path = - worktree.read_with(&cx, |worktree, _| (worktree.id(), path)); + } else { + buffer.update(&mut cx, |buffer, cx| { log::info!( - "Host: opening path {:?}, worktree {}, relative_path {:?}", - file, - project_path.0, - project_path.1 + "Host: updating buffer {:?} ({})", + buffer.file().unwrap().full_path(cx), + buffer.remote_id() ); - let buffer = project - .update(&mut cx, |project, cx| { - project.open_buffer(project_path, cx) - }) - .await - .unwrap(); - self.buffers.insert(buffer.clone()); - buffer - } else { - self.buffers - .iter() - .choose(&mut *rng.lock()) - .unwrap() - .clone() - }; - - if rng.lock().gen_bool(0.1) { - cx.update(|cx| { - log::info!( - "Host: dropping buffer {:?}", - buffer.read(cx).file().unwrap().full_path(cx) - ); - self.buffers.remove(&buffer); - drop(buffer); - }); - } else { - buffer.update(&mut cx, |buffer, cx| { - log::info!( - "Host: updating buffer {:?} ({})", - buffer.file().unwrap().full_path(cx), - buffer.remote_id() - ); - buffer.randomly_edit(&mut *rng.lock(), 5, cx) - }); - } + buffer.randomly_edit(&mut *rng.lock(), 5, cx) + }); } - _ => loop { - let path_component_count = rng.lock().gen_range::(1..=5); - let mut path = PathBuf::new(); - path.push("/"); - for _ in 0..path_component_count { - let letter = rng.lock().gen_range(b'a'..=b'z'); - path.push(std::str::from_utf8(&[letter]).unwrap()); - } - path.set_extension("rs"); - let parent_path = path.parent().unwrap(); - - log::info!("Host: creating file {:?}", path,); - - if fs.create_dir(&parent_path).await.is_ok() - && fs.create_file(&path, Default::default()).await.is_ok() - { - files.lock().push(path); - break; - } else { - log::info!("Host: cannot create file"); - } - }, } + _ => loop { + let path_component_count = rng.lock().gen_range::(1..=5); + let mut path = PathBuf::new(); + path.push("/"); + for _ in 0..path_component_count { + let letter = rng.lock().gen_range(b'a'..=b'z'); + path.push(std::str::from_utf8(&[letter]).unwrap()); + } + path.set_extension("rs"); + let parent_path = path.parent().unwrap(); - cx.background().simulate_random_delay().await; - } + log::info!("Host: creating file {:?}", path,); - log::info!("Host done"); + if fs.create_dir(&parent_path).await.is_ok() + && fs.create_file(&path, Default::default()).await.is_ok() + { + files.lock().push(path); + break; + } else { + log::info!("Host: cannot create file"); + } + }, + } - self.project = Some(project); - (self, cx) + cx.background().simulate_random_delay().await; } + + log::info!("Host done"); + + self.project = Some(project); + (self, cx) } pub async fn simulate_guest( From 3b4cab9094152c3bdf101148910aef2c07fae6ab Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 29 Mar 2022 18:11:14 -0700 Subject: [PATCH 08/24] Move all configuration of individual LSP servers to LspAdapter --- crates/language/src/buffer.rs | 3 +- crates/language/src/language.rs | 46 ++++++++++++------- crates/project/src/project.rs | 55 +++++++++++------------ crates/server/src/rpc.rs | 1 + crates/zed/src/languages/rust.rs | 8 ++++ crates/zed/src/languages/rust/config.toml | 4 -- 6 files changed, 65 insertions(+), 52 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 535798083eb6a5de52738e2b85a4829021a3adf9..6cd89aa8bdf0e14606b691127677a8d5be7b6f2a 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,8 +1,7 @@ pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, - proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig, - PLAIN_TEXT, + proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT, }; use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index a98f1a9845c28f4fed6198a270a1f4a20e74b7c1..9c4aee6a301859d5c65fd7c2794090dad860573c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -8,7 +8,7 @@ mod tests; use anyhow::{anyhow, Context, Result}; use client::http::{self, HttpClient}; -use collections::{HashMap, HashSet}; +use collections::HashMap; use futures::{ future::{BoxFuture, Shared}, FutureExt, TryFutureExt, @@ -52,7 +52,6 @@ lazy_static! { brackets: Default::default(), autoclose_before: Default::default(), line_comment: None, - language_server: Default::default(), }, None, )); @@ -100,6 +99,14 @@ pub trait LspAdapter: 'static + Send + Sync { fn initialization_options(&self) -> Option { None } + + fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + Default::default() + } + + fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + None + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -117,8 +124,6 @@ pub struct LanguageConfig { #[serde(default)] pub autoclose_before: String, pub line_comment: Option, - #[serde(default)] - pub language_server: LanguageServerConfig, } impl Default for LanguageConfig { @@ -129,22 +134,17 @@ impl Default for LanguageConfig { brackets: Default::default(), autoclose_before: Default::default(), line_comment: Default::default(), - language_server: Default::default(), } } } -#[derive(Default, Deserialize)] -pub struct LanguageServerConfig { - pub disk_based_diagnostic_sources: HashSet, - pub disk_based_diagnostics_progress_token: Option, -} - #[cfg(any(test, feature = "test-support"))] pub struct FakeLspAdapter { pub name: &'static str, pub capabilities: lsp::ServerCapabilities, pub initializer: Option>, + pub disk_based_diagnostics_progress_token: Option<&'static str>, + pub disk_based_diagnostics_sources: &'static [&'static str], } #[derive(Clone, Debug, Deserialize)] @@ -500,15 +500,16 @@ impl Language { self.config.line_comment.as_deref() } - pub fn disk_based_diagnostic_sources(&self) -> &HashSet { - &self.config.language_server.disk_based_diagnostic_sources + pub fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + self.adapter.as_ref().map_or(&[] as &[_], |adapter| { + adapter.disk_based_diagnostic_sources() + }) } - pub fn disk_based_diagnostics_progress_token(&self) -> Option<&String> { - self.config - .language_server - .disk_based_diagnostics_progress_token + pub fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + self.adapter .as_ref() + .and_then(|adapter| adapter.disk_based_diagnostics_progress_token()) } pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { @@ -621,10 +622,13 @@ impl Default for FakeLspAdapter { name: "the-fake-language-server", capabilities: lsp::LanguageServer::full_capabilities(), initializer: None, + disk_based_diagnostics_progress_token: None, + disk_based_diagnostics_sources: &[], } } } +#[cfg(any(test, feature = "test-support"))] impl LspAdapter for FakeLspAdapter { fn name(&self) -> LanguageServerName { LanguageServerName(self.name.into()) @@ -651,6 +655,14 @@ impl LspAdapter for FakeLspAdapter { } fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + + fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + self.disk_based_diagnostics_sources + } + + fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + self.disk_based_diagnostics_progress_token + } } impl ToLspPosition for PointUtf16 { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1dd0f907f702db274cf580e6f2be829c79fc2e1e..b27edcc8d437750dd71386dc832ef79a9907466a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1536,7 +1536,7 @@ impl Project { match event { LanguageServerEvent::WorkStart { token } => { - if Some(&token) == disk_diagnostics_token { + if Some(token.as_str()) == disk_diagnostics_token { language_server_status.pending_diagnostic_updates += 1; if language_server_status.pending_diagnostic_updates == 1 { self.disk_based_diagnostics_started(cx); @@ -1558,7 +1558,7 @@ impl Project { } } LanguageServerEvent::WorkProgress { token, progress } => { - if Some(&token) != disk_diagnostics_token { + if Some(token.as_str()) != disk_diagnostics_token { self.on_lsp_work_progress( language_server_id, token.clone(), @@ -1578,7 +1578,7 @@ impl Project { } } LanguageServerEvent::WorkEnd { token } => { - if Some(&token) == disk_diagnostics_token { + if Some(token.as_str()) == disk_diagnostics_token { language_server_status.pending_diagnostic_updates -= 1; if language_server_status.pending_diagnostic_updates == 0 { self.disk_based_diagnostics_finished(cx); @@ -1708,7 +1708,7 @@ impl Project { pub fn update_diagnostics( &mut self, params: lsp::PublishDiagnosticsParams, - disk_based_sources: &HashSet, + disk_based_sources: &[&str], cx: &mut ModelContext, ) -> Result<()> { let abs_path = params @@ -1751,8 +1751,9 @@ impl Project { ); } else { let group_id = post_inc(&mut next_group_id); - let is_disk_based = - source.map_or(false, |source| disk_based_sources.contains(source)); + let is_disk_based = source.map_or(false, |source| { + disk_based_sources.contains(&source.as_str()) + }); sources_by_group_id.insert(group_id, source); primary_diagnostic_group_ids @@ -4606,8 +4607,8 @@ mod tests { use futures::StreamExt; use gpui::test::subscribe; use language::{ - tree_sitter_rust, Diagnostic, FakeLspAdapter, LanguageConfig, LanguageServerConfig, - OffsetRangeExt, Point, ToPoint, + tree_sitter_rust, Diagnostic, FakeLspAdapter, LanguageConfig, OffsetRangeExt, Point, + ToPoint, }; use lsp::Url; use serde_json::json; @@ -4906,20 +4907,20 @@ mod tests { async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let progress_token = "the-progress-token".to_string(); + let progress_token = "the-progress-token"; let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: LanguageServerConfig { - disk_based_diagnostics_progress_token: Some(progress_token.clone()), - ..Default::default() - }, ..Default::default() }, Some(tree_sitter_rust::language()), ); - let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + disk_based_diagnostics_progress_token: Some(progress_token), + disk_based_diagnostics_sources: &["disk"], + ..Default::default() + }); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -4956,15 +4957,15 @@ mod tests { let mut events = subscribe(&project, cx); let mut fake_server = fake_servers.next().await.unwrap(); - fake_server.start_progress(&progress_token).await; + fake_server.start_progress(progress_token).await; assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsStarted ); - fake_server.start_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; - fake_server.start_progress(&progress_token).await; + fake_server.start_progress(progress_token).await; + fake_server.end_progress(progress_token).await; + fake_server.start_progress(progress_token).await; fake_server.notify::( lsp::PublishDiagnosticsParams { @@ -4983,8 +4984,8 @@ mod tests { Event::DiagnosticsUpdated((worktree_id, Path::new("a.rs")).into()) ); - fake_server.end_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; + fake_server.end_progress(progress_token).await; + fake_server.end_progress(progress_token).await; assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsUpdated @@ -5024,20 +5025,18 @@ mod tests { async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - // let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: LanguageServerConfig { - disk_based_diagnostic_sources: ["disk".to_string()].into_iter().collect(), - ..Default::default() - }, ..Default::default() }, Some(tree_sitter_rust::language()), ); - let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + disk_based_diagnostics_sources: &["disk"], + ..Default::default() + }); let text = " fn a() { A } @@ -6551,9 +6550,7 @@ mod tests { }; project - .update(cx, |p, cx| { - p.update_diagnostics(message, &Default::default(), cx) - }) + .update(cx, |p, cx| p.update_diagnostics(message, &[], cx)) .unwrap(); let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot()); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 0f8720c673b731e1f75c63650f46ea0ac7f1aa33..1b22d09b567214ec0311e6ae23e75ca0cff86d92 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -5001,6 +5001,7 @@ mod tests { }); } })), + ..Default::default() }); host_language_registry.add(Arc::new(language)); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 4034e5effa32b216cfcd22dc3f91c5aa1ce3cb69..4f738180420135ff618ba1144efb9769eeae28c6 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -112,6 +112,14 @@ impl LspAdapter for RustLspAdapter { .boxed() } + fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + &["rustc"] + } + + fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + Some("rustAnalyzer/cargo check") + } + fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { lazy_static! { static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap(); diff --git a/crates/zed/src/languages/rust/config.toml b/crates/zed/src/languages/rust/config.toml index 97fe231e0fa2bd2cb457c295659b9da10b8c656c..e4d222cddedaa5a004b71a74ed7d3d4dd9608bb2 100644 --- a/crates/zed/src/languages/rust/config.toml +++ b/crates/zed/src/languages/rust/config.toml @@ -10,7 +10,3 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false }, { start = "/*", end = " */", close = true, newline = false }, ] - -[language_server] -disk_based_diagnostic_sources = ["rustc"] -disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check" From 45ad5f343f0e501283669493e618c3d732ba88ef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 29 Mar 2022 18:14:42 -0700 Subject: [PATCH 09/24] Parse JS as TSX --- crates/zed/src/languages/tsx/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml index 62717266df9e0bd56d83b336cfbf28aa2b01ee4b..a6f4a6d2d000d5bcf83219a27b52446178c7e079 100644 --- a/crates/zed/src/languages/tsx/config.toml +++ b/crates/zed/src/languages/tsx/config.toml @@ -1,5 +1,5 @@ name = "TSX" -path_suffixes = ["tsx"] +path_suffixes = ["tsx", "js"] line_comment = "// " autoclose_before = ";:.,=}])>" brackets = [ From cf9efd70057c129495dc3184b1056ba6aff3fdcc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Mar 2022 16:48:57 -0700 Subject: [PATCH 10/24] Improve installation of npm-based language servers * Use --prefix flag to guarantee that they are installed in .zed * Use the @latest tag when available * Extract helper functions Co-authored-by: Keith Simmons --- crates/language/src/language.rs | 7 +- crates/zed/src/languages.rs | 15 +-- crates/zed/src/languages/c.rs | 37 ++------ crates/zed/src/languages/installation.rs | 111 +++++++++++++++++++++++ crates/zed/src/languages/rust.rs | 37 ++------ crates/zed/src/languages/typescript.rs | 59 +++--------- 6 files changed, 141 insertions(+), 125 deletions(-) create mode 100644 crates/zed/src/languages/installation.rs diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fb736736b6ecd338c7de24133cee682c7c65731b..66cafb422b83c8fd969c0b92a90e1dbd7470804b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -7,7 +7,7 @@ pub mod proto; mod tests; use anyhow::{anyhow, Context, Result}; -use client::http::{self, HttpClient}; +use client::http::HttpClient; use collections::HashMap; use futures::{ future::{BoxFuture, Shared}, @@ -61,11 +61,6 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } -pub struct GitHubLspBinaryVersion { - pub name: String, - pub url: http::Url, -} - #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct LanguageServerName(pub Arc); diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index cc22247025a393cb104ee4317bc76a47155988d3..75a5030ec6a9991ef87d7ebc764c5aabaa6d27d6 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,11 +1,10 @@ -use client::http; use gpui::Task; pub use language::*; use rust_embed::RustEmbed; -use serde::Deserialize; use std::{borrow::Cow, str, sync::Arc}; mod c; +mod installation; mod json; mod rust; mod typescript; @@ -15,18 +14,6 @@ mod typescript; #[exclude = "*.rs"] struct LanguageDir; -#[derive(Deserialize)] -struct GithubRelease { - name: String, - assets: Vec, -} - -#[derive(Deserialize)] -struct GithubReleaseAsset { - name: String, - browser_download_url: http::Url, -} - pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { let languages = LanguageRegistry::new(login_shell_env_loaded); for (name, grammar, lsp_adapter) in [ diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 56994db4251d995d347cb248279e52e3272d84d5..f2ce41a2378cd27c20e8134900ab862eed0fa0b5 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -1,13 +1,12 @@ +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Result}; -use client::http::{self, HttpClient, Method}; +use client::http::{HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; pub use language::*; use smol::fs::{self, File}; use std::{any::Any, path::PathBuf, sync::Arc}; use util::{ResultExt, TryFutureExt}; -use super::GithubRelease; - pub struct CLspAdapter; impl super::LspAdapter for CLspAdapter { @@ -20,33 +19,11 @@ impl super::LspAdapter for CLspAdapter { http: Arc, ) -> BoxFuture<'static, Result>> { async move { - let release = http - .send( - surf::RequestBuilder::new( - Method::Get, - http::Url::parse( - "https://api.github.com/repos/clangd/clangd/releases/latest", - ) - .unwrap(), - ) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error fetching latest release: {}", err))? - .body_json::() - .await - .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let asset_name = format!("clangd-mac-{}.zip", release.name); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(Box::new(GitHubLspBinaryVersion { - name: release.name, - url: asset.browser_download_url.clone(), - }) as Box<_>) + let version = latest_github_release("clangd/clangd", http, |release_name| { + format!("clangd-mac-{release_name}.zip") + }) + .await?; + Ok(Box::new(version) as Box<_>) } .boxed() } diff --git a/crates/zed/src/languages/installation.rs b/crates/zed/src/languages/installation.rs new file mode 100644 index 0000000000000000000000000000000000000000..212ff472fcc224c6586ebc7e59cf450e49230f8c --- /dev/null +++ b/crates/zed/src/languages/installation.rs @@ -0,0 +1,111 @@ +use anyhow::{anyhow, Context, Result}; +use client::http::{self, HttpClient, Method}; +use serde::Deserialize; +use std::{path::Path, sync::Arc}; + +pub struct GitHubLspBinaryVersion { + pub name: String, + pub url: http::Url, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +struct NpmInfo { + #[serde(default)] + dist_tags: NpmInfoDistTags, + versions: Vec, +} + +#[derive(Deserialize, Default)] +struct NpmInfoDistTags { + latest: Option, +} + +#[derive(Deserialize)] +pub(crate) struct GithubRelease { + name: String, + assets: Vec, +} + +#[derive(Deserialize)] +pub(crate) struct GithubReleaseAsset { + name: String, + browser_download_url: http::Url, +} + +pub async fn npm_package_latest_version(name: &str) -> Result { + let output = smol::process::Command::new("npm") + .args(["info", name, "--json"]) + .output() + .await?; + if !output.status.success() { + Err(anyhow!( + "failed to execute npm info: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + info.dist_tags + .latest + .or_else(|| info.versions.pop()) + .ok_or_else(|| anyhow!("no version found for npm package {}", name)) +} + +pub async fn npm_install_packages( + packages: impl IntoIterator, + directory: &Path, +) -> Result<()> { + let output = smol::process::Command::new("npm") + .arg("install") + .arg("--prefix") + .arg(directory) + .args( + packages + .into_iter() + .map(|(name, version)| format!("{name}@{version}")), + ) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!( + "failed to execute npm install: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + Ok(()) +} + +pub async fn latest_github_release( + repo_name_with_owner: &str, + http: Arc, + asset_name: impl Fn(&str) -> String, +) -> Result { + let release = http + .send( + surf::RequestBuilder::new( + Method::Get, + http::Url::parse(&format!( + "https://api.github.com/repos/{repo_name_with_owner}/releases/latest" + )) + .unwrap(), + ) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) + .await + .map_err(|err| anyhow!("error fetching latest release: {}", err))? + .body_json::() + .await + .map_err(|err| anyhow!("error parsing latest release: {}", err))?; + let asset_name = asset_name(&release.name); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + Ok(GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }) +} diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 4f738180420135ff618ba1144efb9769eeae28c6..f419f59abb4bdc89e2202e84879988ade69ba045 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -1,6 +1,7 @@ +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; -use client::http::{self, HttpClient, Method}; +use client::http::{HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; pub use language::*; use lazy_static::lazy_static; @@ -9,8 +10,6 @@ use smol::fs::{self, File}; use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; use util::{ResultExt, TryFutureExt}; -use super::GithubRelease; - pub struct RustLspAdapter; impl LspAdapter for RustLspAdapter { @@ -23,33 +22,11 @@ impl LspAdapter for RustLspAdapter { http: Arc, ) -> BoxFuture<'static, Result>> { async move { - let release = http - .send( - surf::RequestBuilder::new( - Method::Get, - http::Url::parse( - "https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest", - ) - .unwrap(), - ) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error fetching latest release: {}", err))? - .body_json::() - .await - .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(Box::new(GitHubLspBinaryVersion { - name: release.name, - url: asset.browser_download_url.clone(), - }) as Box<_>) + let version = latest_github_release("rust-analyzer/rust-analyzer", http, |_| { + format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH) + }) + .await?; + Ok(Box::new(version) as Box<_>) } .boxed() } diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 4d7af34c38cf2c35ed030136c3f75395efeb9d2a..d08da116a544478693ceeea3a397868b653613cd 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -1,8 +1,8 @@ +use super::installation::{npm_install_packages, npm_package_latest_version}; use anyhow::{anyhow, Context, Result}; use client::http::HttpClient; use futures::{future::BoxFuture, FutureExt, StreamExt}; use language::{LanguageServerName, LspAdapter}; -use serde::Deserialize; use serde_json::json; use smol::fs; use std::{any::Any, path::PathBuf, sync::Arc}; @@ -33,37 +33,9 @@ impl LspAdapter for TypeScriptLspAdapter { _: Arc, ) -> BoxFuture<'static, Result>> { async move { - #[derive(Deserialize)] - struct NpmInfo { - versions: Vec, - } - - let typescript_output = smol::process::Command::new("npm") - .args(["info", "typescript", "--json"]) - .output() - .await?; - if !typescript_output.status.success() { - Err(anyhow!("failed to execute npm info"))?; - } - let mut typescript_info: NpmInfo = serde_json::from_slice(&typescript_output.stdout)?; - - let server_output = smol::process::Command::new("npm") - .args(["info", "typescript-language-server", "--json"]) - .output() - .await?; - if !server_output.status.success() { - Err(anyhow!("failed to execute npm info"))?; - } - let mut server_info: NpmInfo = serde_json::from_slice(&server_output.stdout)?; - Ok(Box::new(Versions { - typescript_version: typescript_info - .versions - .pop() - .ok_or_else(|| anyhow!("no versions found in typescript npm info"))?, - server_version: server_info.versions.pop().ok_or_else(|| { - anyhow!("no versions found in typescript language server npm info") - })?, + typescript_version: npm_package_latest_version("typescript").await?, + server_version: npm_package_latest_version("typescript-language-server").await?, }) as Box<_>) } .boxed() @@ -87,20 +59,17 @@ impl LspAdapter for TypeScriptLspAdapter { let binary_path = version_dir.join(Self::BIN_PATH); if fs::metadata(&binary_path).await.is_err() { - let output = smol::process::Command::new("npm") - .current_dir(&version_dir) - .arg("install") - .arg(format!("typescript@{}", versions.typescript_version)) - .arg(format!( - "typescript-language-server@{}", - versions.server_version - )) - .output() - .await - .context("failed to run npm install")?; - if !output.status.success() { - Err(anyhow!("failed to install typescript-language-server"))?; - } + npm_install_packages( + [ + ("typescript", versions.typescript_version.as_str()), + ( + "typescript-language-server", + &versions.server_version.as_str(), + ), + ], + &version_dir, + ) + .await?; if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { while let Some(entry) = entries.next().await { From c280c85ce714575f1fd7ee980a75211a9ce3c993 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Mar 2022 17:08:40 -0700 Subject: [PATCH 11/24] Hard-code LSP formatting options for now This is needed for auto-formatting to work properly in TypeScript and JSON Co-Authored-By: Keith Simmons --- crates/lsp/src/lsp.rs | 2 ++ crates/project/src/project.rs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index de47381c4666b7980b1c484d06fdae770d032767..51a68b83c8997747ffe0880fdab7f9d597417008 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -181,6 +181,7 @@ impl LanguageServer { buffer.resize(message_len, 0); stdout.read_exact(&mut buffer).await?; + log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer)); if let Ok(AnyNotification { id, method, params }) = serde_json::from_slice(&buffer) @@ -229,6 +230,7 @@ impl LanguageServer { let _clear_response_handlers = ClearResponseHandlers(response_handlers); let mut content_len_buffer = Vec::new(); while let Ok(message) = outbound_rx.recv().await { + log::trace!("outgoing message:{}", String::from_utf8_lossy(&message)); content_len_buffer.clear(); write!(content_len_buffer, "{}", message.len()).unwrap(); stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5a415ca3eb320a63c838300457faf47f3c494179..e859e756f86d0c7abde09392d504abc17a828625 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2046,7 +2046,12 @@ impl Project { language_server .request::(lsp::DocumentFormattingParams { text_document, - options: Default::default(), + options: lsp::FormattingOptions { + tab_size: 4, + insert_spaces: true, + insert_final_newline: Some(true), + ..Default::default() + }, work_done_progress_params: Default::default(), }) .await? @@ -2064,7 +2069,12 @@ impl Project { lsp::DocumentRangeFormattingParams { text_document, range: lsp::Range::new(buffer_start, buffer_end), - options: Default::default(), + options: lsp::FormattingOptions { + tab_size: 4, + insert_spaces: true, + insert_final_newline: Some(true), + ..Default::default() + }, work_done_progress_params: Default::default(), }, ) From 263e3d817667211375686ae8853348017f0657ae Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Mar 2022 17:47:25 -0700 Subject: [PATCH 12/24] Start work on interpreting 'label/insertText' completions These completions don't supply a range that should be overwritten, so the client needs to infer it via substring matching. Co-authored-by: Keith Simmons --- crates/project/src/project.rs | 5 ++++- crates/text/src/tests.rs | 9 +++++++++ crates/text/src/text.rs | 7 +++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e859e756f86d0c7abde09392d504abc17a828625..77dfc741c159d5871f15da33f8bb2ed1f53514bf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2363,7 +2363,10 @@ impl Project { Some(lsp::CompletionTextEdit::Edit(edit)) => { (range_from_lsp(edit.range), edit.new_text.clone()) } - None => (position..position, lsp_completion.label.clone()), + None => ( + this.common_prefix_at_position(position, &lsp_completion.label), + lsp_completion.label.clone(), + ), Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { log::info!("unsupported insert/replace completion"); return None; diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 7961dccd569c8380c3bb32e57e9057481e4371fd..0faf8e19de91e8b6c9b2416d6874cda55d458f30 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -164,6 +164,15 @@ fn test_line_len() { assert_eq!(buffer.line_len(5), 0); } +#[test] +fn test_common_prefix_at_positionn() { + let buffer = Buffer::new(0, 0, History::new("a = (bcd)".into())); + assert_eq!( + buffer.common_prefix_at_position(Point::new(0, 8), "bcdef"), + Point::new(0, 5)..Point::new(0, 8) + ) +} + #[test] fn test_text_summary_for_range() { let buffer = Buffer::new(0, 0, History::new("ab\nefg\nhklm\nnopqrs\ntuvwxyz".into())); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index b811d08c046c58f8c5d6c020c27c45a430258474..0e742f8a8b74517058adfe2ba448a3be1a85ca37 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1508,6 +1508,13 @@ impl BufferSnapshot { .eq(needle.bytes()) } + pub fn common_prefix_at_position(&self, position: T, needle: &str) -> Range + where + T: TextDimension + ToOffset, + { + todo!() + } + pub fn text(&self) -> String { self.visible_text.to_string() } From 9385690b988cacfadf28e55ecbcc799170ca8131 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 31 Mar 2022 00:55:55 -0700 Subject: [PATCH 13/24] Add test for common_prefix_at and rewrite it to be more readable and pass the new test cases --- crates/project/src/project.rs | 2 +- crates/text/src/tests.rs | 10 +++++++--- crates/text/src/text.rs | 29 ++++++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 77dfc741c159d5871f15da33f8bb2ed1f53514bf..df84aec0c5f13219ace51c1e5d8d08b325e8db1c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2364,7 +2364,7 @@ impl Project { (range_from_lsp(edit.range), edit.new_text.clone()) } None => ( - this.common_prefix_at_position(position, &lsp_completion.label), + this.common_prefix_at(position, &lsp_completion.label), lsp_completion.label.clone(), ), Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 0faf8e19de91e8b6c9b2416d6874cda55d458f30..54e802b52194721484761e95507de9af516b0512 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -7,6 +7,7 @@ use std::{ iter::Iterator, time::{Duration, Instant}, }; +use util::test::marked_text_ranges; #[cfg(test)] #[ctor::ctor] @@ -166,10 +167,13 @@ fn test_line_len() { #[test] fn test_common_prefix_at_positionn() { - let buffer = Buffer::new(0, 0, History::new("a = (bcd)".into())); + let (text, ranges) = marked_text_ranges("a = [bcd]"); + let buffer = Buffer::new(0, 0, History::new(text.into())); + let snapshot = &buffer.snapshot(); + let expected_range = ranges[0].to_offset(&snapshot); assert_eq!( - buffer.common_prefix_at_position(Point::new(0, 8), "bcdef"), - Point::new(0, 5)..Point::new(0, 8) + buffer.common_prefix_at(expected_range.end, "bcdef"), + expected_range ) } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 0e742f8a8b74517058adfe2ba448a3be1a85ca37..ea0af9d21f5394409dbeda9d5d22f4964110ebf5 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1508,11 +1508,34 @@ impl BufferSnapshot { .eq(needle.bytes()) } - pub fn common_prefix_at_position(&self, position: T, needle: &str) -> Range + pub fn common_prefix_at(&self, position: T, needle: &str) -> Range where - T: TextDimension + ToOffset, + T: Clone + ToOffset + FromAnchor, { - todo!() + let position_offset = position.to_offset(self); + // Get byte indices and char counts for every character in needle in reverse order + let char_indices = needle + .char_indices() + .map(|(index, _)| index) + .chain(std::iter::once(needle.len())) + .enumerate() + // Don't test any prefixes that are bigger than the requested position + .take_while(|(_, prefix_length)| *prefix_length <= position_offset); + + let start = char_indices + // Compute the prefix string and prefix start location + .map(move |(byte_position, char_length)| { + (position_offset - char_length, &needle[..byte_position]) + }) + // Only take strings when the prefix is contained at the expected prefix position + .filter(|(prefix_offset, prefix)| self.contains_str_at(prefix_offset, prefix)) + // Convert offset to T + .map(|(prefix_offset, _)| T::from_anchor(&self.anchor_before(prefix_offset), self)) + .last() + // If no prefix matches, return the passed in position to create an empty range + .unwrap_or(position.clone()); + + start..position } pub fn text(&self) -> String { From 564225c4015340bdd87bfae551b690ea16904119 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 31 Mar 2022 15:39:52 -0700 Subject: [PATCH 14/24] Provide diagnostic context to codeAction Co-authored-by: Max Brunsfeld --- crates/language/src/diagnostic_set.rs | 17 +++++++++++ crates/language/src/language.rs | 17 ++++++----- crates/project/src/lsp_command.rs | 14 ++++----- crates/project/src/project.rs | 41 +++++++++++++-------------- crates/server/src/rpc.rs | 13 +++------ 5 files changed, 58 insertions(+), 44 deletions(-) diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 490789a8c80c3abe320d54bc12b88ec3529832cc..51c921e61c90081e516376ad25d729315d13c678 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -34,6 +34,23 @@ pub struct Summary { count: usize, } +impl DiagnosticEntry { + // Used to provide diagnostic context to lsp codeAction request + pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic { + let code = self + .diagnostic + .code + .clone() + .map(lsp::NumberOrString::String); + + lsp::Diagnostic { + code, + severity: Some(self.diagnostic.severity), + ..Default::default() + } + } +} + impl DiagnosticSet { pub fn from_sorted_entries(iter: I, buffer: &text::BufferSnapshot) -> Self where diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 66cafb422b83c8fd969c0b92a90e1dbd7470804b..243597320190782c2cbdaaf5ccf9ce4ba9c2407e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -662,18 +662,21 @@ impl LspAdapter for FakeLspAdapter { } } -impl ToLspPosition for PointUtf16 { - fn to_lsp_position(self) -> lsp::Position { - lsp::Position::new(self.row, self.column) - } +pub fn point_to_lsp(point: PointUtf16) -> lsp::Position { + lsp::Position::new(point.row, point.column) } pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 { PointUtf16::new(point.line, point.character) } +pub fn range_to_lsp(range: Range) -> lsp::Range { + lsp::Range { + start: point_to_lsp(range.start), + end: point_to_lsp(range.end), + } +} + pub fn range_from_lsp(range: lsp::Range) -> Range { - let start = PointUtf16::new(range.start.line, range.start.character); - let end = PointUtf16::new(range.end.line, range.end.character); - start..end + point_from_lsp(range.start)..point_from_lsp(range.end) } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index b6f007659dad840b3ee9145131fd522498f5524d..71ad489d07320af8a3e45ece6e7df6d4b163f551 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -4,9 +4,9 @@ use async_trait::async_trait; use client::{proto, PeerId}; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ - point_from_lsp, + point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16, + range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16, }; use lsp::{DocumentHighlightKind, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path}; @@ -91,7 +91,7 @@ impl LspCommand for PrepareRename { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), } } @@ -208,7 +208,7 @@ impl LspCommand for PerformRename { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, new_name: self.new_name.clone(), work_done_progress_params: Default::default(), @@ -325,7 +325,7 @@ impl LspCommand for GetDefinition { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), @@ -497,7 +497,7 @@ impl LspCommand for GetReferences { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), @@ -659,7 +659,7 @@ impl LspCommand for GetDocumentHighlights { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index df84aec0c5f13219ace51c1e5d8d08b325e8db1c..65c943c888247b5c02badc3e3b59c603f325a290 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -15,11 +15,12 @@ use gpui::{ MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, }; use language::{ + point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, - DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, - LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt, Operation, Patch, PointUtf16, - TextBufferSnapshot, ToLspPosition, ToOffset, ToPointUtf16, Transaction, + range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, + Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, + LanguageRegistry, LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt, Operation, Patch, + PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -1215,8 +1216,8 @@ impl Project { .collect(); lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range::new( - edit_start.to_lsp_position(), - edit_end.to_lsp_position(), + point_to_lsp(edit_start), + point_to_lsp(edit_end), )), range_length: None, text: new_text, @@ -2061,9 +2062,8 @@ impl Project { .map_or(false, |provider| *provider != lsp::OneOf::Left(false)) { let buffer_start = lsp::Position::new(0, 0); - let buffer_end = buffer - .read_with(&cx, |buffer, _| buffer.max_point_utf16()) - .to_lsp_position(); + let buffer_end = + buffer.read_with(&cx, |buffer, _| point_to_lsp(buffer.max_point_utf16())); language_server .request::( lsp::DocumentRangeFormattingParams { @@ -2337,7 +2337,7 @@ impl Project { lsp::TextDocumentIdentifier::new( lsp::Url::from_file_path(buffer_abs_path).unwrap(), ), - position.to_lsp_position(), + point_to_lsp(position), ), context: Default::default(), work_done_progress_params: Default::default(), @@ -2511,7 +2511,7 @@ impl Project { } } - pub fn code_actions( + pub fn code_actions( &self, buffer_handle: &ModelHandle, range: Range, @@ -2519,6 +2519,11 @@ impl Project { ) -> Task>> { let buffer_handle = buffer_handle.clone(); let buffer = buffer_handle.read(cx); + let snapshot = buffer.snapshot(); + let relevant_diagnostics = snapshot + .diagnostics_in_range::(range.to_offset(&snapshot), false) + .map(|entry| entry.to_lsp_diagnostic_stub()) + .collect(); let buffer_id = buffer.remote_id(); let worktree; let buffer_abs_path; @@ -2539,10 +2544,7 @@ impl Project { return Task::ready(Ok(Default::default())); }; - let lsp_range = lsp::Range::new( - range.start.to_point_utf16(buffer).to_lsp_position(), - range.end.to_point_utf16(buffer).to_lsp_position(), - ); + let lsp_range = range_to_lsp(range.to_point_utf16(buffer)); cx.foreground().spawn(async move { if !lang_server.capabilities().code_action_provider.is_some() { return Ok(Default::default()); @@ -2557,11 +2559,12 @@ impl Project { work_done_progress_params: Default::default(), partial_result_params: Default::default(), context: lsp::CodeActionContext { - diagnostics: Default::default(), + diagnostics: relevant_diagnostics, only: Some(vec![ lsp::CodeActionKind::QUICKFIX, lsp::CodeActionKind::REFACTOR, lsp::CodeActionKind::REFACTOR_EXTRACT, + lsp::CodeActionKind::SOURCE, ]), }, }) @@ -2636,11 +2639,7 @@ impl Project { .and_then(|d| d.get_mut("codeActionParams")) .and_then(|d| d.get_mut("range")) { - *lsp_range = serde_json::to_value(&lsp::Range::new( - range.start.to_lsp_position(), - range.end.to_lsp_position(), - )) - .unwrap(); + *lsp_range = serde_json::to_value(&range_to_lsp(range)).unwrap(); action.lsp_action = lang_server .request::(action.lsp_action) .await?; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 1b22d09b567214ec0311e6ae23e75ca0cff86d92..a010e55c32940e8f1ec5304e87d0798557b72f74 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1088,8 +1088,8 @@ mod tests { }; use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle}; use language::{ - tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, - LanguageRegistry, OffsetRangeExt, Point, ToLspPosition, + range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, + LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, }; use lsp::{self, FakeLanguageServer}; use parking_lot::Mutex; @@ -4979,14 +4979,9 @@ mod tests { for _ in 0..highlight_count { let range = buffer.random_byte_range(prev_end, &mut *rng.lock()); - let start = buffer - .offset_to_point_utf16(range.start) - .to_lsp_position(); - let end = buffer - .offset_to_point_utf16(range.end) - .to_lsp_position(); + highlights.push(lsp::DocumentHighlight { - range: lsp::Range::new(start, end), + range: range_to_lsp(range.to_point_utf16(buffer)), kind: Some(lsp::DocumentHighlightKind::READ), }); prev_end = range.end; From afbddc1bcde8543dee73a818c4a660e3116a7efe Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 31 Mar 2022 18:22:55 -0700 Subject: [PATCH 15/24] Address panic when completions requested and returned to outdated buffer --- crates/project/src/project.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 65c943c888247b5c02badc3e3b59c603f325a290..856405d620a0e00741b8e7976d4e560db6e3d6c5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2331,6 +2331,13 @@ impl Project { }; cx.spawn(|_, cx| async move { + let clipped_position = source_buffer_handle + .read_with(&cx, |this, _| this.clip_point_utf16(position, Bias::Left)); + if clipped_position != position { + log::info!("Completion position out of date"); + return Ok(Default::default()); + } + let completions = lang_server .request::(lsp::CompletionParams { text_document_position: lsp::TextDocumentPositionParams::new( From e987a8ba634dbd5246ec1d4dc4f7ab3f61e4f8b8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 31 Mar 2022 21:52:14 -0700 Subject: [PATCH 16/24] Let fake and real LanguageServer access AsyncAppContext in handler callbacks Also, reimplement FakeLanguageServer by wrapping LanguageServer, instead of duplicating its functionality differently. --- crates/editor/src/editor.rs | 14 +- crates/language/src/language.rs | 18 +- crates/lsp/src/lsp.rs | 496 +++++++++++++------------------- crates/project/src/project.rs | 82 +++--- crates/server/src/rpc.rs | 118 ++++---- 5 files changed, 313 insertions(+), 415 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c7373d84f27863fe5f9d03702cf438d5c14fc280..0dccce37a9e5461c113f79b89f3faa2cdcd04d8f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8928,7 +8928,7 @@ mod tests { .unwrap(); cx.foreground().start_waiting(); - let mut fake_server = fake_servers.next().await.unwrap(); + let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); @@ -8942,10 +8942,10 @@ mod tests { params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); - Some(vec![lsp::TextEdit::new( + Ok(Some(vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), ", ".to_string(), - )]) + )])) }) .next() .await; @@ -9173,7 +9173,7 @@ mod tests { params.text_document_position.position, lsp::Position::new(position.row, position.column) ); - Some(lsp::CompletionResponse::Array( + Ok(Some(lsp::CompletionResponse::Array( completions .iter() .map(|(range, new_text)| lsp::CompletionItem { @@ -9188,7 +9188,7 @@ mod tests { ..Default::default() }) .collect(), - )) + ))) } }) .next() @@ -9202,7 +9202,7 @@ mod tests { fake.handle_request::(move |_, _| { let edit = edit.clone(); async move { - lsp::CompletionItem { + Ok(lsp::CompletionItem { additional_text_edits: edit.map(|(range, new_text)| { vec![lsp::TextEdit::new( lsp::Range::new( @@ -9213,7 +9213,7 @@ mod tests { )] }), ..Default::default() - } + }) } }) .next() diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 243597320190782c2cbdaaf5ccf9ce4ba9c2407e..322fd19b9e8de659202ff2773907932109917bdc 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -263,14 +263,12 @@ impl LanguageRegistry { #[cfg(any(test, feature = "test-support"))] if language.fake_adapter.is_some() { let language = language.clone(); - return Some(cx.spawn(|mut cx| async move { + return Some(cx.spawn(|cx| async move { let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap(); - let (server, mut fake_server) = cx.update(|cx| { - lsp::LanguageServer::fake_with_capabilities( - fake_adapter.capabilities.clone(), - cx, - ) - }); + let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities( + fake_adapter.capabilities.clone(), + cx.clone(), + ); if let Some(initializer) = &fake_adapter.initializer { initializer(&mut fake_server); @@ -297,10 +295,9 @@ impl LanguageRegistry { let this = self.clone(); let adapter = language.adapter.clone()?; - let background = cx.background().clone(); let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone(); - Some(cx.background().spawn(async move { + Some(cx.spawn(|cx| async move { login_shell_env_loaded.await; let server_binary_path = this .lsp_binary_paths @@ -328,8 +325,7 @@ impl LanguageRegistry { &server_binary_path, server_args, &root_path, - adapter.initialization_options(), - background, + cx, )?; Ok(server) })) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 51a68b83c8997747ffe0880fdab7f9d597417008..6d89b5e8706cb8b05c7919d930f8ec2c2afaa7fc 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,15 +1,17 @@ +pub use lsp_types::*; + use anyhow::{anyhow, Context, Result}; use collections::HashMap; use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; -use gpui::{executor, Task}; -use parking_lot::{Mutex, RwLock}; +use gpui::{executor, AsyncAppContext, Task}; +use parking_lot::Mutex; use postage::{barrier, prelude::Stream}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::{json, value::RawValue, Value}; use smol::{ channel, io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, - process::Command, + process, }; use std::{ future::Future, @@ -22,15 +24,12 @@ use std::{ }, }; use std::{path::Path, process::Stdio}; -use util::TryFutureExt; - -pub use lsp_types::*; +use util::{ResultExt, TryFutureExt}; const JSON_RPC_VERSION: &'static str = "2.0"; const CONTENT_LEN_HEADER: &'static str = "Content-Length: "; -type NotificationHandler = - Box, &str, &mut channel::Sender>) -> Result<()>>; +type NotificationHandler = Box, &str, AsyncAppContext)>; type ResponseHandler = Box)>; pub struct LanguageServer { @@ -39,18 +38,17 @@ pub struct LanguageServer { outbound_tx: channel::Sender>, name: String, capabilities: ServerCapabilities, - notification_handlers: Arc>>, + notification_handlers: Arc>>, response_handlers: Arc>>, executor: Arc, io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, root_path: PathBuf, - options: Option, } pub struct Subscription { method: &'static str, - notification_handlers: Arc>>, + notification_handlers: Arc>>, } #[derive(Serialize, Deserialize)] @@ -61,18 +59,6 @@ struct Request<'a, T> { params: T, } -#[cfg(any(test, feature = "test-support"))] -#[derive(Deserialize)] -struct AnyRequest<'a> { - id: usize, - #[serde(borrow)] - jsonrpc: &'a str, - #[serde(borrow)] - method: &'a str, - #[serde(borrow)] - params: &'a RawValue, -} - #[derive(Serialize, Deserialize)] struct AnyResponse<'a> { id: usize, @@ -85,7 +71,8 @@ struct AnyResponse<'a> { #[derive(Serialize)] struct Response { id: usize, - result: T, + result: Option, + error: Option, } #[derive(Serialize, Deserialize)] @@ -118,15 +105,14 @@ impl LanguageServer { binary_path: &Path, args: &[&str], root_path: &Path, - options: Option, - background: Arc, + cx: AsyncAppContext, ) -> Result { let working_dir = if root_path.is_dir() { root_path } else { root_path.parent().unwrap_or(Path::new("/")) }; - let mut server = Command::new(binary_path) + let mut server = process::Command::new(binary_path) .current_dir(working_dir) .args(args) .stdin(Stdio::piped()) @@ -136,95 +122,91 @@ impl LanguageServer { let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); let mut server = - Self::new_internal(server_id, stdin, stdout, root_path, options, background); + Self::new_internal(server_id, stdin, stdout, root_path, cx, |notification| { + log::info!( + "unhandled notification {}:\n{}", + notification.method, + serde_json::to_string_pretty( + &Value::from_str(notification.params.get()).unwrap() + ) + .unwrap() + ); + }); if let Some(name) = binary_path.file_name() { server.name = name.to_string_lossy().to_string(); } Ok(server) } - fn new_internal( + fn new_internal( server_id: usize, stdin: Stdin, stdout: Stdout, root_path: &Path, - options: Option, - executor: Arc, + cx: AsyncAppContext, + mut on_unhandled_notification: F, ) -> Self where Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send, { let mut stdin = BufWriter::new(stdin); let mut stdout = BufReader::new(stdout); let (outbound_tx, outbound_rx) = channel::unbounded::>(); let notification_handlers = - Arc::new(RwLock::new(HashMap::<_, NotificationHandler>::default())); + Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default())); let response_handlers = Arc::new(Mutex::new(HashMap::<_, ResponseHandler>::default())); - let input_task = executor.spawn( - { - let notification_handlers = notification_handlers.clone(); - let response_handlers = response_handlers.clone(); - let mut outbound_tx = outbound_tx.clone(); - async move { - let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone()); - let mut buffer = Vec::new(); - loop { - buffer.clear(); - stdout.read_until(b'\n', &mut buffer).await?; - stdout.read_until(b'\n', &mut buffer).await?; - let message_len: usize = std::str::from_utf8(&buffer)? - .strip_prefix(CONTENT_LEN_HEADER) - .ok_or_else(|| anyhow!("invalid header"))? - .trim_end() - .parse()?; - - buffer.resize(message_len, 0); - stdout.read_exact(&mut buffer).await?; - log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer)); - - if let Ok(AnyNotification { id, method, params }) = - serde_json::from_slice(&buffer) - { - if let Some(handler) = notification_handlers.write().get_mut(method) { - if let Err(e) = handler(id, params.get(), &mut outbound_tx) { - log::error!("error handling {} message: {:?}", method, e); - } + let input_task = cx.spawn(|cx| { + let notification_handlers = notification_handlers.clone(); + let response_handlers = response_handlers.clone(); + async move { + let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone()); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + stdout.read_until(b'\n', &mut buffer).await?; + stdout.read_until(b'\n', &mut buffer).await?; + let message_len: usize = std::str::from_utf8(&buffer)? + .strip_prefix(CONTENT_LEN_HEADER) + .ok_or_else(|| anyhow!("invalid header"))? + .trim_end() + .parse()?; + + buffer.resize(message_len, 0); + stdout.read_exact(&mut buffer).await?; + log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer)); + + if let Ok(msg) = serde_json::from_slice::(&buffer) { + if let Some(handler) = notification_handlers.lock().get_mut(msg.method) { + handler(msg.id, msg.params.get(), cx.clone()); + } else { + on_unhandled_notification(msg); + } + } else if let Ok(AnyResponse { id, error, result }) = + serde_json::from_slice(&buffer) + { + if let Some(handler) = response_handlers.lock().remove(&id) { + if let Some(error) = error { + handler(Err(error)); + } else if let Some(result) = result { + handler(Ok(result.get())); } else { - log::info!( - "unhandled notification {}:\n{}", - method, - serde_json::to_string_pretty( - &Value::from_str(params.get()).unwrap() - ) - .unwrap() - ); - } - } else if let Ok(AnyResponse { id, error, result }) = - serde_json::from_slice(&buffer) - { - if let Some(handler) = response_handlers.lock().remove(&id) { - if let Some(error) = error { - handler(Err(error)); - } else if let Some(result) = result { - handler(Ok(result.get())); - } else { - handler(Ok("null")); - } + handler(Ok("null")); } - } else { - return Err(anyhow!( - "failed to deserialize message:\n{}", - std::str::from_utf8(&buffer)? - )); } + } else { + return Err(anyhow!( + "failed to deserialize message:\n{}", + std::str::from_utf8(&buffer)? + )); } } } - .log_err(), - ); + .log_err() + }); let (output_done_tx, output_done_rx) = barrier::channel(); - let output_task = executor.spawn({ + let output_task = cx.background().spawn({ let response_handlers = response_handlers.clone(); async move { let _clear_response_handlers = ClearResponseHandlers(response_handlers); @@ -253,18 +235,15 @@ impl LanguageServer { capabilities: Default::default(), next_id: Default::default(), outbound_tx, - executor: executor.clone(), + executor: cx.background().clone(), io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), root_path: root_path.to_path_buf(), - options, } } - pub async fn initialize(mut self) -> Result> { - let options = self.options.take(); - let mut this = Arc::new(self); - let root_uri = Url::from_file_path(&this.root_path).unwrap(); + pub async fn initialize(mut self, options: Option) -> Result> { + let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { process_id: Default::default(), @@ -290,12 +269,13 @@ impl LanguageServer { value_set: vec![ CodeActionKind::REFACTOR.as_str().into(), CodeActionKind::QUICKFIX.as_str().into(), + CodeActionKind::SOURCE.as_str().into(), ], }, }), data_support: Some(true), resolve_support: Some(CodeActionCapabilityResolveSupport { - properties: vec!["edit".to_string()], + properties: vec!["edit".to_string(), "command".to_string()], }), ..Default::default() }), @@ -326,16 +306,14 @@ impl LanguageServer { locale: Default::default(), }; - let response = this.request::(params).await?; - { - let this = Arc::get_mut(&mut this).unwrap(); - if let Some(info) = response.server_info { - this.name = info.name; - } - this.capabilities = response.capabilities; + let response = self.request::(params).await?; + if let Some(info) = response.server_info { + self.name = info.name; } - this.notify::(InitializedParams {})?; - Ok(this) + self.capabilities = response.capabilities; + + self.notify::(InitializedParams {})?; + Ok(Arc::new(self)) } pub fn shutdown(&self) -> Option>> { @@ -370,37 +348,42 @@ impl LanguageServer { } } - pub fn on_notification(&mut self, f: F) -> Subscription + #[must_use] + pub fn on_notification(&self, f: F) -> Subscription where T: notification::Notification, - F: 'static + Send + Sync + FnMut(T::Params), + F: 'static + Send + FnMut(T::Params, AsyncAppContext), { self.on_custom_notification(T::METHOD, f) } - pub fn on_request(&mut self, f: F) -> Subscription + #[must_use] + pub fn on_request(&self, f: F) -> Subscription where T: request::Request, - F: 'static + Send + Sync + FnMut(T::Params) -> Result, + T::Params: 'static + Send, + F: 'static + Send + FnMut(T::Params, AsyncAppContext) -> Fut, + Fut: 'static + Future>, { self.on_custom_request(T::METHOD, f) } - pub fn on_custom_notification( - &mut self, - method: &'static str, - mut f: F, - ) -> Subscription + pub fn remove_request_handler(&self) { + self.notification_handlers.lock().remove(T::METHOD); + } + + #[must_use] + pub fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription where - F: 'static + Send + Sync + FnMut(Params), + F: 'static + Send + FnMut(Params, AsyncAppContext), Params: DeserializeOwned, { - let prev_handler = self.notification_handlers.write().insert( + let prev_handler = self.notification_handlers.lock().insert( method, - Box::new(move |_, params, _| { - let params = serde_json::from_str(params)?; - f(params); - Ok(()) + Box::new(move |_, params, cx| { + if let Some(params) = serde_json::from_str(params).log_err() { + f(params, cx); + } }), ); assert!( @@ -413,26 +396,52 @@ impl LanguageServer { } } - pub fn on_custom_request( - &mut self, + #[must_use] + pub fn on_custom_request( + &self, method: &'static str, mut f: F, ) -> Subscription where - F: 'static + Send + Sync + FnMut(Params) -> Result, - Params: DeserializeOwned, + F: 'static + Send + FnMut(Params, AsyncAppContext) -> Fut, + Fut: 'static + Future>, + Params: DeserializeOwned + Send + 'static, Res: Serialize, { - let prev_handler = self.notification_handlers.write().insert( + let outbound_tx = self.outbound_tx.clone(); + let prev_handler = self.notification_handlers.lock().insert( method, - Box::new(move |id, params, tx| { + Box::new(move |id, params, cx| { if let Some(id) = id { - let params = serde_json::from_str(params)?; - let result = f(params)?; - let response = serde_json::to_vec(&Response { id, result })?; - tx.try_send(response)?; + if let Some(params) = serde_json::from_str(params).log_err() { + let response = f(params, cx.clone()); + cx.foreground() + .spawn({ + let outbound_tx = outbound_tx.clone(); + async move { + let response = match response.await { + Ok(result) => Response { + id, + result: Some(result), + error: None, + }, + Err(error) => Response { + id, + result: None, + error: Some(Error { + message: error.to_string(), + }), + }, + }; + if let Some(response) = serde_json::to_vec(&response).log_err() + { + outbound_tx.try_send(response).ok(); + } + } + }) + .detach(); + } } - Ok(()) }), ); assert!( @@ -458,7 +467,7 @@ impl LanguageServer { } pub fn request( - self: &Arc, + &self, params: T::Params, ) -> impl Future> where @@ -549,36 +558,16 @@ impl Subscription { impl Drop for Subscription { fn drop(&mut self) { - self.notification_handlers.write().remove(self.method); + self.notification_handlers.lock().remove(self.method); } } #[cfg(any(test, feature = "test-support"))] pub struct FakeLanguageServer { - handlers: FakeLanguageServerHandlers, - outgoing_tx: futures::channel::mpsc::UnboundedSender>, - incoming_rx: futures::channel::mpsc::UnboundedReceiver>, - _input_task: Task>, - _output_task: Task>, + server: Arc, + notifications_rx: channel::Receiver<(String, String)>, } -#[cfg(any(test, feature = "test-support"))] -type FakeLanguageServerHandlers = Arc< - Mutex< - HashMap< - &'static str, - Box< - dyn Send - + FnMut( - usize, - &[u8], - gpui::AsyncAppContext, - ) -> futures::future::BoxFuture<'static, Vec>, - >, - >, - >, ->; - #[cfg(any(test, feature = "test-support"))] impl LanguageServer { pub fn full_capabilities() -> ServerCapabilities { @@ -591,177 +580,101 @@ impl LanguageServer { } } - pub fn fake(cx: &mut gpui::MutableAppContext) -> (Self, FakeLanguageServer) { + pub fn fake(cx: AsyncAppContext) -> (Self, FakeLanguageServer) { Self::fake_with_capabilities(Self::full_capabilities(), cx) } pub fn fake_with_capabilities( capabilities: ServerCapabilities, - cx: &mut gpui::MutableAppContext, + cx: AsyncAppContext, ) -> (Self, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); + let (notifications_tx, notifications_rx) = channel::unbounded(); - let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx); + let server = Self::new_internal( + 0, + stdin_writer, + stdout_reader, + Path::new("/"), + cx.clone(), + |_| {}, + ); + let fake = FakeLanguageServer { + server: Arc::new(Self::new_internal( + 0, + stdout_writer, + stdin_reader, + Path::new("/"), + cx.clone(), + move |msg| { + notifications_tx + .try_send((msg.method.to_string(), msg.params.get().to_string())) + .ok(); + }, + )), + notifications_rx, + }; fake.handle_request::({ let capabilities = capabilities.clone(); move |_, _| { let capabilities = capabilities.clone(); async move { - InitializeResult { + Ok(InitializeResult { capabilities, ..Default::default() - } + }) } } }); - let executor = cx.background().clone(); - let server = Self::new_internal( - 0, - stdin_writer, - stdout_reader, - Path::new("/"), - None, - executor, - ); (server, fake) } } #[cfg(any(test, feature = "test-support"))] impl FakeLanguageServer { - fn new( - stdin: async_pipe::PipeReader, - stdout: async_pipe::PipeWriter, - cx: &mut gpui::MutableAppContext, - ) -> Self { - use futures::StreamExt as _; - - let (incoming_tx, incoming_rx) = futures::channel::mpsc::unbounded(); - let (outgoing_tx, mut outgoing_rx) = futures::channel::mpsc::unbounded(); - let handlers = FakeLanguageServerHandlers::default(); - - let input_task = cx.spawn(|cx| { - let handlers = handlers.clone(); - let outgoing_tx = outgoing_tx.clone(); - async move { - let mut buffer = Vec::new(); - let mut stdin = smol::io::BufReader::new(stdin); - while Self::receive(&mut stdin, &mut buffer).await.is_ok() { - cx.background().simulate_random_delay().await; - - if let Ok(request) = serde_json::from_slice::(&buffer) { - assert_eq!(request.jsonrpc, JSON_RPC_VERSION); - - let response; - if let Some(handler) = handlers.lock().get_mut(request.method) { - response = - handler(request.id, request.params.get().as_bytes(), cx.clone()) - .await; - log::debug!("handled lsp request. method:{}", request.method); - } else { - response = serde_json::to_vec(&AnyResponse { - id: request.id, - error: Some(Error { - message: "no handler".to_string(), - }), - result: None, - }) - .unwrap(); - log::debug!("unhandled lsp request. method:{}", request.method); - } - outgoing_tx.unbounded_send(response)?; - } else { - incoming_tx.unbounded_send(buffer.clone())?; - } - } - Ok::<_, anyhow::Error>(()) - } - }); - - let output_task = cx.background().spawn(async move { - let mut stdout = smol::io::BufWriter::new(stdout); - while let Some(message) = outgoing_rx.next().await { - stdout.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; - stdout - .write_all((format!("{}", message.len())).as_bytes()) - .await?; - stdout.write_all("\r\n\r\n".as_bytes()).await?; - stdout.write_all(&message).await?; - stdout.flush().await?; - } - Ok(()) - }); - - Self { - outgoing_tx, - incoming_rx, - handlers, - _input_task: input_task, - _output_task: output_task, - } - } - - pub fn notify(&mut self, params: T::Params) { - let message = serde_json::to_vec(&Notification { - jsonrpc: JSON_RPC_VERSION, - method: T::METHOD, - params, - }) - .unwrap(); - self.outgoing_tx.unbounded_send(message).unwrap(); + pub fn notify(&self, params: T::Params) { + self.server.notify::(params).ok(); } pub async fn receive_notification(&mut self) -> T::Params { use futures::StreamExt as _; loop { - let bytes = self.incoming_rx.next().await.unwrap(); - if let Ok(notification) = serde_json::from_slice::>(&bytes) { - assert_eq!(notification.method, T::METHOD); - return notification.params; + let (method, params) = self.notifications_rx.next().await.unwrap(); + if &method == T::METHOD { + return serde_json::from_str::(¶ms).unwrap(); } else { - log::info!( - "skipping message in fake language server {:?}", - std::str::from_utf8(&bytes) - ); + log::info!("skipping message in fake language server {:?}", params); } } } pub fn handle_request( - &mut self, + &self, mut handler: F, ) -> futures::channel::mpsc::UnboundedReceiver<()> where T: 'static + request::Request, + T::Params: 'static + Send, F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut, - Fut: 'static + Send + Future, + Fut: 'static + Send + Future>, { - use futures::FutureExt as _; - let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded(); - self.handlers.lock().insert( - T::METHOD, - Box::new(move |id, params, cx| { - let result = handler(serde_json::from_slice::(params).unwrap(), cx); + self.server.remove_request_handler::(); + self.server + .on_request::(move |params, cx| { + let result = handler(params, cx.clone()); let responded_tx = responded_tx.clone(); async move { + cx.background().simulate_random_delay().await; let result = result.await; - let result = serde_json::to_string(&result).unwrap(); - let result = serde_json::from_str::<&RawValue>(&result).unwrap(); - let response = AnyResponse { - id, - error: None, - result: Some(result), - }; responded_tx.unbounded_send(()).ok(); - serde_json::to_vec(&response).unwrap() + result } - .boxed() - }), - ); + }) + .detach(); responded_rx } @@ -769,7 +682,7 @@ impl FakeLanguageServer { where T: 'static + request::Request, { - self.handlers.lock().remove(T::METHOD); + self.server.remove_request_handler::(); } pub async fn start_progress(&mut self, token: impl Into) { @@ -785,25 +698,6 @@ impl FakeLanguageServer { value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())), }); } - - async fn receive( - stdin: &mut smol::io::BufReader, - buffer: &mut Vec, - ) -> Result<()> { - buffer.clear(); - stdin.read_until(b'\n', buffer).await?; - stdin.read_until(b'\n', buffer).await?; - let message_len: usize = std::str::from_utf8(buffer) - .unwrap() - .strip_prefix(CONTENT_LEN_HEADER) - .ok_or_else(|| anyhow!("invalid content length header"))? - .trim_end() - .parse() - .unwrap(); - buffer.resize(message_len, 0); - stdin.read_exact(buffer).await?; - Ok(()) - } } struct ClearResponseHandlers(Arc>>); @@ -828,22 +722,22 @@ mod tests { #[gpui::test] async fn test_fake(cx: &mut TestAppContext) { - let (mut server, mut fake) = cx.update(LanguageServer::fake); + let (server, mut fake) = LanguageServer::fake(cx.to_async()); let (message_tx, message_rx) = channel::unbounded(); let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); server - .on_notification::(move |params| { + .on_notification::(move |params, _| { message_tx.try_send(params).unwrap() }) .detach(); server - .on_notification::(move |params| { + .on_notification::(move |params, _| { diagnostics_tx.try_send(params).unwrap() }) .detach(); - let server = server.initialize().await.unwrap(); + let server = server.initialize(None).await.unwrap(); server .notify::(DidOpenTextDocumentParams { text_document: TextDocumentItem::new( @@ -878,7 +772,7 @@ mod tests { "file://b/c" ); - fake.handle_request::(|_, _| async move {}); + fake.handle_request::(|_, _| async move { Ok(()) }); drop(server); fake.receive_notification::().await; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 856405d620a0e00741b8e7976d4e560db6e3d6c5..036a3a29318ce7f0a393cdd489cc3cdcd5e829ae 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1325,7 +1325,7 @@ impl Project { cx, ); cx.spawn_weak(|this, mut cx| async move { - let mut language_server = language_server?.await.log_err()?; + let language_server = language_server?.await.log_err()?; let this = this.upgrade(&cx)?; let (language_server_events_tx, language_server_events_rx) = smol::channel::unbounded(); @@ -1333,7 +1333,7 @@ impl Project { language_server .on_notification::({ let language_server_events_tx = language_server_events_tx.clone(); - move |params| { + move |params, _| { language_server_events_tx .try_send(LanguageServerEvent::DiagnosticsUpdate(params)) .ok(); @@ -1342,31 +1342,33 @@ impl Project { .detach(); language_server - .on_request::({ + .on_request::({ let settings = this .read_with(&cx, |this, _| this.language_server_settings.clone()); - move |params| { - let settings = settings.lock(); - Ok(params - .items - .into_iter() - .map(|item| { - if let Some(section) = &item.section { - settings - .get(section) - .cloned() - .unwrap_or(serde_json::Value::Null) - } else { - settings.clone() - } - }) - .collect()) + move |params, _| { + let settings = settings.lock().clone(); + async move { + Ok(params + .items + .into_iter() + .map(|item| { + if let Some(section) = &item.section { + settings + .get(section) + .cloned() + .unwrap_or(serde_json::Value::Null) + } else { + settings.clone() + } + }) + .collect()) + } } }) .detach(); language_server - .on_notification::(move |params| { + .on_notification::(move |params, _| { let token = match params.token { lsp::NumberOrString::String(token) => token, lsp::NumberOrString::Number(token) => { @@ -1406,6 +1408,11 @@ impl Project { }) .detach(); + let language_server = language_server + .initialize(adapter.initialization_options()) + .await + .log_err()?; + // Process all the LSP events. cx.spawn(|mut cx| { let this = this.downgrade(); @@ -1424,7 +1431,6 @@ impl Project { }) .detach(); - let language_server = language_server.initialize().await.log_err()?; this.update(&mut cx, |this, cx| { this.language_servers .insert(key.clone(), (adapter, language_server.clone())); @@ -4974,9 +4980,9 @@ mod tests { }); let mut rust_shutdown_requests = fake_rust_server - .handle_request::(|_, _| future::ready(())); + .handle_request::(|_, _| future::ready(Ok(()))); let mut json_shutdown_requests = fake_json_server - .handle_request::(|_, _| future::ready(())); + .handle_request::(|_, _| future::ready(Ok(()))); futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next()); let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); @@ -5917,19 +5923,11 @@ mod tests { .await; let buffer = project - .update(cx, |project, cx| { - project.open_buffer( - ProjectPath { - worktree_id, - path: Path::new("").into(), - }, - cx, - ) - }) + .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) .await .unwrap(); - let mut fake_server = fake_servers.next().await.unwrap(); + let fake_server = fake_servers.next().await.unwrap(); fake_server.handle_request::(|params, _| async move { let params = params.text_document_position_params; assert_eq!( @@ -5938,9 +5936,11 @@ mod tests { ); assert_eq!(params.position, lsp::Position::new(0, 22)); - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/dir/a.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/dir/a.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + ), ))) }); @@ -6854,7 +6854,7 @@ mod tests { .await .unwrap(); - let mut fake_server = fake_servers.next().await.unwrap(); + let fake_server = fake_servers.next().await.unwrap(); let response = project.update(cx, |project, cx| { project.prepare_rename(buffer.clone(), 7, cx) @@ -6863,10 +6863,10 @@ mod tests { .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( lsp::Position::new(0, 6), lsp::Position::new(0, 9), - ))) + )))) }) .next() .await @@ -6889,7 +6889,7 @@ mod tests { lsp::Position::new(0, 7) ); assert_eq!(params.new_name, "THREE"); - Some(lsp::WorkspaceEdit { + Ok(Some(lsp::WorkspaceEdit { changes: Some( [ ( @@ -6926,7 +6926,7 @@ mod tests { .collect(), ), ..Default::default() - }) + })) }) .next() .await diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index a010e55c32940e8f1ec5304e87d0798557b72f74..b158a17e877b8e61915fbb94b4459cf549ea6f99 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -2342,7 +2342,7 @@ mod tests { Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) }); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); buffer_b .condition(&cx_b, |buffer, _| !buffer.completion_triggers().is_empty()) .await; @@ -2368,7 +2368,7 @@ mod tests { lsp::Position::new(0, 14), ); - Some(lsp::CompletionResponse::Array(vec![ + Ok(Some(lsp::CompletionResponse::Array(vec![ lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), @@ -2395,7 +2395,7 @@ mod tests { insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() }, - ])) + ]))) }) .next() .await @@ -2425,7 +2425,7 @@ mod tests { fake_language_server.handle_request::( |params, _| async move { assert_eq!(params.label, "first_method(…)"); - lsp::CompletionItem { + Ok(lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { @@ -2441,7 +2441,7 @@ mod tests { }]), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() - } + }) }, ); @@ -2530,9 +2530,9 @@ mod tests { .await .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| async move { - Some(vec![ + Ok(Some(vec![ lsp::TextEdit { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), new_text: "h".to_string(), @@ -2541,7 +2541,7 @@ mod tests { range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)), new_text: "y".to_string(), }, - ]) + ])) }); project_b @@ -2637,12 +2637,14 @@ mod tests { .unwrap(); // Request the definition of a symbol as the guest. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |_, _| async move { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ), ))) }, ); @@ -2669,9 +2671,11 @@ mod tests { // the previous call to `definition`. fake_language_server.handle_request::( |_, _| async move { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), + ), ))) }, ); @@ -2778,14 +2782,14 @@ mod tests { .unwrap(); // Request references to a symbol as the guest. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |params, _| async move { assert_eq!( params.text_document_position.text_document.uri.as_str(), "file:///root-1/one.rs" ); - Some(vec![ + Ok(Some(vec![ lsp::Location { uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), range: lsp::Range::new( @@ -2807,7 +2811,7 @@ mod tests { lsp::Position::new(0, 40), ), }, - ]) + ])) }, ); @@ -3018,7 +3022,7 @@ mod tests { .unwrap(); // Request document highlights as the guest. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |params, _| async move { assert_eq!( @@ -3033,7 +3037,7 @@ mod tests { params.text_document_position_params.position, lsp::Position::new(0, 34) ); - Some(vec![ + Ok(Some(vec![ lsp::DocumentHighlight { kind: Some(lsp::DocumentHighlightKind::WRITE), range: lsp::Range::new( @@ -3055,7 +3059,7 @@ mod tests { lsp::Position::new(0, 47), ), }, - ]) + ])) }, ); @@ -3162,11 +3166,11 @@ mod tests { .await .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |_, _| async move { #[allow(deprecated)] - Some(vec![lsp::SymbolInformation { + Ok(Some(vec![lsp::SymbolInformation { name: "TWO".into(), location: lsp::Location { uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(), @@ -3176,7 +3180,7 @@ mod tests { tags: None, container_name: None, deprecated: None, - }]) + }])) }, ); @@ -3292,12 +3296,14 @@ mod tests { .await .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |_, _| async move { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/root/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ), ))) }, ); @@ -3413,7 +3419,7 @@ mod tests { ); assert_eq!(params.range.start, lsp::Position::new(0, 0)); assert_eq!(params.range.end, lsp::Position::new(0, 0)); - None + Ok(None) }) .next() .await; @@ -3433,7 +3439,7 @@ mod tests { assert_eq!(params.range.start, lsp::Position::new(1, 31)); assert_eq!(params.range.end, lsp::Position::new(1, 31)); - Some(vec![lsp::CodeActionOrCommand::CodeAction( + Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( lsp::CodeAction { title: "Inline into all callers".to_string(), edit: Some(lsp::WorkspaceEdit { @@ -3475,7 +3481,7 @@ mod tests { })), ..Default::default() }, - )]) + )])) }) .next() .await; @@ -3498,7 +3504,7 @@ mod tests { .unwrap(); fake_language_server.handle_request::( |_, _| async move { - lsp::CodeAction { + Ok(lsp::CodeAction { title: "Inline into all callers".to_string(), edit: Some(lsp::WorkspaceEdit { changes: Some( @@ -3530,7 +3536,7 @@ mod tests { ..Default::default() }), ..Default::default() - } + }) }, ); @@ -3637,7 +3643,7 @@ mod tests { .unwrap() .downcast::() .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); // Move cursor to a location that can be renamed. let prepare_rename = editor_b.update(cx_b, |editor, cx| { @@ -3649,10 +3655,10 @@ mod tests { .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( lsp::Position::new(0, 6), lsp::Position::new(0, 9), - ))) + )))) }) .next() .await @@ -3686,7 +3692,7 @@ mod tests { lsp::Position::new(0, 6) ); assert_eq!(params.new_name, "THREE"); - Some(lsp::WorkspaceEdit { + Ok(Some(lsp::WorkspaceEdit { changes: Some( [ ( @@ -3723,7 +3729,7 @@ mod tests { .collect(), ), ..Default::default() - }) + })) }) .next() .await @@ -4894,36 +4900,38 @@ mod tests { move |fake_server: &mut FakeLanguageServer| { fake_server.handle_request::( |_, _| async move { - Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem { - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 0), - ), - new_text: "the-new-text".to_string(), - })), - ..Default::default() - }])) + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "the-new-text".to_string(), + })), + ..Default::default() + }, + ]))) }, ); fake_server.handle_request::( |_, _| async move { - Some(vec![lsp::CodeActionOrCommand::CodeAction( + Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( lsp::CodeAction { title: "the-code-action".to_string(), ..Default::default() }, - )]) + )])) }, ); fake_server.handle_request::( |params, _| async move { - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( params.position, params.position, - ))) + )))) }, ); @@ -4941,7 +4949,7 @@ mod tests { .map(|_| files.choose(&mut *rng).unwrap()) .collect::>(); log::info!("LSP: Returning definitions in files {:?}", &files); - Some(lsp::GotoDefinitionResponse::Array( + Ok(Some(lsp::GotoDefinitionResponse::Array( files .into_iter() .map(|file| lsp::Location { @@ -4949,7 +4957,7 @@ mod tests { range: Default::default(), }) .collect(), - )) + ))) } } }); @@ -4991,7 +4999,7 @@ mod tests { } else { None }; - async move { highlights } + async move { Ok(highlights) } } }); } From fed5d141bcd34e9188d4e9c5d88db6d4c8ec8ff5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 31 Mar 2022 22:03:52 -0700 Subject: [PATCH 17/24] Start work on applying code actions that use commands Co-Authored-By: Keith Simmons --- crates/project/src/project.rs | 326 +++++++++++++++++++++++++--------- 1 file changed, 243 insertions(+), 83 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 036a3a29318ce7f0a393cdd489cc3cdcd5e829ae..e37124dda01d4cd2f2fc1e3e8b089bd16dba1f3f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -143,6 +143,7 @@ enum LanguageServerEvent { token: String, }, DiagnosticsUpdate(lsp::PublishDiagnosticsParams), + WorkspaceEdit(lsp::ApplyWorkspaceEditParams), } pub struct LanguageServerStatus { @@ -1367,6 +1368,24 @@ impl Project { }) .detach(); + language_server + .on_request::({ + let language_server_events_tx = language_server_events_tx.clone(); + move |params, _| { + language_server_events_tx + .try_send(LanguageServerEvent::WorkspaceEdit(params)) + .ok(); + async move { + Ok(lsp::ApplyWorkspaceEditResponse { + applied: true, + failed_change: None, + failure_reason: None, + }) + } + } + }) + .detach(); + language_server .on_notification::(move |params, _| { let token = match params.token { @@ -1416,12 +1435,20 @@ impl Project { // Process all the LSP events. cx.spawn(|mut cx| { let this = this.downgrade(); + let adapter = adapter.clone(); + let language_server = language_server.clone(); async move { while let Ok(event) = language_server_events_rx.recv().await { let this = this.upgrade(&cx)?; - this.update(&mut cx, |this, cx| { - this.on_lsp_event(server_id, event, &language, cx) - }); + Self::on_lsp_event( + this, + server_id, + &adapter, + &language_server, + event, + &mut cx, + ) + .await; // Don't starve the main thread when lots of events arrive all at once. smol::future::yield_now().await; @@ -1585,109 +1612,142 @@ impl Project { .detach(); } - fn on_lsp_event( - &mut self, + async fn on_lsp_event( + this: ModelHandle, language_server_id: usize, + adapter: &Arc, + language_server: &Arc, event: LanguageServerEvent, - language: &Arc, - cx: &mut ModelContext, + cx: &mut AsyncAppContext, ) { - let disk_diagnostics_token = language.disk_based_diagnostics_progress_token(); - let language_server_status = - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - status - } else { - return; - }; - + let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token(); match event { LanguageServerEvent::WorkStart { token } => { - if Some(token.as_str()) == disk_diagnostics_token { - language_server_status.pending_diagnostic_updates += 1; - if language_server_status.pending_diagnostic_updates == 1 { - self.disk_based_diagnostics_started(cx); - self.broadcast_language_server_update( + this.update(cx, |this, cx| { + let language_server_status = if let Some(status) = + this.language_server_statuses.get_mut(&language_server_id) + { + status + } else { + return; + }; + + if Some(token.as_str()) == disk_based_diagnostics_progress_token { + language_server_status.pending_diagnostic_updates += 1; + if language_server_status.pending_diagnostic_updates == 1 { + this.disk_based_diagnostics_started(cx); + this.broadcast_language_server_update( language_server_id, proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( proto::LspDiskBasedDiagnosticsUpdating {}, ), ); + } + } else { + this.on_lsp_work_start(language_server_id, token.clone(), cx); + this.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::WorkStart( + proto::LspWorkStart { token }, + ), + ); } - } else { - self.on_lsp_work_start(language_server_id, token.clone(), cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { - token, - }), - ); - } + }); } LanguageServerEvent::WorkProgress { token, progress } => { - if Some(token.as_str()) != disk_diagnostics_token { - self.on_lsp_work_progress( - language_server_id, - token.clone(), - progress.clone(), - cx, - ); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::WorkProgress( - proto::LspWorkProgress { - token, - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), - }, - ), - ); - } + this.update(cx, |this, cx| { + if Some(token.as_str()) != disk_based_diagnostics_progress_token { + this.on_lsp_work_progress( + language_server_id, + token.clone(), + progress.clone(), + cx, + ); + this.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::WorkProgress( + proto::LspWorkProgress { + token, + message: progress.message, + percentage: progress.percentage.map(|p| p as u32), + }, + ), + ); + } + }); } LanguageServerEvent::WorkEnd { token } => { - if Some(token.as_str()) == disk_diagnostics_token { - language_server_status.pending_diagnostic_updates -= 1; - if language_server_status.pending_diagnostic_updates == 0 { - self.disk_based_diagnostics_finished(cx); - self.broadcast_language_server_update( + this.update(cx, |this, cx| { + if Some(token.as_str()) == disk_based_diagnostics_progress_token { + let language_server_status = if let Some(status) = + this.language_server_statuses.get_mut(&language_server_id) + { + status + } else { + return; + }; + + language_server_status.pending_diagnostic_updates -= 1; + if language_server_status.pending_diagnostic_updates == 0 { + this.disk_based_diagnostics_finished(cx); + this.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + } + } else { + this.on_lsp_work_end(language_server_id, token.clone(), cx); + this.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { + token, + }), + ); + } + }); + } + LanguageServerEvent::DiagnosticsUpdate(mut params) => { + this.update(cx, |this, cx| { + adapter.process_diagnostics(&mut params); + + if disk_based_diagnostics_progress_token.is_none() { + this.disk_based_diagnostics_started(cx); + this.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); + } + this.update_diagnostics(params, adapter.disk_based_diagnostic_sources(), cx) + .log_err(); + if disk_based_diagnostics_progress_token.is_none() { + this.disk_based_diagnostics_finished(cx); + this.broadcast_language_server_update( language_server_id, proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( proto::LspDiskBasedDiagnosticsUpdated {}, ), ); } - } else { - self.on_lsp_work_end(language_server_id, token.clone(), cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { - token, - }), - ); - } + }); } - LanguageServerEvent::DiagnosticsUpdate(mut params) => { - language.process_diagnostics(&mut params); + LanguageServerEvent::WorkspaceEdit(params) => { + let transaction = Self::deserialize_workspace_edit( + this, + params.edit, + false, + adapter.clone(), + language_server.clone(), + cx, + ) + .await + .log_err(); - if disk_diagnostics_token.is_none() { - self.disk_based_diagnostics_started(cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, - ), - ); - } - self.update_diagnostics(params, language.disk_based_diagnostic_sources(), cx) - .log_err(); - if disk_diagnostics_token.is_none() { - self.disk_based_diagnostics_finished(cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - ), - ); - } + // Check if there is a code action currently running, using the state that is + // set in `start_code_action`. If so, then store the transaction for later use. } } } @@ -2679,6 +2739,16 @@ impl Project { &mut cx, ) .await + } else if let Some(command) = action.lsp_action.command { + this.update(&mut cx, |this, _| this.start_code_action()); + lang_server + .request::(lsp::ExecuteCommandParams { + command: command.command, + arguments: command.arguments.unwrap_or_default(), + ..Default::default() + }) + .await?; + Ok(this.update(&mut cx, |this, cx| this.finish_code_action(cx))) } else { Ok(ProjectTransaction::default()) } @@ -2837,6 +2907,17 @@ impl Project { Ok(project_transaction) } + fn start_code_action(&mut self) { + // Set some state that will be read inside of `on_lsp_event` when handling a `WorkspaceEdit` + // event, and will cause the `ProjectTransaction` to be stored. + } + + fn finish_code_action(&mut self, cx: &mut ModelContext) -> ProjectTransaction { + // Retrieve all stored `ProjectTransactions` that have been received since `start_code_action` + // was called, and combine them together. + Default::default() + } + pub fn prepare_rename( &self, buffer: ModelHandle, @@ -5992,6 +6073,85 @@ mod tests { } } + #[gpui::test] + async fn test_apply_code_action(cx: &mut gpui::TestAppContext) { + let mut language = Language::new( + LanguageConfig { + name: "TypeScript".into(), + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.ts": "", + }), + ) + .await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let (tree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(cx, |tree, _| tree.id()); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let buffer = project + .update(cx, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx)) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); + fake_server + .handle_request::(|params, _| async move { + Ok(Some(vec![ + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "The code action".into(), + command: Some(lsp::Command { + title: "The command".into(), + command: "_the/command".into(), + arguments: Some(vec![json!("the-argument")]), + }), + ..Default::default() + }), + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "two".into(), + ..Default::default() + }), + ])) + }) + .next() + .await; + + let action = actions.await.unwrap()[0].clone(); + let apply = project.update(cx, |project, cx| { + project.apply_code_action(buffer.clone(), action, true, cx) + }); + fake_server.handle_request::( + |action, _| async move { Ok(action) }, + ); + fake_server + .handle_request::(move |params, cx| async move { + // fake_server.send(); + Ok(Some(json!(null))) + }) + .next() + .await; + } + #[gpui::test] async fn test_save_file(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.background()); From 56523b5775e780f2755e4683008d8b3c57f27de8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Apr 2022 10:16:26 -0700 Subject: [PATCH 18/24] Allow applying code actions that use commands Co-Authored-By: Antonio Scandurra --- crates/lsp/src/lsp.rs | 3 +- crates/project/src/project.rs | 83 +++++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 6d89b5e8706cb8b05c7919d930f8ec2c2afaa7fc..f5fc98640d7400160b104ec6654d0260c3ffd25c 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -563,8 +563,9 @@ impl Drop for Subscription { } #[cfg(any(test, feature = "test-support"))] +#[derive(Clone)] pub struct FakeLanguageServer { - server: Arc, + pub server: Arc, notifications_rx: channel::Receiver<(String, String)>, } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e37124dda01d4cd2f2fc1e3e8b089bd16dba1f3f..4225f9657fbfcf351f1eb0d2efe1aa841278252a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -64,6 +64,7 @@ pub struct Project { HashMap<(WorktreeId, LanguageServerName), Task>>>, language_server_statuses: BTreeMap, language_server_settings: Arc>, + last_workspace_edits_by_language_server: HashMap, next_language_server_id: usize, client: Arc, next_entry_id: Arc, @@ -346,6 +347,7 @@ impl Project { language_servers: Default::default(), started_language_servers: Default::default(), language_server_statuses: Default::default(), + last_workspace_edits_by_language_server: Default::default(), language_server_settings: Default::default(), next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), @@ -433,6 +435,7 @@ impl Project { ) }) .collect(), + last_workspace_edits_by_language_server: Default::default(), next_language_server_id: 0, opened_buffers: Default::default(), buffer_snapshots: Default::default(), @@ -1736,18 +1739,21 @@ impl Project { } LanguageServerEvent::WorkspaceEdit(params) => { let transaction = Self::deserialize_workspace_edit( - this, + this.clone(), params.edit, - false, + true, adapter.clone(), language_server.clone(), cx, ) .await .log_err(); - - // Check if there is a code action currently running, using the state that is - // set in `start_code_action`. If so, then store the transaction for later use. + this.update(cx, |this, _| { + if let Some(transaction) = transaction { + this.last_workspace_edits_by_language_server + .insert(language_server_id, transaction); + } + }); } } } @@ -2740,7 +2746,6 @@ impl Project { ) .await } else if let Some(command) = action.lsp_action.command { - this.update(&mut cx, |this, _| this.start_code_action()); lang_server .request::(lsp::ExecuteCommandParams { command: command.command, @@ -2748,7 +2753,11 @@ impl Project { ..Default::default() }) .await?; - Ok(this.update(&mut cx, |this, cx| this.finish_code_action(cx))) + Ok(this.update(&mut cx, |this, _| { + this.last_workspace_edits_by_language_server + .remove(&lang_server.server_id()) + .unwrap_or_default() + })) } else { Ok(ProjectTransaction::default()) } @@ -2907,17 +2916,6 @@ impl Project { Ok(project_transaction) } - fn start_code_action(&mut self) { - // Set some state that will be read inside of `on_lsp_event` when handling a `WorkspaceEdit` - // event, and will cause the `ProjectTransaction` to be stored. - } - - fn finish_code_action(&mut self, cx: &mut ModelContext) -> ProjectTransaction { - // Retrieve all stored `ProjectTransactions` that have been received since `start_code_action` - // was called, and combine them together. - Default::default() - } - pub fn prepare_rename( &self, buffer: ModelHandle, @@ -6089,7 +6087,7 @@ mod tests { fs.insert_tree( "/dir", json!({ - "a.ts": "", + "a.ts": "a", }), ) .await; @@ -6116,7 +6114,7 @@ mod tests { let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); fake_server - .handle_request::(|params, _| async move { + .handle_request::(|_, _| async move { Ok(Some(vec![ lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { title: "The code action".into(), @@ -6144,12 +6142,51 @@ mod tests { |action, _| async move { Ok(action) }, ); fake_server - .handle_request::(move |params, cx| async move { - // fake_server.send(); - Ok(Some(json!(null))) + .handle_request::({ + let fake = fake_server.clone(); + move |params, _| { + assert_eq!(params.command, "_the/command"); + let fake = fake.clone(); + async move { + fake.server + .request::( + lsp::ApplyWorkspaceEditParams { + label: None, + edit: lsp::WorkspaceEdit { + changes: Some( + [( + lsp::Url::from_file_path("/dir/a.ts").unwrap(), + vec![lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "X".into(), + }], + )] + .into_iter() + .collect(), + ), + ..Default::default() + }, + }, + ) + .await + .unwrap(); + Ok(Some(json!(null))) + } + } }) .next() .await; + + let transaction = apply.await.unwrap(); + assert!(transaction.0.contains_key(&buffer)); + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "Xa"); + buffer.undo(cx); + assert_eq!(buffer.text(), "a"); + }); } #[gpui::test] From ba009724dd89b2f5ace6eb505da2d17aca0e1ed2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Apr 2022 11:59:21 -0700 Subject: [PATCH 19/24] Handle LSP apply workspace edit request fully before responding --- crates/editor/src/editor.rs | 2 + crates/project/src/project.rs | 366 ++++++++++++++++------------------ 2 files changed, 172 insertions(+), 196 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0dccce37a9e5461c113f79b89f3faa2cdcd04d8f..1135c9b260367a563add678c6a73a46b2d6d561e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2593,6 +2593,8 @@ impl Editor { } } } + } else { + return Ok(()); } let mut ranges_to_highlight = Vec::new(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4225f9657fbfcf351f1eb0d2efe1aa841278252a..a8311ccf3b144589925ab966fbeaef10cb36122e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -132,21 +132,6 @@ pub enum Event { CollaboratorLeft(PeerId), } -enum LanguageServerEvent { - WorkStart { - token: String, - }, - WorkProgress { - token: String, - progress: LanguageServerProgress, - }, - WorkEnd { - token: String, - }, - DiagnosticsUpdate(lsp::PublishDiagnosticsParams), - WorkspaceEdit(lsp::ApplyWorkspaceEditParams), -} - pub struct LanguageServerStatus { pub name: String, pub pending_work: BTreeMap, @@ -1330,17 +1315,30 @@ impl Project { ); cx.spawn_weak(|this, mut cx| async move { let language_server = language_server?.await.log_err()?; + let language_server = language_server + .initialize(adapter.initialization_options()) + .await + .log_err()?; let this = this.upgrade(&cx)?; - let (language_server_events_tx, language_server_events_rx) = - smol::channel::unbounded(); + let disk_based_diagnostics_progress_token = + adapter.disk_based_diagnostics_progress_token(); language_server .on_notification::({ - let language_server_events_tx = language_server_events_tx.clone(); - move |params, _| { - language_server_events_tx - .try_send(LanguageServerEvent::DiagnosticsUpdate(params)) - .ok(); + let this = this.downgrade(); + let adapter = adapter.clone(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_diagnostics_published( + server_id, + params, + &adapter, + disk_based_diagnostics_progress_token, + cx, + ); + }); + } } }) .detach(); @@ -1373,94 +1371,40 @@ impl Project { language_server .on_request::({ - let language_server_events_tx = language_server_events_tx.clone(); - move |params, _| { - language_server_events_tx - .try_send(LanguageServerEvent::WorkspaceEdit(params)) - .ok(); - async move { - Ok(lsp::ApplyWorkspaceEditResponse { - applied: true, - failed_change: None, - failure_reason: None, - }) - } + let this = this.downgrade(); + let adapter = adapter.clone(); + let language_server = language_server.clone(); + move |params, cx| { + Self::on_lsp_workspace_edit( + this, + params, + server_id, + adapter.clone(), + language_server.clone(), + cx, + ) } }) .detach(); language_server - .on_notification::(move |params, _| { - let token = match params.token { - lsp::NumberOrString::String(token) => token, - lsp::NumberOrString::Number(token) => { - log::info!("skipping numeric progress token {}", token); - return; + .on_notification::({ + let this = this.downgrade(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_progress( + params, + server_id, + disk_based_diagnostics_progress_token, + cx, + ); + }); } - }; - - match params.value { - lsp::ProgressParamsValue::WorkDone(progress) => match progress { - lsp::WorkDoneProgress::Begin(_) => { - language_server_events_tx - .try_send(LanguageServerEvent::WorkStart { token }) - .ok(); - } - lsp::WorkDoneProgress::Report(report) => { - language_server_events_tx - .try_send(LanguageServerEvent::WorkProgress { - token, - progress: LanguageServerProgress { - message: report.message, - percentage: report - .percentage - .map(|p| p as usize), - last_update_at: Instant::now(), - }, - }) - .ok(); - } - lsp::WorkDoneProgress::End(_) => { - language_server_events_tx - .try_send(LanguageServerEvent::WorkEnd { token }) - .ok(); - } - }, } }) .detach(); - let language_server = language_server - .initialize(adapter.initialization_options()) - .await - .log_err()?; - - // Process all the LSP events. - cx.spawn(|mut cx| { - let this = this.downgrade(); - let adapter = adapter.clone(); - let language_server = language_server.clone(); - async move { - while let Ok(event) = language_server_events_rx.recv().await { - let this = this.upgrade(&cx)?; - Self::on_lsp_event( - this, - server_id, - &adapter, - &language_server, - event, - &mut cx, - ) - .await; - - // Don't starve the main thread when lots of events arrive all at once. - smol::future::yield_now().await; - } - Some(()) - } - }) - .detach(); - this.update(&mut cx, |this, cx| { this.language_servers .insert(key.clone(), (adapter, language_server.clone())); @@ -1615,75 +1559,111 @@ impl Project { .detach(); } - async fn on_lsp_event( - this: ModelHandle, - language_server_id: usize, + fn on_lsp_diagnostics_published( + &mut self, + server_id: usize, + mut params: lsp::PublishDiagnosticsParams, adapter: &Arc, - language_server: &Arc, - event: LanguageServerEvent, - cx: &mut AsyncAppContext, + disk_based_diagnostics_progress_token: Option<&str>, + cx: &mut ModelContext, ) { - let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token(); - match event { - LanguageServerEvent::WorkStart { token } => { - this.update(cx, |this, cx| { - let language_server_status = if let Some(status) = - this.language_server_statuses.get_mut(&language_server_id) - { - status - } else { - return; - }; + adapter.process_diagnostics(&mut params); + if disk_based_diagnostics_progress_token.is_none() { + self.disk_based_diagnostics_started(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); + } + self.update_diagnostics(params, adapter.disk_based_diagnostic_sources(), cx) + .log_err(); + if disk_based_diagnostics_progress_token.is_none() { + self.disk_based_diagnostics_finished(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + } + } + + fn on_lsp_progress( + &mut self, + progress: lsp::ProgressParams, + server_id: usize, + disk_based_diagnostics_progress_token: Option<&str>, + cx: &mut ModelContext, + ) { + let token = match progress.token { + lsp::NumberOrString::String(token) => token, + lsp::NumberOrString::Number(token) => { + log::info!("skipping numeric progress token {}", token); + return; + } + }; + + match progress.value { + lsp::ProgressParamsValue::WorkDone(progress) => match progress { + lsp::WorkDoneProgress::Begin(_) => { + let language_server_status = + if let Some(status) = self.language_server_statuses.get_mut(&server_id) { + status + } else { + return; + }; if Some(token.as_str()) == disk_based_diagnostics_progress_token { language_server_status.pending_diagnostic_updates += 1; if language_server_status.pending_diagnostic_updates == 1 { - this.disk_based_diagnostics_started(cx); - this.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, - ), - ); + self.disk_based_diagnostics_started(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); } } else { - this.on_lsp_work_start(language_server_id, token.clone(), cx); - this.broadcast_language_server_update( - language_server_id, + self.on_lsp_work_start(server_id, token.clone(), cx); + self.broadcast_language_server_update( + server_id, proto::update_language_server::Variant::WorkStart( proto::LspWorkStart { token }, ), ); } - }); - } - LanguageServerEvent::WorkProgress { token, progress } => { - this.update(cx, |this, cx| { + } + lsp::WorkDoneProgress::Report(report) => { if Some(token.as_str()) != disk_based_diagnostics_progress_token { - this.on_lsp_work_progress( - language_server_id, + self.on_lsp_work_progress( + server_id, token.clone(), - progress.clone(), + LanguageServerProgress { + message: report.message.clone(), + percentage: report.percentage.map(|p| p as usize), + last_update_at: Instant::now(), + }, cx, ); - this.broadcast_language_server_update( - language_server_id, + self.broadcast_language_server_update( + server_id, proto::update_language_server::Variant::WorkProgress( proto::LspWorkProgress { token, - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), + message: report.message, + percentage: report.percentage.map(|p| p as u32), }, ), ); } - }); - } - LanguageServerEvent::WorkEnd { token } => { - this.update(cx, |this, cx| { + } + lsp::WorkDoneProgress::End(_) => { if Some(token.as_str()) == disk_based_diagnostics_progress_token { let language_server_status = if let Some(status) = - this.language_server_statuses.get_mut(&language_server_id) + self.language_server_statuses.get_mut(&server_id) { status } else { @@ -1692,69 +1672,25 @@ impl Project { language_server_status.pending_diagnostic_updates -= 1; if language_server_status.pending_diagnostic_updates == 0 { - this.disk_based_diagnostics_finished(cx); - this.broadcast_language_server_update( - language_server_id, + self.disk_based_diagnostics_finished(cx); + self.broadcast_language_server_update( + server_id, proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( proto::LspDiskBasedDiagnosticsUpdated {}, ), ); } } else { - this.on_lsp_work_end(language_server_id, token.clone(), cx); - this.broadcast_language_server_update( - language_server_id, + self.on_lsp_work_end(server_id, token.clone(), cx); + self.broadcast_language_server_update( + server_id, proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { token, }), ); } - }); - } - LanguageServerEvent::DiagnosticsUpdate(mut params) => { - this.update(cx, |this, cx| { - adapter.process_diagnostics(&mut params); - - if disk_based_diagnostics_progress_token.is_none() { - this.disk_based_diagnostics_started(cx); - this.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, - ), - ); - } - this.update_diagnostics(params, adapter.disk_based_diagnostic_sources(), cx) - .log_err(); - if disk_based_diagnostics_progress_token.is_none() { - this.disk_based_diagnostics_finished(cx); - this.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - ), - ); - } - }); - } - LanguageServerEvent::WorkspaceEdit(params) => { - let transaction = Self::deserialize_workspace_edit( - this.clone(), - params.edit, - true, - adapter.clone(), - language_server.clone(), - cx, - ) - .await - .log_err(); - this.update(cx, |this, _| { - if let Some(transaction) = transaction { - this.last_workspace_edits_by_language_server - .insert(language_server_id, transaction); - } - }); - } + } + }, } } @@ -1802,6 +1738,40 @@ impl Project { } } + async fn on_lsp_workspace_edit( + this: WeakModelHandle, + params: lsp::ApplyWorkspaceEditParams, + server_id: usize, + adapter: Arc, + language_server: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project project closed"))?; + let transaction = Self::deserialize_workspace_edit( + this.clone(), + params.edit, + true, + adapter.clone(), + language_server.clone(), + &mut cx, + ) + .await + .log_err(); + this.update(&mut cx, |this, _| { + if let Some(transaction) = transaction { + this.last_workspace_edits_by_language_server + .insert(server_id, transaction); + } + }); + Ok(lsp::ApplyWorkspaceEditResponse { + applied: true, + failed_change: None, + failure_reason: None, + }) + } + fn broadcast_language_server_update( &self, language_server_id: usize, @@ -2746,6 +2716,10 @@ impl Project { ) .await } else if let Some(command) = action.lsp_action.command { + this.update(&mut cx, |this, _| { + this.last_workspace_edits_by_language_server + .remove(&lang_server.server_id()); + }); lang_server .request::(lsp::ExecuteCommandParams { command: command.command, @@ -6071,7 +6045,7 @@ mod tests { } } - #[gpui::test] + #[gpui::test(iterations = 100)] async fn test_apply_code_action(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { From fe8e06e781b312b3a7771b2564f9c8723a9de3c1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Apr 2022 12:07:41 -0700 Subject: [PATCH 20/24] Fix clipping when using label-only completions --- crates/project/src/project.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a8311ccf3b144589925ab966fbeaef10cb36122e..d82629ab67eacb93be8940c703bd451e600a47a9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2373,13 +2373,6 @@ impl Project { }; cx.spawn(|_, cx| async move { - let clipped_position = source_buffer_handle - .read_with(&cx, |this, _| this.clip_point_utf16(position, Bias::Left)); - if clipped_position != position { - log::info!("Completion position out of date"); - return Ok(Default::default()); - } - let completions = lang_server .request::(lsp::CompletionParams { text_document_position: lsp::TextDocumentPositionParams::new( @@ -2412,10 +2405,21 @@ impl Project { Some(lsp::CompletionTextEdit::Edit(edit)) => { (range_from_lsp(edit.range), edit.new_text.clone()) } - None => ( - this.common_prefix_at(position, &lsp_completion.label), - lsp_completion.label.clone(), - ), + None => { + let clipped_position = + this.clip_point_utf16(position, Bias::Left); + if position != clipped_position { + log::info!("completion out of expected range"); + return None; + } + ( + this.common_prefix_at( + clipped_position, + &lsp_completion.label, + ), + lsp_completion.label.clone(), + ) + } Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { log::info!("unsupported insert/replace completion"); return None; From 6f28033efe8194699da795dfabd824d5c97a65a9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Apr 2022 13:00:06 -0700 Subject: [PATCH 21/24] Add explanatory comments in unit test for code actions w/ commands --- crates/project/src/project.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d82629ab67eacb93be8940c703bd451e600a47a9..30865b9d5afe118bf96283ff1c1b7ba1871e32e5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -6049,8 +6049,8 @@ mod tests { } } - #[gpui::test(iterations = 100)] - async fn test_apply_code_action(cx: &mut gpui::TestAppContext) { + #[gpui::test(iterations = 10)] + async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), @@ -6090,6 +6090,7 @@ mod tests { let fake_server = fake_language_servers.next().await.unwrap(); + // Language server returns code actions that contain commands, and not edits. let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); fake_server .handle_request::(|_, _| async move { @@ -6116,9 +6117,15 @@ mod tests { let apply = project.update(cx, |project, cx| { project.apply_code_action(buffer.clone(), action, true, cx) }); + + // Resolving the code action does not populate its edits. In absence of + // edits, we must execute the given command. fake_server.handle_request::( |action, _| async move { Ok(action) }, ); + + // While executing the command, the language server sends the editor + // a `workspaceEdit` request. fake_server .handle_request::({ let fake = fake_server.clone(); @@ -6158,6 +6165,8 @@ mod tests { .next() .await; + // Applying the code action returns a project transaction containing the edits + // sent by the language server in its `workspaceEdit` request. let transaction = apply.await.unwrap(); assert!(transaction.0.contains_key(&buffer)); buffer.update(cx, |buffer, cx| { From 5090e6f146de4bbd00f6a57f1d8f3fd37286e3eb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Apr 2022 14:49:36 -0700 Subject: [PATCH 22/24] Fix common_prefix_at panic when needle contains multibyte chars Also, make the prefix matching case-insensitive, since this is the typical behavior with autocomplete. --- crates/text/src/tests.rs | 50 ++++++++++++++++++++++++++++++++++------ crates/text/src/text.rs | 34 +++++++++++---------------- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 54e802b52194721484761e95507de9af516b0512..9348ff0ba6a26e6510840b39587b5558d92f297d 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -7,7 +7,6 @@ use std::{ iter::Iterator, time::{Duration, Instant}, }; -use util::test::marked_text_ranges; #[cfg(test)] #[ctor::ctor] @@ -167,14 +166,51 @@ fn test_line_len() { #[test] fn test_common_prefix_at_positionn() { - let (text, ranges) = marked_text_ranges("a = [bcd]"); + let text = "a = str; b = δα"; let buffer = Buffer::new(0, 0, History::new(text.into())); - let snapshot = &buffer.snapshot(); - let expected_range = ranges[0].to_offset(&snapshot); + + let offset1 = offset_after(text, "str"); + let offset2 = offset_after(text, "δα"); + + // the preceding word is a prefix of the suggestion + assert_eq!( + buffer.common_prefix_at(offset1, "string"), + range_of(text, "str"), + ); + // a suffix of the preceding word is a prefix of the suggestion + assert_eq!( + buffer.common_prefix_at(offset1, "tree"), + range_of(text, "tr"), + ); + // the preceding word is a substring of the suggestion, but not a prefix + assert_eq!( + buffer.common_prefix_at(offset1, "astro"), + empty_range_after(text, "str"), + ); + + // prefix matching is case insenstive. assert_eq!( - buffer.common_prefix_at(expected_range.end, "bcdef"), - expected_range - ) + buffer.common_prefix_at(offset1, "Strαngε"), + range_of(text, "str"), + ); + assert_eq!( + buffer.common_prefix_at(offset2, "ΔΑΜΝ"), + range_of(text, "δα"), + ); + + fn offset_after(text: &str, part: &str) -> usize { + text.find(part).unwrap() + part.len() + } + + fn empty_range_after(text: &str, part: &str) -> Range { + let offset = offset_after(text, part); + offset..offset + } + + fn range_of(text: &str, part: &str) -> Range { + let start = text.find(part).unwrap(); + start..start + part.len() + } } #[test] diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index ea0af9d21f5394409dbeda9d5d22f4964110ebf5..1c351079a7ddae872652707b106abea01c8f70fd 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1510,31 +1510,25 @@ impl BufferSnapshot { pub fn common_prefix_at(&self, position: T, needle: &str) -> Range where - T: Clone + ToOffset + FromAnchor, + T: ToOffset + TextDimension, { - let position_offset = position.to_offset(self); - // Get byte indices and char counts for every character in needle in reverse order - let char_indices = needle + let offset = position.to_offset(self); + let common_prefix_len = needle .char_indices() .map(|(index, _)| index) - .chain(std::iter::once(needle.len())) - .enumerate() - // Don't test any prefixes that are bigger than the requested position - .take_while(|(_, prefix_length)| *prefix_length <= position_offset); - - let start = char_indices - // Compute the prefix string and prefix start location - .map(move |(byte_position, char_length)| { - (position_offset - char_length, &needle[..byte_position]) + .chain([needle.len()]) + .take_while(|&len| len <= offset) + .filter(|&len| { + let left = self + .chars_for_range(offset - len..offset) + .flat_map(|c| char::to_lowercase(c)); + let right = needle[..len].chars().flat_map(|c| char::to_lowercase(c)); + left.eq(right) }) - // Only take strings when the prefix is contained at the expected prefix position - .filter(|(prefix_offset, prefix)| self.contains_str_at(prefix_offset, prefix)) - // Convert offset to T - .map(|(prefix_offset, _)| T::from_anchor(&self.anchor_before(prefix_offset), self)) .last() - // If no prefix matches, return the passed in position to create an empty range - .unwrap_or(position.clone()); - + .unwrap_or(0); + let start_offset = offset - common_prefix_len; + let start = self.text_summary_for_range(0..start_offset); start..position } From 7ad862673d748f669c2170b36df937a6c42bbf85 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Apr 2022 15:05:03 -0700 Subject: [PATCH 23/24] Add basic syntax highlight colors for typescript completions --- crates/zed/src/languages/typescript.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index d08da116a544478693ceeea3a397868b653613cd..aca8cdef52df87e5e1d566726f75901f834543c1 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -115,6 +115,29 @@ impl LspAdapter for TypeScriptLspAdapter { fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &language::Language, + ) -> Option { + use lsp::CompletionItemKind as Kind; + let len = item.label.len(); + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), + Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), + Kind::CONSTANT => grammar.highlight_id_for_name("constant"), + Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), + Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"), + _ => None, + }?; + Some(language::CodeLabel { + text: item.label.clone(), + runs: vec![(0..len, highlight_id)], + filter_range: 0..len, + }) + } + fn initialization_options(&self) -> Option { Some(json!({ "provideFormatter": true From c4d3bbf18448e3488eb75b03a23cf4cc3ad30220 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Apr 2022 15:17:30 -0700 Subject: [PATCH 24/24] Bump protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index cfe780d5118c764a829cf047d288bc1e7a0b590e..9ee18faae2543b93e194f0e3c2a4fbe9e6604a84 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -5,4 +5,4 @@ pub mod proto; pub use conn::Connection; pub use peer::*; -pub const PROTOCOL_VERSION: u32 = 12; +pub const PROTOCOL_VERSION: u32 = 13;