Add local next LSP adapter

Mikayla created

Change summary

crates/lsp/src/lsp.rs                         |   4 
crates/project/src/project.rs                 |  20 ++
crates/project_symbols/src/project_symbols.rs |   2 
crates/zed/src/languages.rs                   |  14 
crates/zed/src/languages/elixir_next.rs       | 178 +++++++++++++++++++++
5 files changed, 206 insertions(+), 12 deletions(-)

Detailed changes

crates/lsp/src/lsp.rs 🔗

@@ -712,11 +712,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
     }
 

crates/project/src/project.rs 🔗

@@ -2267,11 +2267,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();
@@ -8274,3 +8276,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)
+}

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(),
         ));

crates/zed/src/languages.rs 🔗

@@ -72,22 +72,20 @@ pub fn init(
         ],
     );
 
-    match settings::get::<ElixirSettings>(cx).next {
+    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_next::ElixirNextSetting::On => todo!(),
+        elixir_next::ElixirNextSetting::Local { path } => language(
             "elixir",
             tree_sitter_elixir::language(),
-            vec![Arc::new(elixir_next::BundledNextLspAdapter)],
+            vec![Arc::new(elixir_next::LocalNextLspAdapter {
+                path: path.clone(),
+            })],
         ),
-        elixir_next::ElixirNextSetting::Local { port } => unimplemented!(), /*language(
-                                                                            "elixir",
-                                                                            tree_sitter_elixir::language(),
-                                                                            vec![Arc::new(elixir_next::LocalNextLspAdapter { port })],
-                                                                            )*/
     }
 
     language(

crates/zed/src/languages/elixir_next.rs 🔗

@@ -0,0 +1,178 @@
+use anyhow::Result;
+use async_trait::async_trait;
+pub use language::*;
+use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
+use std::{any::Any, path::PathBuf, sync::Arc};
+
+#[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 },
+}
+
+#[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 LocalNextLspAdapter {
+    pub path: String,
+}
+
+#[async_trait]
+impl LspAdapter for LocalNextLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("elixir-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> {
+        Ok(LanguageServerBinary {
+            path: self.path.clone().into(),
+            arguments: vec!["--stdio".into()],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: self.path.clone().into(),
+            arguments: vec!["--stdio".into()],
+        })
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: self.path.clone().into(),
+            arguments: vec!["--stdio".into()],
+        })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        match completion.kind.zip(completion.detail.as_ref()) {
+            Some((_, detail)) if detail.starts_with("(function)") => {
+                let text = detail.strip_prefix("(function) ")?;
+                let filter_range = 0..text.find('(').unwrap_or(text.len());
+                let source = Rope::from(format!("def {text}").as_str());
+                let runs = language.highlight_text(&source, 4..4 + text.len());
+                return Some(CodeLabel {
+                    text: text.to_string(),
+                    runs,
+                    filter_range,
+                });
+            }
+            Some((_, detail)) if detail.starts_with("(macro)") => {
+                let text = detail.strip_prefix("(macro) ")?;
+                let filter_range = 0..text.find('(').unwrap_or(text.len());
+                let source = Rope::from(format!("defmacro {text}").as_str());
+                let runs = language.highlight_text(&source, 9..9 + text.len());
+                return Some(CodeLabel {
+                    text: text.to_string(),
+                    runs,
+                    filter_range,
+                });
+            }
+            Some((
+                CompletionItemKind::CLASS
+                | CompletionItemKind::MODULE
+                | CompletionItemKind::INTERFACE
+                | CompletionItemKind::STRUCT,
+                _,
+            )) => {
+                let filter_range = 0..completion
+                    .label
+                    .find(" (")
+                    .unwrap_or(completion.label.len());
+                let text = &completion.label[filter_range.clone()];
+                let source = Rope::from(format!("defmodule {text}").as_str());
+                let runs = language.highlight_text(&source, 10..10 + text.len());
+                return Some(CodeLabel {
+                    text: completion.label.clone(),
+                    runs,
+                    filter_range,
+                });
+            }
+            _ => {}
+        }
+
+        None
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            SymbolKind::METHOD | SymbolKind::FUNCTION => {
+                let text = format!("def {}", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
+                let text = format!("defmodule {}", name);
+                let filter_range = 10..10 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}