diff --git a/.gitignore b/.gitignore index df0ec9c1..b7017fdc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock +!editors/zed/Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk @@ -20,6 +21,8 @@ tags generated.rs docs/public/vide-lab/ docs/public/schemas/ +editors/zed/extension.wasm +editors/zed/grammars/systemverilog/ .DS_Store .vscode/ diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index e70fcecd..7263dc62 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -69,6 +69,7 @@ export default defineConfig({ items: [ 'user-guide/installation', 'user-guide/vscode-installation', + 'user-guide/zed-installation', 'user-guide/neovim-installation', 'user-guide/emacs-installation', ], diff --git a/docs/src/content/docs/advanced-guide/user-configuration.mdx b/docs/src/content/docs/advanced-guide/user-configuration.mdx index 15f2ea0e..094a68a0 100644 --- a/docs/src/content/docs/advanced-guide/user-configuration.mdx +++ b/docs/src/content/docs/advanced-guide/user-configuration.mdx @@ -93,6 +93,33 @@ vim.lsp.enable('vide') 如果你的 `"vide"` 已经写成可执行文件的绝对路径,保留原来的路径即可。`t` 会发送成 JSON 里的 `true`。 +## Zed + +如果你使用 [Zed 安装](../../user-guide/zed-installation/),在 Zed settings 里把用户配置写到 `lsp.vide.initialization_options`: + +```json +{ + "lsp": { + "vide": { + "initialization_options": { + "diagnostics": { + "update": "onType" + }, + "inlayHints": { + "port": { + "connection": { + "enable": true + } + } + } + } + } + } +} +``` + +如果你还需要指定本地 `vide` 二进制,可以在同一个 `lsp.vide` 下加入 `binary.path`。用户配置只需要放在 `initialization_options` 里。 + ## 其他 LSP 编辑器 不同编辑器或插件的字段名可能叫 `initializationOptions`、`init_options` 或类似名字。它们的含义相同:启动语言服务器时,把一个 JSON 对象一起发给服务器。 @@ -130,7 +157,7 @@ https://vide.pascal-lab.net/schemas/v1/user-config.schema.json } ``` -直接写在 Neovim `init_options` 或 Emacs `:initializationOptions` 里时,通常不需要写 `$schema`。 +直接写在 Neovim `init_options`、Emacs `:initializationOptions` 或 Zed `initialization_options` 里时,通常不需要写 `$schema`。 ## 修改后让配置生效 diff --git a/docs/src/content/docs/en/advanced-guide/user-configuration.mdx b/docs/src/content/docs/en/advanced-guide/user-configuration.mdx index ced2d574..7c0a85bf 100644 --- a/docs/src/content/docs/en/advanced-guide/user-configuration.mdx +++ b/docs/src/content/docs/en/advanced-guide/user-configuration.mdx @@ -93,6 +93,33 @@ If you use the Eglot setup from [Emacs Installation](../../user-guide/emacs-inst If your `"vide"` entry already uses an absolute executable path, keep that path. `t` is sent as JSON `true`. +## Zed + +If you use [Zed Installation](../../user-guide/zed-installation/), put user configuration under `lsp.vide.initialization_options` in Zed settings: + +```json +{ + "lsp": { + "vide": { + "initialization_options": { + "diagnostics": { + "update": "onType" + }, + "inlayHints": { + "port": { + "connection": { + "enable": true + } + } + } + } + } + } +} +``` + +If you also need to point Zed at a local `vide` binary, add `binary.path` under the same `lsp.vide` object. User configuration only needs to live under `initialization_options`. + ## Other LSP Editors Different editors or plugins may call the field `initializationOptions`, `init_options`, or something similar. The meaning is the same: send a JSON object when the language server starts. @@ -130,7 +157,7 @@ If you keep user configuration in a standalone JSON file, you can add `$schema` } ``` -When writing options directly in Neovim `init_options` or Emacs `:initializationOptions`, you usually do not need `$schema`. +When writing options directly in Neovim `init_options`, Emacs `:initializationOptions`, or Zed `initialization_options`, you usually do not need `$schema`. ## Apply Changes diff --git a/docs/src/content/docs/en/user-guide/installation.mdx b/docs/src/content/docs/en/user-guide/installation.mdx index bfedb0c1..2081eb6b 100644 --- a/docs/src/content/docs/en/user-guide/installation.mdx +++ b/docs/src/content/docs/en/user-guide/installation.mdx @@ -5,7 +5,7 @@ description: Install Vide for your editor. import { CardGrid, LinkCard } from '@astrojs/starlight/components'; -Vide's core is a language server that implements the [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/), so it can in principle be adapted to more LSP-capable editors and tools. We still recommend VS Code first: the extension gives you one-step installation of the Vide language server, along with the most complete and easiest setup. For Neovim and Emacs users, we also provide detailed guides that show how to download the Vide language server binary first and then configure the editor's LSP client to start it. We will continue to expand support for more editors based on user needs. +Vide's core is a language server that implements the [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/), so it can in principle be adapted to more LSP-capable editors and tools. We still recommend VS Code first: the extension gives you one-step installation of the Vide language server, along with the most complete and easiest setup. The Zed extension can download the language server from GitHub Releases on first start. For Neovim and Emacs users, we also provide detailed guides that show how to download the Vide language server binary first and then configure the editor's LSP client to start it. We will continue to expand support for more editors based on user needs. + + " + ">=" + "%" + ">>" + "<<" + "|=" + "|=>" + "|->" + ">>>" + "<<<" + "->>" + "->" + "=>" + "*>" + ".*" + (unary_operator) + (inc_or_dec_operator) + (queue_dimension) +] @operator + +"#" @constructor + +[ + ";" + "::" + "," + "." + ":" +] @punctuation.delimiter + +(conditional_expression + [ + "?" + ":" + ] @keyword.conditional.ternary) + +[ + "[" + "]" + "(" + ")" + "{" + "}" + "'{" +] @punctuation.bracket + +[ + "or" + "and" + "not" + "nor" + "nand" + "xor" + "xnor" +] @keyword.operator + +[ + "input" + "output" + "inout" + "var" + "wire" + "reg" + "packed" + "signed" + "unsigned" + "assert" + "cover" + "assume" + "disable" + "automatic" + "static" + "rand" + (dpi_function_import_property) + (dpi_task_import_property) +] @keyword.modifier + +[ + "include" + "import" +] @keyword.import + +[ + (one_line_comment) + (block_comment) +] @comment + +[ + "@" + (cycle_delay_range) + (delay_control) + (cycle_delay) + (attribute_instance) +] @attribute + +(attribute_instance + (attr_spec + (simple_identifier) @property)) + +[ + (integral_number) + (unbased_unsized_literal) + (fixed_point_number) + (unsigned_number) +] @number + +[ + (net_type) + (data_type) + (time_unit) +] @type.builtin + +(list_of_variable_decl_assignments + (variable_decl_assignment + name: (simple_identifier) @variable)) + +(hierarchical_identifier + (simple_identifier) @variable) + +(tf_port_item + (simple_identifier) @variable) + +port_name: (simple_identifier) @variable + +(port + (simple_identifier) @variable) + +(list_of_port_identifiers + (simple_identifier) @variable) + +(net_lvalue + (simple_identifier) @variable) + +(sequence_port_item + (simple_identifier) @variable) + +(property_port_item + (simple_identifier) @variable) + +(net_decl_assignment + (simple_identifier) @variable) + +(hierarchical_identifier + (simple_identifier) + (simple_identifier) @variable.member) + +(select + (simple_identifier) @variable.member) + +(named_port_connection + port_name: (simple_identifier) @variable.member) + +(named_parameter_assignment + (simple_identifier) @variable.parameter) + +(parameter_declaration + (list_of_param_assignments + (param_assignment + (simple_identifier) @variable.parameter))) + +(local_parameter_declaration + (list_of_param_assignments + (param_assignment + (simple_identifier) @variable.parameter))) + +[ + (simulation_control_task) + (system_tf_identifier) + (severity_system_task) + (randomize_call) + (array_or_queue_method_name) + "new" +] @function.builtin + +(task_body_declaration + . + name: (simple_identifier) @function + (simple_identifier)? @label) + +(function_body_declaration + . + name: (simple_identifier) @function + (simple_identifier)? @label) + +(function_body_declaration + . + (data_type_or_void) + name: (simple_identifier) @function + (simple_identifier)? @label) + +(clocking_declaration + . + name: (simple_identifier) @constructor + (simple_identifier)? @label) + +(sequence_declaration + . + name: (simple_identifier) @constructor + (simple_identifier)? @label) + +(property_declaration + . + name: (simple_identifier) @constructor + (simple_identifier)? @label) + +(class_declaration + . + name: (simple_identifier) @constructor + (simple_identifier)? @label) + +(interface_class_declaration + . + name: (simple_identifier) @constructor + (simple_identifier)? @label) + +(covergroup_declaration + . + name: (simple_identifier) @constructor + (simple_identifier)? @label) + +(package_declaration + . + name: (simple_identifier) @constructor + (simple_identifier)? @label) + +(checker_declaration + . + name: (simple_identifier) @constructor + (simple_identifier)? @label) + +(interface_declaration + . + [ + (simple_identifier) @constructor + (interface_nonansi_header + (simple_identifier) @constructor) + (interface_ansi_header + (simple_identifier) @constructor) + ] + (simple_identifier)? @label) + +(module_declaration + . + [ + (simple_identifier) @constructor + (module_nonansi_header + (simple_identifier) @constructor) + (module_ansi_header + (simple_identifier) @constructor) + ] + (simple_identifier)? @label) + +(program_declaration + . + [ + (simple_identifier) @constructor + (program_nonansi_header + (simple_identifier) @constructor) + (program_ansi_header + (simple_identifier) @constructor) + ] + (simple_identifier)? @label) + +(method_call_body + name: (simple_identifier) @function.call) + +(tf_call + (hierarchical_identifier + (simple_identifier) @function.call)) + +(module_instantiation + instance_type: (simple_identifier) @constructor) + +(name_of_instance + instance_name: (simple_identifier) @module) + +(sequence_instance + (hierarchical_identifier + (simple_identifier) @module)) + +(udp_instantiation + (simple_identifier) @constructor) + +(bind_directive + (bind_target_scope + (simple_identifier) @constructor)) + +(bind_target_instance + (hierarchical_identifier + (simple_identifier) @module)) + +(concurrent_assertion_item + (simple_identifier) @label) + +(cover_point + name: (simple_identifier) @label) + +(cover_cross + name: (simple_identifier) @module) + +(list_of_cross_items + (simple_identifier) @constructor) + +(package_import_item + (simple_identifier) @constructor) + +(seq_block + (simple_identifier) @label) + +(statement + block_name: (simple_identifier) @label) + +(dpi_spec_string) @string + +c_name: (c_identifier) @function + +(dpi_import_export + name: (simple_identifier) @function) + +(class_type + (simple_identifier) @constructor) + +(class_type + (simple_identifier) + (simple_identifier) @type) + +(task_prototype + name: (simple_identifier) @function) + +(function_prototype + name: (simple_identifier) @function) + +(type_assignment + name: (simple_identifier) @type.definition) + +(interface_class_type + (simple_identifier) @type.definition) + +(package_scope + (simple_identifier) @constructor) + +(data_declaration + (type_declaration + type_name: (simple_identifier) @type.definition)) + +(net_declaration + (simple_identifier) @type) + +(constraint_declaration + (simple_identifier) @constructor) + +(method_call + (primary + (hierarchical_identifier + (simple_identifier) @constructor))) + +(string_literal + (quoted_string) @string) + +(include_statement + (file_path_spec) @string) + +[ + "`define" + "`default_nettype" + (resetall_compiler_directive) + (include_compiler_directive) + "`timescale" + "`undef" + (undefineall_compiler_directive) + "`ifdef" + "`ifndef" + "`elsif" + "`endif" + "`else" +] @keyword.directive.define + +(include_compiler_directive + (quoted_string) @string) + +(include_compiler_directive + (system_lib_string) @string) + +(default_nettype_compiler_directive + (default_nettype_value) @type.builtin) + +(text_macro_definition + (text_macro_name + (simple_identifier) @keyword.directive)) + +(text_macro_usage) @keyword.directive + +(ifdef_condition + (simple_identifier) @keyword.directive) + +(undefine_compiler_directive + (simple_identifier) @keyword.directive) diff --git a/editors/zed/languages/systemverilog/indents.scm b/editors/zed/languages/systemverilog/indents.scm new file mode 100644 index 00000000..c8b919f7 --- /dev/null +++ b/editors/zed/languages/systemverilog/indents.scm @@ -0,0 +1,3 @@ +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent +(_ "begin" "end" @end) @indent diff --git a/editors/zed/languages/systemverilog/injections.scm b/editors/zed/languages/systemverilog/injections.scm new file mode 100644 index 00000000..a1c43ec6 --- /dev/null +++ b/editors/zed/languages/systemverilog/injections.scm @@ -0,0 +1,8 @@ +((one_line_comment) @injection.content + (#set! injection.language "comment")) + +((block_comment) @injection.content + (#set! injection.language "comment")) + +((macro_text) @injection.content + (#set! injection.language "SystemVerilog")) diff --git a/editors/zed/languages/systemverilog/outline.scm b/editors/zed/languages/systemverilog/outline.scm new file mode 100644 index 00000000..7390b069 --- /dev/null +++ b/editors/zed/languages/systemverilog/outline.scm @@ -0,0 +1,62 @@ +; Modules +(module_declaration + (module_ansi_header + (module_keyword) @context + name: (_) @name)) @item + +; Always blocks +(always_construct + (always_keyword) @context + (statement + (statement_item + (seq_block (simple_identifier) @name)? + (procedural_timing_control_statement + (statement_or_null + (statement + (statement_item (seq_block (simple_identifier) @name)))))?) + )) @item + +; Module instances +(module_instantiation + instance_type: (_) @context + (hierarchical_instance + (name_of_instance + instance_name: (_) @name))) @item + +; Typedefs +(type_declaration + "typedef" @context + type_name: (_) @name) @item + +; Classes +(class_declaration + "class" @context + name: (_) @name +) @item + +; Functions and tasks +( + "extern"? @context + (method_qualifier)? @context + [ + (function_declaration + "function" @context + (lifetime)? @context + (function_body_declaration + (data_type_or_void) @context + (class_scope)? @name + name: (_) @name) + ) + (function_prototype + "function" @context + (data_type_or_void) @context + name: (_) @name) + (task_prototype + "task" @context + name: (_) @name) + (task_declaration + "task" @context + (task_body_declaration + (class_scope)? @name + name: (_) @name)) + ]) @item diff --git a/editors/zed/src/lib.rs b/editors/zed/src/lib.rs new file mode 100644 index 00000000..bfff848d --- /dev/null +++ b/editors/zed/src/lib.rs @@ -0,0 +1,198 @@ +use std::{fs, path::Path}; + +use zed::settings::LspSettings; +use zed_extension_api::{ + self as zed, Architecture, Command, DownloadedFileType, GithubReleaseOptions, LanguageServerId, + LanguageServerInstallationStatus, Os, Result, Worktree, serde_json, +}; + +const LANGUAGE_SERVER_ID: &str = "vide"; +const RELEASE_REPO: &str = "pascal-lab/vide"; + +struct VideBinary { + path: String, + args: Vec, +} + +#[derive(Default)] +struct VideExtension { + cached_binary_path: Option, +} + +impl VideExtension { + fn language_server_binary( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> Result { + let mut args = Vec::new(); + if let Ok(settings) = LspSettings::for_worktree(language_server_id.as_ref(), worktree) { + if let Some(binary) = settings.binary { + args = binary.arguments.unwrap_or_default(); + if let Some(path) = binary.path { + return Ok(VideBinary { path, args }); + } + } + } + + let path = worktree + .which(binary_name(current_os())) + .unwrap_or(self.zed_managed_binary_path(language_server_id)?); + + Ok(VideBinary { path, args }) + } + + fn zed_managed_binary_path(&mut self, language_server_id: &LanguageServerId) -> Result { + if let Some(path) = &self.cached_binary_path { + if is_file(path) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + + let release = zed::latest_github_release( + RELEASE_REPO, + GithubReleaseOptions { require_assets: true, pre_release: false }, + )?; + + let (os, arch) = zed::current_platform(); + let asset_name = release_asset_name(os, arch)?; + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no Vide release asset found matching {asset_name:?}"))?; + + let version_dir = format!("vide-{}", release.version); + let binary_path = binary_path(&version_dir, os); + + if !is_file(&binary_path) { + zed::set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + + prepare_version_dir(&version_dir)?; + zed::download_file(&asset.download_url, &binary_path, DownloadedFileType::Uncompressed) + .map_err(|error| format!("failed to download {asset_name}: {error}"))?; + ensure_executable(&binary_path, os)?; + + prune_old_downloads(&version_dir)?; + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } +} + +impl zed::Extension for VideExtension { + fn new() -> Self { + Self::default() + } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> Result { + if language_server_id.as_ref() != LANGUAGE_SERVER_ID { + return Err(format!("unknown language server `{language_server_id}`")); + } + + let binary = self.language_server_binary(language_server_id, worktree)?; + Ok(Command { command: binary.path, args: binary.args, env: Vec::new() }) + } + + fn language_server_initialization_options( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> Result> { + let initialization_options = + LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .ok() + .and_then(|settings| settings.initialization_options.clone()) + .unwrap_or_default(); + + Ok(Some(initialization_options)) + } + + fn language_server_workspace_configuration( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> Result> { + let settings = LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .ok() + .and_then(|settings| settings.settings.clone()) + .unwrap_or_default(); + + Ok(Some(settings)) + } +} + +fn current_os() -> Os { + let (os, _) = zed::current_platform(); + os +} + +fn binary_name(os: Os) -> &'static str { + match os { + Os::Windows => "vide.exe", + Os::Mac | Os::Linux => "vide", + } +} + +fn release_asset_name(os: Os, arch: Architecture) -> Result { + match (os, arch) { + (Os::Linux, Architecture::X8664) => Ok("vide-linux-x64".to_string()), + (Os::Linux, Architecture::Aarch64) => Ok("vide-linux-arm64".to_string()), + (Os::Mac, Architecture::Aarch64) => Ok("vide-darwin-arm64".to_string()), + (Os::Windows, Architecture::X8664) => Ok("vide-win32-x64.exe".to_string()), + (os, arch) => Err(format!("Vide does not publish a release asset for {os:?}-{arch:?}")), + } +} + +fn binary_path(version_dir: &str, os: Os) -> String { + format!("{version_dir}/{}", binary_name(os)) +} + +fn is_file(path: &str) -> bool { + fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) +} + +fn prepare_version_dir(version_dir: &str) -> Result<()> { + if fs::metadata(version_dir).is_ok_and(|metadata| metadata.is_file()) { + fs::remove_file(version_dir) + .map_err(|error| format!("failed to remove old Vide binary {version_dir}: {error}"))?; + } + + fs::create_dir_all(version_dir) + .map_err(|error| format!("failed to create Vide binary directory {version_dir}: {error}")) +} + +fn ensure_executable(path: &str, os: Os) -> Result<()> { + if os != Os::Windows && Path::new(path).exists() { + zed::make_file_executable(path)?; + } + Ok(()) +} + +fn prune_old_downloads(current_dir: &str) -> Result<()> { + let entries = + fs::read_dir(".").map_err(|error| format!("failed to list extension data: {error}"))?; + for entry in entries { + let entry = + entry.map_err(|error| format!("failed to read extension data entry: {error}"))?; + if entry.file_name().to_str() != Some(current_dir) { + fs::remove_dir_all(entry.path()).ok(); + } + } + Ok(()) +} + +zed::register_extension!(VideExtension);