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",
Mikayla Maki created
This is a PR I built for a friend of a friend at StrangeLoop, who is
making a much better LSP for elixir that elixir folks want to experiment
with. This PR also improves the our debug log viewer to handle LSP
restarts.
TODO:
- [ ] Make sure NextLS binary loading works.
Release Notes:
- Added support for the experimental Next LS for Elxir, to enable it add
the following field to your settings to enable:
```json
"elixir": {
"next": "on"
}
```
Cargo.lock | 2
assets/settings/default.json | 21 +
crates/language_tools/src/lsp_log.rs | 50 +++
crates/lsp/src/lsp.rs | 4
crates/project/src/project.rs | 20 +
crates/project_symbols/src/project_symbols.rs | 2
crates/semantic_index/examples/eval.rs | 2
crates/zed/Cargo.toml | 2
crates/zed/src/languages.rs | 39 ++
crates/zed/src/languages/elixir_next.rs | 266 +++++++++++++++++++++
crates/zed/src/main.rs | 2
crates/zed/src/zed.rs | 3
12 files changed, 397 insertions(+), 16 deletions(-)
@@ -9875,12 +9875,14 @@ dependencies = [
"rpc",
"rsa",
"rust-embed",
+ "schemars",
"search",
"semantic_index",
"serde",
"serde_derive",
"serde_json",
"settings",
+ "shellexpand",
"simplelog",
"smallvec",
"smol",
@@ -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": {
@@ -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<LanguageServerId>,
is_showing_rpc_trace: bool,
project: ModelHandle<Project>,
+ _log_store_subscription: Subscription,
}
pub struct LspLogToolbarItemView {
log_view: Option<ViewHandle<LspLogView>>,
+ _log_view_subscription: Option<Subscription>,
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<Self>,
+ cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
self.menu_open = false;
if let Some(item) = active_pane_item {
if let Some(log_view) = item.downcast::<LspLogView>() {
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,
}
}
@@ -716,11 +716,11 @@ impl LanguageServer {
}
}
- pub fn name<'a>(self: &'a Arc<Self>) -> &'a str {
+ pub fn name(&self) -> &str {
&self.name
}
- pub fn capabilities<'a>(self: &'a Arc<Self>) -> &'a ServerCapabilities {
+ pub fn capabilities(&self) -> &ServerCapabilities {
&self.capabilities
}
@@ -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::notification::DidSaveTextDocument>(
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)
+}
@@ -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(),
));
@@ -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);
@@ -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
@@ -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<LanguageRegistry>, node_runtime: Arc<dyn NodeRuntime>) {
+pub fn init(
+ languages: Arc<LanguageRegistry>,
+ node_runtime: Arc<dyn NodeRuntime>,
+ cx: &mut AppContext,
+) {
+ settings::register::<elixir_next::ElixirSettings>(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<LanguageRegistry>, node_runtime: Arc<dyn NodeRuntime>
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
],
);
- language(
- "elixir",
- tree_sitter_elixir::language(),
- vec![Arc::new(elixir::ElixirLspAdapter)],
- );
+
+ match &settings::get::<ElixirSettings>(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(),
@@ -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<String>,
+ },
+}
+
+#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
+pub struct ElixirSettingsContent {
+ next: Option<ElixirNextSetting>,
+}
+
+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<Self>
+ 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<Box<dyn 'static + Send + Any>> {
+ 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<dyn 'static + Send + Any>,
+ container_dir: PathBuf,
+ delegate: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ let version = version.downcast::<GitHubLspBinaryVersion>().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,
+ <fs::Permissions as fs::unix::PermissionsExt>::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<LanguageServerBinary> {
+ 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<LanguageServerBinary> {
+ 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<Language>,
+ ) -> Option<CodeLabel> {
+ label_for_symbol_next(name, symbol_kind, language)
+ }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ 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<String>,
+}
+
+#[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<Box<dyn 'static + Send + Any>> {
+ Ok(Box::new(()) as Box<_>)
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ _: Box<dyn 'static + Send + Any>,
+ _: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ 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<LanguageServerBinary> {
+ 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<LanguageServerBinary> {
+ 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<Language>,
+ ) -> Option<CodeLabel> {
+ label_for_symbol_next(name, symbol, language)
+ }
+}
+
+fn label_for_symbol_next(name: &str, _: SymbolKind, language: &Arc<Language>) -> Option<CodeLabel> {
+ Some(CodeLabel {
+ runs: language.highlight_text(&name.into(), 0..name.len()),
+ text: name.to_string(),
+ filter_range: 0..name.len(),
+ })
+}
@@ -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));
@@ -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);
}