Detailed changes
@@ -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"
@@ -140,6 +140,7 @@ members = [
"extensions/snippets",
"extensions/svelte",
"extensions/terraform",
+ "extensions/test-extension",
"extensions/toml",
"extensions/uiua",
"extensions/vue",
@@ -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")]);
@@ -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]
@@ -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"
@@ -0,0 +1 @@
+../../LICENSE-APACHE
@@ -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.
@@ -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 <elliott.codes@gmail.com>"]
+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"
@@ -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
@@ -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
@@ -0,0 +1,3 @@
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent
@@ -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
@@ -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<String>,
+}
+
+impl TestExtension {
+ fn language_server_binary_path(
+ &mut self,
+ language_server_id: &LanguageServerId,
+ _worktree: &zed::Worktree,
+ ) -> Result<String> {
+ 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<zed::Command> {
+ 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<zed::CodeLabel> {
+ 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::<Vec<_>>()
+ .join(", ")
+}