diff --git a/Cargo.lock b/Cargo.lock index 878604f3609fab58f724f62e16312a150522fc35..b42ca85b8d2421f6a871280df81eea2b14086d63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9875,12 +9875,14 @@ dependencies = [ "rpc", "rsa", "rust-embed", + "schemars", "search", "semantic_index", "serde", "serde_derive", "serde_json", "settings", + "shellexpand", "simplelog", "smallvec", "smol", diff --git a/assets/settings/default.json b/assets/settings/default.json index 22ea2665333b6eb1630755840f58888a56c67d67..1f8068d109fac75f228dc81ab3cf1dd199ecf8f0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -372,6 +372,27 @@ "semantic_index": { "enabled": false }, + // Settings specific to our elixir integration + "elixir": { + // Set Zed to use the experimental Next LS LSP server. + // Note that changing this setting requires a restart of Zed + // to take effect. + // + // May take 3 values: + // 1. Use the standard elixir-ls LSP server + // "next": "off" + // 2. Use a bundled version of the next Next LS LSP server + // "next": "on", + // 3. Use a local build of the next Next LS LSP server: + // "next": { + // "local": { + // "path": "~/next-ls/bin/start", + // "arguments": ["--stdio"] + // } + // }, + // + "next": "off" + }, // Different settings for specific languages. "languages": { "Plain Text": { diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 3f29d6f79b87b1c168b8fda47d82f73511c370bd..faed37a97cfd5bf9d3a15c218b82c9f2be960ef4 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -8,8 +8,8 @@ use gpui::{ ParentElement, Stack, }, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext, - ViewHandle, WeakModelHandle, + AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View, + ViewContext, ViewHandle, WeakModelHandle, }; use language::{Buffer, LanguageServerId, LanguageServerName}; use lsp::IoKind; @@ -53,10 +53,12 @@ pub struct LspLogView { current_server_id: Option, is_showing_rpc_trace: bool, project: ModelHandle, + _log_store_subscription: Subscription, } pub struct LspLogToolbarItemView { log_view: Option>, + _log_view_subscription: Option, menu_open: bool, } @@ -373,12 +375,49 @@ impl LspLogView { .get(&project.downgrade()) .and_then(|project| project.servers.keys().copied().next()); let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); + let _log_store_subscription = cx.observe(&log_store, |this, store, cx| { + (|| -> Option<()> { + let project_state = store.read(cx).projects.get(&this.project.downgrade())?; + if let Some(current_lsp) = this.current_server_id { + if !project_state.servers.contains_key(¤t_lsp) { + if let Some(server) = project_state.servers.iter().next() { + if this.is_showing_rpc_trace { + this.show_rpc_trace_for_server(*server.0, cx) + } else { + this.show_logs_for_server(*server.0, cx) + } + } else { + this.current_server_id = None; + this.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.clear(cx); + editor.set_read_only(true); + }); + cx.notify(); + } + } + } else { + if let Some(server) = project_state.servers.iter().next() { + if this.is_showing_rpc_trace { + this.show_rpc_trace_for_server(*server.0, cx) + } else { + this.show_logs_for_server(*server.0, cx) + } + } + } + + Some(()) + })(); + + cx.notify(); + }); let mut this = Self { editor: Self::editor_for_buffer(project.clone(), buffer, cx), project, log_store, current_server_id: None, is_showing_rpc_trace: false, + _log_store_subscription, }; if let Some(server_id) = server_id { this.show_logs_for_server(server_id, cx); @@ -601,18 +640,22 @@ impl ToolbarItemView for LspLogToolbarItemView { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, - _: &mut ViewContext, + cx: &mut ViewContext, ) -> workspace::ToolbarItemLocation { self.menu_open = false; if let Some(item) = active_pane_item { if let Some(log_view) = item.downcast::() { self.log_view = Some(log_view.clone()); + self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { + cx.notify(); + })); return ToolbarItemLocation::PrimaryLeft { flex: Some((1., false)), }; } } self.log_view = None; + self._log_view_subscription = None; ToolbarItemLocation::Hidden } } @@ -743,6 +786,7 @@ impl LspLogToolbarItemView { Self { menu_open: false, log_view: None, + _log_view_subscription: None, } } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 9b0d6c98b0ec48fd7207daa25f6a602abe8fd16b..33581721ae8fff8bc32f085ad3b7359209ee6cf2 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -716,11 +716,11 @@ impl LanguageServer { } } - pub fn name<'a>(self: &'a Arc) -> &'a str { + pub fn name(&self) -> &str { &self.name } - pub fn capabilities<'a>(self: &'a Arc) -> &'a ServerCapabilities { + pub fn capabilities(&self) -> &ServerCapabilities { &self.capabilities } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e4858587ad46fb5a7eef8baa52cdb18c2692ce4c..4b31bc31bc78d470107e900857c0df51cc1d947b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2280,11 +2280,13 @@ impl Project { }; for (_, _, server) in self.language_servers_for_worktree(worktree_id) { + let text = include_text(server.as_ref()).then(|| buffer.read(cx).text()); + server .notify::( lsp::DidSaveTextDocumentParams { text_document: text_document.clone(), - text: None, + text, }, ) .log_err(); @@ -8325,3 +8327,19 @@ async fn wait_for_loading_buffer( receiver.next().await; } } + +fn include_text(server: &lsp::LanguageServer) -> bool { + server + .capabilities() + .text_document_sync + .as_ref() + .and_then(|sync| match sync { + lsp::TextDocumentSyncCapability::Kind(_) => None, + lsp::TextDocumentSyncCapability::Options(options) => options.save.as_ref(), + }) + .and_then(|save_options| match save_options { + lsp::TextDocumentSyncSaveOptions::Supported(_) => None, + lsp::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text, + }) + .unwrap_or(false) +} diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index e88aee5dcfb6b9b5862ba92e6e8fc32fbc0da8cb..3273d5c6e664fb8ec84af4270222dbcf7b0d673f 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -69,7 +69,7 @@ impl ProjectSymbolsDelegate { &self.external_match_candidates, query, false, - MAX_MATCHES - visible_matches.len(), + MAX_MATCHES - visible_matches.len().min(MAX_MATCHES), &Default::default(), cx.background().clone(), )); diff --git a/crates/semantic_index/examples/eval.rs b/crates/semantic_index/examples/eval.rs index a0cdbeea0505e378230b9208b9c08402fbe00c4e..573cf73d783fde74aba7669b1d09fc088bed67ff 100644 --- a/crates/semantic_index/examples/eval.rs +++ b/crates/semantic_index/examples/eval.rs @@ -456,7 +456,7 @@ fn main() { let languages = Arc::new(languages); let node_runtime = RealNodeRuntime::new(http.clone()); - languages::init(languages.clone(), node_runtime.clone()); + languages::init(languages.clone(), node_runtime.clone(), cx); language::init(cx); project::Project::init(&client, cx); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fc06e8865a1d6b97d22981d960c7cce72d3195eb..1d43b482c2c836e76b291db16283ef9e2e024504 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -62,6 +62,7 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } +shellexpand = "2.1.0" text = { path = "../text" } terminal_view = { path = "../terminal_view" } theme = { path = "../theme" } @@ -99,6 +100,7 @@ rust-embed.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true +schemars.workspace = true simplelog = "0.9" smallvec.workspace = true smol.workspace = true diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 0b1fa750c084dd44a006cad258f4e7d11fc153f9..be8d05256ae30646da0f759b13abf8786c3a301a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,13 +1,17 @@ use anyhow::Context; +use gpui::AppContext; pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; use util::asset_str; +use self::elixir_next::ElixirSettings; + mod c; mod css; mod elixir; +mod elixir_next; mod go; mod html; mod json; @@ -37,7 +41,13 @@ mod yaml; #[exclude = "*.rs"] struct LanguageDir; -pub fn init(languages: Arc, node_runtime: Arc) { +pub fn init( + languages: Arc, + node_runtime: Arc, + cx: &mut AppContext, +) { + settings::register::(cx); + let language = |name, grammar, adapters| { languages.register(name, load_config(name), grammar, adapters, load_queries) }; @@ -61,11 +71,28 @@ pub fn init(languages: Arc, node_runtime: Arc Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); - language( - "elixir", - tree_sitter_elixir::language(), - vec![Arc::new(elixir::ElixirLspAdapter)], - ); + + match &settings::get::(cx).next { + elixir_next::ElixirNextSetting::Off => language( + "elixir", + tree_sitter_elixir::language(), + vec![Arc::new(elixir::ElixirLspAdapter)], + ), + elixir_next::ElixirNextSetting::On => language( + "elixir", + tree_sitter_elixir::language(), + vec![Arc::new(elixir_next::NextLspAdapter)], + ), + elixir_next::ElixirNextSetting::Local { path, arguments } => language( + "elixir", + tree_sitter_elixir::language(), + vec![Arc::new(elixir_next::LocalNextLspAdapter { + path: path.clone(), + arguments: arguments.clone(), + })], + ), + } + language( "go", tree_sitter_go::language(), diff --git a/crates/zed/src/languages/elixir_next.rs b/crates/zed/src/languages/elixir_next.rs new file mode 100644 index 0000000000000000000000000000000000000000..f5a77c75681289c3ab50d79fc990f4e3b7d621dc --- /dev/null +++ b/crates/zed/src/languages/elixir_next.rs @@ -0,0 +1,266 @@ +use anyhow::{anyhow, bail, Result}; + +use async_trait::async_trait; +pub use language::*; +use lsp::{LanguageServerBinary, SymbolKind}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings::Setting; +use smol::{fs, stream::StreamExt}; +use std::{any::Any, env::consts, ops::Deref, path::PathBuf, sync::Arc}; +use util::{ + async_iife, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct ElixirSettings { + pub next: ElixirNextSetting, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ElixirNextSetting { + Off, + On, + Local { + path: String, + arguments: Vec, + }, +} + +#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] +pub struct ElixirSettingsContent { + next: Option, +} + +impl Setting for ElixirSettings { + const KEY: Option<&'static str> = Some("elixir"); + + type FileContent = ElixirSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> Result + where + Self: Sized, + { + Self::load_via_json_merge(default_value, user_values) + } +} + +pub struct NextLspAdapter; + +#[async_trait] +impl LspAdapter for NextLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("next-ls".into()) + } + + fn short_name(&self) -> &'static str { + "next-ls" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = + latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?; + let version = release.name.clone(); + let platform = match consts::ARCH { + "x86_64" => "darwin_arm64", + "aarch64" => "darwin_amd64", + other => bail!("Running on unsupported platform: {other}"), + }; + let asset_name = format!("next_ls_{}", platform); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: version, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + + let binary_path = container_dir.join("next-ls"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + + let mut file = smol::fs::File::create(&binary_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + } + + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec!["--stdio".into()], + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--stdio".into()]; + binary + }) + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) + } + + async fn label_for_symbol( + &self, + name: &str, + symbol_kind: SymbolKind, + language: &Arc, + ) -> Option { + label_for_symbol_next(name, symbol_kind, language) + } +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + async_iife!({ + let mut last_binary_path = 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_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name == "next-ls") + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: Vec::new(), + }) + } else { + Err(anyhow!("no cached binary")) + } + }) + .await + .log_err() +} + +pub struct LocalNextLspAdapter { + pub path: String, + pub arguments: Vec, +} + +#[async_trait] +impl LspAdapter for LocalNextLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("local-next-ls".into()) + } + + fn short_name(&self) -> &'static str { + "next-ls" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(()) as Box<_>) + } + + async fn fetch_server_binary( + &self, + _: Box, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let path = shellexpand::full(&self.path)?; + Ok(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let path = shellexpand::full(&self.path).ok()?; + Some(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + let path = shellexpand::full(&self.path).ok()?; + Some(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) + } + + async fn label_for_symbol( + &self, + name: &str, + symbol: SymbolKind, + language: &Arc, + ) -> Option { + label_for_symbol_next(name, symbol, language) + } +} + +fn label_for_symbol_next(name: &str, _: SymbolKind, language: &Arc) -> Option { + Some(CodeLabel { + runs: language.highlight_text(&name.into(), 0..name.len()), + text: name.to_string(), + filter_range: 0..name.len(), + }) +} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0032c24cbbdf7bf250719540f2488178933f3f6d..bb44f67841eda0af2007521c275a7ff255bdc677 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -135,7 +135,7 @@ fn main() { let languages = Arc::new(languages); let node_runtime = RealNodeRuntime::new(http.clone()); - languages::init(languages.clone(), node_runtime.clone()); + languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let channel_store = cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bdf060205a7895a3e78736edf43bf1df03714fbc..fb380d775d30e89bd07099a7b6d6582f9edff470 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2388,11 +2388,12 @@ mod tests { #[gpui::test] fn test_bundled_languages(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); let mut languages = LanguageRegistry::test(); languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); let node_runtime = node_runtime::FakeNodeRuntime::new(); - languages::init(languages.clone(), node_runtime); + languages::init(languages.clone(), node_runtime, cx); for name in languages.language_names() { languages.language_for_name(&name); }