diff --git a/Cargo.lock b/Cargo.lock index 85991f82124bfc095abf12ada32d52a6d8ba3625..a89e343e1d771528d1393c7070cf20d14a9bfa7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6060,8 +6060,8 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.94.1" -source = "git+https://github.com/zed-industries/lsp-types?branch=updated-completion-list-item-defaults#90a040a1d195687bd19e1df47463320a44e93d7a" +version = "0.95.1" +source = "git+https://github.com/zed-industries/lsp-types?branch=apply-snippet-edit#853c7881d200777e20799026651ca36727144646" dependencies = [ "bitflags 1.3.2", "serde", @@ -7683,6 +7683,7 @@ dependencies = [ "sha2 0.10.7", "similar", "smol", + "snippet", "task", "terminal", "text", @@ -13200,9 +13201,9 @@ dependencies = [ [[package]] name = "zed_html" -version = "0.0.1" +version = "0.0.2" dependencies = [ - "zed_extension_api 0.0.4", + "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c40807e668c5510fecedea30d27c45595945ca86..26d53c66e090cd48462d9963472e467bd5aa5551 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1587,7 +1587,21 @@ impl Editor { project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { if let project::Event::RefreshInlayHints = event { editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); - }; + } else if let project::Event::SnippetEdit(id, snippet_edits) = event { + if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { + let focus_handle = editor.focus_handle(cx); + if focus_handle.is_focused(cx) { + let snapshot = buffer.read(cx).snapshot(); + for (range, snippet) in snippet_edits { + let editor_range = + language::range_from_lsp(*range).to_offset(&snapshot); + editor + .insert_snippet(&[editor_range], snippet.clone(), cx) + .ok(); + } + } + } + } })); let task_inventory = project.read(cx).task_inventory().clone(); project_subscriptions.push(cx.observe(&task_inventory, |editor, _, cx| { @@ -1601,7 +1615,6 @@ impl Editor { &buffer.read(cx).snapshot(cx), cx, ); - let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, Self::handle_focus).detach(); cx.on_blur(&focus_handle, Self::handle_blur).detach(); @@ -10728,7 +10741,6 @@ impl Editor { fn handle_focus(&mut self, cx: &mut ViewContext) { cx.emit(EditorEvent::Focused); - if let Some(rename) = self.pending_rename.as_ref() { let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone(); cx.focus(&rename_editor_focus_handle); diff --git a/crates/languages/src/javascript/config.toml b/crates/languages/src/javascript/config.toml index 32e08c1cd93e99093071ebc4e6e1a8ced313e949..66d3bcdaa0a1b28629a903efa7290dc460d70e50 100644 --- a/crates/languages/src/javascript/config.toml +++ b/crates/languages/src/javascript/config.toml @@ -16,12 +16,12 @@ brackets = [ ] word_characters = ["$", "#"] tab_size = 2 -scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"] +scope_opt_in_language_servers = ["tailwindcss-language-server","vscode-html-language-server", "emmet-language-server"] [overrides.element] line_comments = { remove = true } block_comment = ["{/* ", " */}"] -opt_into_language_servers = ["emmet-language-server"] +opt_into_language_servers = ["emmet-language-server", "vscode-html-language-server"] [overrides.string] word_characters = ["-"] diff --git a/crates/languages/src/tsx/config.toml b/crates/languages/src/tsx/config.toml index f7c8b80a9089faac58cff4419dc692816dfc48f1..255b611d25bd196fc5885b2f2b786b5b69c5b7b6 100644 --- a/crates/languages/src/tsx/config.toml +++ b/crates/languages/src/tsx/config.toml @@ -14,13 +14,13 @@ brackets = [ { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, ] word_characters = ["#", "$"] -scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"] +scope_opt_in_language_servers = ["vscode-html-language-server", "tailwindcss-language-server", "emmet-language-server"] tab_size = 2 [overrides.element] line_comments = { remove = true } block_comment = ["{/* ", " */}"] -opt_into_language_servers = ["emmet-language-server"] +opt_into_language_servers = ["vscode-html-language-server", "emmet-language-server"] [overrides.string] word_characters = ["-"] diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index e685ddf5a26dcea5f761ba39060edf8f913f4362..f28c54634ef7ca9e6dc07aa6b02e2ecaada9e5c1 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -22,7 +22,7 @@ collections.workspace = true futures.workspace = true gpui.workspace = true log.workspace = true -lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "updated-completion-list-item-defaults" } +lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "apply-snippet-edit" } parking_lot.workspace = true postage.workspace = true serde.workspace = true diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 7981922d3dd97d65120ac06f96fb7f906d423ff2..ce90a5c0683afbc6d3598fafc1e5299ce507dbfe 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -601,6 +601,7 @@ impl LanguageServer { ResourceOperationKind::Delete, ]), document_changes: Some(true), + snippet_edit_support: Some(true), ..WorkspaceEditClientCapabilities::default() }), ..Default::default() @@ -712,6 +713,7 @@ impl LanguageServer { } }), locale: None, + ..Default::default() }; cx.spawn(|_| async move { diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index f523e537e865251f2b00ebddec92066d8d0e0be6..3e8b7c0c0b350fb5f22cf8e9cb7f2da589470578 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -58,6 +58,7 @@ settings.workspace = true sha2.workspace = true similar = "1.3" smol.workspace = true +snippet.workspace = true terminal.workspace = true text.workspace = true util.workspace = true diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ed5ef9d067cca9d0c5792be536fc2e9d0e9bc823..9dd27b1a3191845b861e91b40c5d3eda31183e5d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -55,9 +55,9 @@ use language::{ use log::error; use lsp::{ DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, - DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, + DocumentHighlightKind, Edit, LanguageServer, LanguageServerBinary, LanguageServerId, LspRequestFuture, MessageActionItem, OneOf, ServerCapabilities, ServerHealthStatus, - ServerStatus, + ServerStatus, TextEdit, }; use lsp_command::*; use node_runtime::NodeRuntime; @@ -67,6 +67,7 @@ use prettier_support::{DefaultPrettier, PrettierInstance}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search_history::SearchHistory; +use snippet::Snippet; use worktree::LocalSnapshot; use http::{HttpClient, Url}; @@ -332,6 +333,7 @@ pub enum Event { CollaboratorLeft(proto::PeerId), RefreshInlayHints, RevealInProjectPanel(ProjectEntryId), + SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>), } pub enum LanguageServerState { @@ -2694,7 +2696,6 @@ impl Project { }; let next_version = previous_snapshot.version + 1; - buffer_snapshots.push(LspBufferSnapshot { version: next_version, snapshot: next_snapshot.clone(), @@ -6209,7 +6210,7 @@ impl Project { uri, version: None, }, - edits: edits.into_iter().map(OneOf::Left).collect(), + edits: edits.into_iter().map(Edit::Plain).collect(), }) })); } @@ -6287,7 +6288,7 @@ impl Project { let buffer_to_edit = this .update(cx, |this, cx| { this.open_local_buffer_via_lsp( - op.text_document.uri, + op.text_document.uri.clone(), language_server.server_id(), lsp_adapter.name.clone(), cx, @@ -6297,10 +6298,68 @@ impl Project { let edits = this .update(cx, |this, cx| { - let edits = op.edits.into_iter().map(|edit| match edit { - OneOf::Left(edit) => edit, - OneOf::Right(edit) => edit.text_edit, + let path = buffer_to_edit.read(cx).project_path(cx); + let active_entry = this.active_entry; + let is_active_entry = path.clone().map_or(false, |project_path| { + this.entry_for_path(&project_path, cx) + .map_or(false, |entry| Some(entry.id) == active_entry) }); + + let (mut edits, mut snippet_edits) = (vec![], vec![]); + for edit in op.edits { + match edit { + Edit::Plain(edit) => edits.push(edit), + Edit::Annotated(edit) => edits.push(edit.text_edit), + Edit::Snippet(edit) => { + let Ok(snippet) = Snippet::parse(&edit.snippet.value) + else { + continue; + }; + + if is_active_entry { + snippet_edits.push((edit.range, snippet)); + } else { + // Since this buffer is not focused, apply a normal edit. + edits.push(TextEdit { + range: edit.range, + new_text: snippet.text, + }); + } + } + } + } + if !snippet_edits.is_empty() { + if let Some(buffer_version) = op.text_document.version { + let buffer_id = buffer_to_edit.read(cx).remote_id(); + // Check if the edit that triggered that edit has been made by this participant. + let should_apply_edit = this + .buffer_snapshots + .get(&buffer_id) + .and_then(|server_to_snapshots| { + let all_snapshots = server_to_snapshots + .get(&language_server.server_id())?; + all_snapshots + .binary_search_by_key(&buffer_version, |snapshot| { + snapshot.version + }) + .ok() + .and_then(|index| all_snapshots.get(index)) + }) + .map_or(false, |lsp_snapshot| { + let version = lsp_snapshot.snapshot.version(); + let most_recent_edit = version + .iter() + .max_by_key(|timestamp| timestamp.value); + most_recent_edit.map_or(false, |edit| { + edit.replica_id == this.replica_id() + }) + }); + if should_apply_edit { + cx.emit(Event::SnippetEdit(buffer_id, snippet_edits)); + } + } + } + this.edits_from_lsp( &buffer_to_edit, edits, diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 2957f415daa80a9e06570dc977324faaed8addb2..7d627f683306fcc7cbfa22b3f078773207c1a7d1 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Context, Result}; use smallvec::SmallVec; use std::{collections::BTreeMap, ops::Range}; -#[derive(Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Snippet { pub text: String, pub tabstops: Vec, diff --git a/extensions/html/Cargo.toml b/extensions/html/Cargo.toml index f3b7a80b31aeeb80d70e9e6dd93d18abac467cb7..f992647d57fccd140ff23ed03da4a8e3a2f0aaaa 100644 --- a/extensions/html/Cargo.toml +++ b/extensions/html/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_html" -version = "0.0.1" +version = "0.0.2" edition = "2021" publish = false license = "Apache-2.0" @@ -13,4 +13,4 @@ path = "src/html.rs" crate-type = ["cdylib"] [dependencies] -zed_extension_api = "0.0.4" +zed_extension_api = "0.0.6" diff --git a/extensions/html/extension.toml b/extensions/html/extension.toml index 47f04289da3ae0aca6fb84ea0de4e7a2cd9d1cf3..0406749240712812caef0717bdcff8edaebe9023 100644 --- a/extensions/html/extension.toml +++ b/extensions/html/extension.toml @@ -1,7 +1,7 @@ id = "html" name = "HTML" description = "HTML support." -version = "0.0.1" +version = "0.0.2" schema_version = 1 authors = ["Isaac Clayton "] repository = "https://github.com/zed-industries/zed" @@ -9,6 +9,15 @@ repository = "https://github.com/zed-industries/zed" [language_servers.vscode-html-language-server] name = "vscode-html-language-server" language = "HTML" +languages = ["TypeScript", "HTML", "TSX", "JavaScript", "JSDoc"] + +[language_servers.vscode-html-language-server.language_ids] +"HTML" = "html" +"PHP" = "php" +"ERB" = "eruby" +"JavaScript" = "javascriptreact" +"TSX" = "typescriptreact" +"CSS" = "css" [grammars.html] repository = "https://github.com/tree-sitter/tree-sitter-html" diff --git a/extensions/html/languages/html/config.toml b/extensions/html/languages/html/config.toml index 389020b89d07f607b07c7cadf89588605c133e5b..e8974b382f2220531975773f64994d90f29fd476 100644 --- a/extensions/html/languages/html/config.toml +++ b/extensions/html/languages/html/config.toml @@ -8,7 +8,7 @@ brackets = [ { start = "[", end = "]", close = true, newline = true }, { start = "(", end = ")", close = true, newline = true }, { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, - { start = "<", end = ">", close = true, newline = true, not_in = ["comment", "string"] }, + { start = "<", end = ">", close = false, newline = true, not_in = ["comment", "string"] }, { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] }, ] word_characters = ["-"] diff --git a/extensions/html/src/html.rs b/extensions/html/src/html.rs index 4cbc41d14b470f44206a47227d0fef548db98c48..8d8b1af6ab5585908591c229fc1befeac995ff60 100644 --- a/extensions/html/src/html.rs +++ b/extensions/html/src/html.rs @@ -1,79 +1,93 @@ -use std::{env, fs}; +use std::{env, fs, path::PathBuf}; use zed_extension_api::{self as zed, Result}; -const SERVER_PATH: &str = - "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server"; -const PACKAGE_NAME: &str = "vscode-langservers-extracted"; +const PACKAGE_NAME: &str = "vscode-language-server"; struct HtmlExtension { - did_find_server: bool, + path: Option, } impl HtmlExtension { - fn server_exists(&self) -> bool { - fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) - } - - fn server_script_path(&mut self, config: zed::LanguageServerConfig) -> Result { - let server_exists = self.server_exists(); - if self.did_find_server && server_exists { - return Ok(SERVER_PATH.to_string()); + fn server_script_path(&self, language_server_id: &zed::LanguageServerId) -> Result { + if let Some(path) = self.path.as_ref() { + if fs::metadata(path).map_or(false, |stat| stat.is_dir()) { + return Ok(path.clone()); + } } zed::set_language_server_installation_status( - &config.name, + language_server_id, &zed::LanguageServerInstallationStatus::CheckingForUpdate, ); - let version = zed::npm_package_latest_version(PACKAGE_NAME)?; + let release = zed::latest_github_release( + "zed-industries/vscode-langservers-extracted", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; - if !server_exists - || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) - { + let asset_name = "vscode-language-server.tar.gz"; + + 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!("{}-{}", PACKAGE_NAME, release.version); + if !fs::metadata(&version_dir).map_or(false, |stat| stat.is_dir()) { zed::set_language_server_installation_status( - &config.name, + &language_server_id, &zed::LanguageServerInstallationStatus::Downloading, ); - let result = zed::npm_install_package(PACKAGE_NAME, &version); - match result { - Ok(()) => { - if !self.server_exists() { - Err(format!( - "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'", - ))?; - } - } - Err(error) => { - if !self.server_exists() { - Err(error)?; - } + + 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.did_find_server = true; - Ok(SERVER_PATH.to_string()) + Ok(PathBuf::from(version_dir) + .join("bin") + .join("vscode-html-language-server")) } } impl zed::Extension for HtmlExtension { fn new() -> Self { - Self { - did_find_server: false, - } + Self { path: None } } fn language_server_command( &mut self, - config: zed::LanguageServerConfig, + language_server_id: &zed::LanguageServerId, _worktree: &zed::Worktree, ) -> Result { - let server_path = self.server_script_path(config)?; + let path = match &self.path { + Some(path) => path, + None => { + let path = self.server_script_path(language_server_id)?; + self.path = Some(path); + self.path.as_ref().unwrap() + } + }; + Ok(zed::Command { command: zed::node_binary_path()?, args: vec![ env::current_dir() .unwrap() - .join(&server_path) + .join(path) .to_string_lossy() .to_string(), "--stdio".to_string(),