From 089cc85d4a09914808bde0849ee6ca6e7d11033a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 3 Jul 2024 11:10:51 -0400 Subject: [PATCH] Use a dedicated test extension in extension tests (#13781) This PR updates the `extension` crate's tests to use a dedicated test extension for its tests instead of the real Gleam extension. As the Gleam extension continues to evolve, it makes it less suitable to use as a test fixture: 1. For a while now, the test has failed locally due to me having `gleam` on my $PATH, which causes the extension's `get_language_server_command` to go down a separate codepath. 2. With the addition of the `indexed_docs_providers` the test was hanging indefinitely. While these problems are likely solvable, it seems reasonable to have a dedicated extension to use as a test fixture. That way we can do whatever we need to exercise our test criteria. The `test-extension` is a fork of the Gleam extension with some additional functionality removed. Release Notes: - N/A --- Cargo.lock | 7 + Cargo.toml | 1 + crates/extension/src/extension_store_test.rs | 13 +- extensions/gleam/extension.toml | 2 +- extensions/test-extension/Cargo.toml | 16 ++ extensions/test-extension/LICENSE-APACHE | 1 + extensions/test-extension/README.md | 5 + extensions/test-extension/extension.toml | 15 ++ .../languages/gleam/config.toml | 12 ++ .../languages/gleam/highlights.scm | 130 ++++++++++++++ .../languages/gleam/indents.scm | 3 + .../languages/gleam/outline.scm | 31 ++++ .../test-extension/src/test_extension.rs | 160 ++++++++++++++++++ 13 files changed, 390 insertions(+), 6 deletions(-) create mode 100644 extensions/test-extension/Cargo.toml create mode 120000 extensions/test-extension/LICENSE-APACHE create mode 100644 extensions/test-extension/README.md create mode 100644 extensions/test-extension/extension.toml create mode 100644 extensions/test-extension/languages/gleam/config.toml create mode 100644 extensions/test-extension/languages/gleam/highlights.scm create mode 100644 extensions/test-extension/languages/gleam/indents.scm create mode 100644 extensions/test-extension/languages/gleam/outline.scm create mode 100644 extensions/test-extension/src/test_extension.rs diff --git a/Cargo.lock b/Cargo.lock index 14c6edd672..3a5b676102 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13896,6 +13896,13 @@ dependencies = [ "zed_extension_api 0.0.6", ] +[[package]] +name = "zed_test_extension" +version = "0.1.0" +dependencies = [ + "zed_extension_api 0.0.6", +] + [[package]] name = "zed_toml" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index c9fe41f772..5dfcb9d1e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -140,6 +140,7 @@ members = [ "extensions/snippets", "extensions/svelte", "extensions/terraform", + "extensions/test-extension", "extensions/toml", "extensions/uiua", "extensions/vue", diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index d956d88d64..f2337aa6b9 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -446,7 +446,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { +async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); @@ -456,7 +456,8 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { .parent() .unwrap(); let cache_dir = root_dir.join("target"); - let gleam_extension_dir = root_dir.join("extensions").join("gleam"); + let test_extension_id = "test-extension"; + let test_extension_dir = root_dir.join("extensions").join(test_extension_id); let fs = Arc::new(RealFs::default()); let extensions_dir = temp_tree(json!({ @@ -596,7 +597,7 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { extension_store .update(cx, |store, cx| { - store.install_dev_extension(gleam_extension_dir.clone(), cx) + store.install_dev_extension(test_extension_dir.clone(), cx) }) .await .unwrap(); @@ -611,7 +612,8 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { .unwrap(); let fake_server = fake_servers.next().await.unwrap(); - let expected_server_path = extensions_dir.join("work/gleam/gleam-v1.2.3/gleam"); + let expected_server_path = + extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam")); let expected_binary_contents = language_server_version.lock().binary_contents.clone(); assert_eq!(fake_server.binary.path, expected_server_path); @@ -725,7 +727,8 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { // The extension re-fetches the latest version of the language server. let fake_server = fake_servers.next().await.unwrap(); - let new_expected_server_path = extensions_dir.join("work/gleam/gleam-v2.0.0/gleam"); + let new_expected_server_path = + extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam")); let expected_binary_contents = language_server_version.lock().binary_contents.clone(); assert_eq!(fake_server.binary.path, new_expected_server_path); assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]); diff --git a/extensions/gleam/extension.toml b/extensions/gleam/extension.toml index 6d76de2967..089bdea4e6 100644 --- a/extensions/gleam/extension.toml +++ b/extensions/gleam/extension.toml @@ -24,4 +24,4 @@ description = "Returns Gleam docs." requires_argument = true tooltip_text = "Insert Gleam docs" -# [indexed_docs_providers.gleam-hexdocs] +[indexed_docs_providers.gleam-hexdocs] diff --git a/extensions/test-extension/Cargo.toml b/extensions/test-extension/Cargo.toml new file mode 100644 index 0000000000..02fa351c28 --- /dev/null +++ b/extensions/test-extension/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_test_extension" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/test_extension.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.0.6" diff --git a/extensions/test-extension/LICENSE-APACHE b/extensions/test-extension/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/test-extension/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/test-extension/README.md b/extensions/test-extension/README.md new file mode 100644 index 0000000000..5941f23ec4 --- /dev/null +++ b/extensions/test-extension/README.md @@ -0,0 +1,5 @@ +# Test Extension + +This is a test extension that we use in the tests for the `extension` crate. + +Originally based off the Gleam extension. diff --git a/extensions/test-extension/extension.toml b/extensions/test-extension/extension.toml new file mode 100644 index 0000000000..6ac0a38731 --- /dev/null +++ b/extensions/test-extension/extension.toml @@ -0,0 +1,15 @@ +id = "test-extension" +name = "Test Extension" +description = "An extension for use in tests." +version = "0.1.0" +schema_version = 1 +authors = ["Marshall Bowers "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.gleam] +name = "Gleam LSP" +language = "Gleam" + +[grammars.gleam] +repository = "https://github.com/gleam-lang/tree-sitter-gleam" +commit = "8432ffe32ccd360534837256747beb5b1c82fca1" diff --git a/extensions/test-extension/languages/gleam/config.toml b/extensions/test-extension/languages/gleam/config.toml new file mode 100644 index 0000000000..51874945e2 --- /dev/null +++ b/extensions/test-extension/languages/gleam/config.toml @@ -0,0 +1,12 @@ +name = "Gleam" +grammar = "gleam" +path_suffixes = ["gleam"] +line_comments = ["// ", "/// "] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, +] +tab_size = 2 diff --git a/extensions/test-extension/languages/gleam/highlights.scm b/extensions/test-extension/languages/gleam/highlights.scm new file mode 100644 index 0000000000..4b85b88d01 --- /dev/null +++ b/extensions/test-extension/languages/gleam/highlights.scm @@ -0,0 +1,130 @@ +; Comments +(module_comment) @comment +(statement_comment) @comment +(comment) @comment + +; Constants +(constant + name: (identifier) @constant) + +; Variables +(identifier) @variable +(discard) @comment.unused + +; Modules +(module) @module +(import alias: (identifier) @module) +(remote_type_identifier + module: (identifier) @module) +(remote_constructor_name + module: (identifier) @module) +((field_access + record: (identifier) @module + field: (label) @function) + (#is-not? local)) + +; Functions +(unqualified_import (identifier) @function) +(unqualified_import "type" (type_identifier) @type) +(unqualified_import (type_identifier) @constructor) +(function + name: (identifier) @function) +(external_function + name: (identifier) @function) +(function_parameter + name: (identifier) @variable.parameter) +((function_call + function: (identifier) @function) + (#is-not? local)) +((binary_expression + operator: "|>" + right: (identifier) @function) + (#is-not? local)) + +; "Properties" +; Assumed to be intended to refer to a name for a field; something that comes +; before ":" or after "." +; e.g. record field names, tuple indices, names for named arguments, etc +(label) @property +(tuple_access + index: (integer) @property) + +; Attributes +(attribute + "@" @attribute + name: (identifier) @attribute) + +(attribute_value (identifier) @constant) + +; Type names +(remote_type_identifier) @type +(type_identifier) @type + +; Data constructors +(constructor_name) @constructor + +; Literals +(string) @string +((escape_sequence) @warning + ; Deprecated in v0.33.0-rc2: + (#eq? @warning "\\e")) +(escape_sequence) @string.escape +(bit_string_segment_option) @function.builtin +(integer) @number +(float) @number + +; Reserved identifiers +; TODO: when tree-sitter supports `#any-of?` in the Rust bindings, +; refactor this to use `#any-of?` rather than `#match?` +((identifier) @warning + (#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$")) + +; Keywords +[ + (visibility_modifier) ; "pub" + (opacity_modifier) ; "opaque" + "as" + "assert" + "case" + "const" + ; DEPRECATED: 'external' was removed in v0.30. + "external" + "fn" + "if" + "import" + "let" + "panic" + "todo" + "type" + "use" +] @keyword + +; Operators +(binary_expression + operator: _ @operator) +(boolean_negation "!" @operator) +(integer_negation "-" @operator) + +; Punctuation +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<<" + ">>" +] @punctuation.bracket +[ + "." + "," + ;; Controversial -- maybe some are operators? + ":" + "#" + "=" + "->" + ".." + "-" + "<-" +] @punctuation.delimiter diff --git a/extensions/test-extension/languages/gleam/indents.scm b/extensions/test-extension/languages/gleam/indents.scm new file mode 100644 index 0000000000..112b414aa4 --- /dev/null +++ b/extensions/test-extension/languages/gleam/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/extensions/test-extension/languages/gleam/outline.scm b/extensions/test-extension/languages/gleam/outline.scm new file mode 100644 index 0000000000..5df7a6af80 --- /dev/null +++ b/extensions/test-extension/languages/gleam/outline.scm @@ -0,0 +1,31 @@ +(external_type + (visibility_modifier)? @context + "type" @context + (type_name) @name) @item + +(type_definition + (visibility_modifier)? @context + (opacity_modifier)? @context + "type" @context + (type_name) @name) @item + +(data_constructor + (constructor_name) @name) @item + +(data_constructor_argument + (label) @name) @item + +(type_alias + (visibility_modifier)? @context + "type" @context + (type_name) @name) @item + +(function + (visibility_modifier)? @context + "fn" @context + name: (_) @name) @item + +(constant + (visibility_modifier)? @context + "const" @context + name: (_) @name) @item diff --git a/extensions/test-extension/src/test_extension.rs b/extensions/test-extension/src/test_extension.rs new file mode 100644 index 0000000000..bbdb587782 --- /dev/null +++ b/extensions/test-extension/src/test_extension.rs @@ -0,0 +1,160 @@ +use std::fs; +use zed::lsp::CompletionKind; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +struct TestExtension { + cached_binary_path: Option, +} + +impl TestExtension { + fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "gleam-lang/gleam", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + let asset_name = format!( + "gleam-{version}-{arch}-{os}.tar.gz", + version = release.version, + arch = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X86 => "x86", + zed::Architecture::X8664 => "x86_64", + }, + os = match platform { + zed::Os::Mac => "apple-darwin", + zed::Os::Linux => "unknown-linux-musl", + zed::Os::Windows => "pc-windows-msvc", + }, + ); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("gleam-{}", release.version); + let binary_path = format!("{version_dir}/gleam"); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &version_dir, + zed::DownloadedFileType::GzipTar, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(&entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } +} + +impl zed::Extension for TestExtension { + fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + Ok(zed::Command { + command: self.language_server_binary_path(language_server_id, worktree)?, + args: vec!["lsp".to_string()], + env: Default::default(), + }) + } + + fn label_for_completion( + &self, + _language_server_id: &LanguageServerId, + completion: zed::lsp::Completion, + ) -> Option { + let name = &completion.label; + let ty = strip_newlines_from_detail(&completion.detail?); + let let_binding = "let a"; + let colon = ": "; + let assignment = " = "; + let call = match completion.kind? { + CompletionKind::Function | CompletionKind::Constructor => "()", + _ => "", + }; + let code = format!("{let_binding}{colon}{ty}{assignment}{name}{call}"); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::code_range({ + let start = let_binding.len() + colon.len() + ty.len() + assignment.len(); + start..start + name.len() + }), + CodeLabelSpan::code_range({ + let start = let_binding.len(); + start..start + colon.len() + }), + CodeLabelSpan::code_range({ + let start = let_binding.len() + colon.len(); + start..start + ty.len() + }), + ], + filter_range: (0..name.len()).into(), + code, + }) + } +} + +zed::register_extension!(TestExtension); + +/// Removes newlines from the completion detail. +/// +/// The Gleam LSP can return types containing newlines, which causes formatting +/// issues within the Zed completions menu. +fn strip_newlines_from_detail(detail: &str) -> String { + let without_newlines = detail + .replace("->\n ", "-> ") + .replace("\n ", "") + .replace(",\n", ""); + + let comma_delimited_parts = without_newlines.split(','); + comma_delimited_parts + .map(|part| part.trim()) + .collect::>() + .join(", ") +}