From 8d7f5eab7907513c429b025768a29727cb5555e3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 11 Apr 2024 15:20:19 -0700 Subject: [PATCH] Extract Ocaml language support into an extension (#10450) Release Notes: - Extracted Ocaml language support into an extension --------- Co-authored-by: Marshall --- Cargo.lock | 17 +- Cargo.toml | 2 +- crates/extensions_ui/src/extension_suggest.rs | 1 + crates/language/src/language_registry.rs | 18 +- crates/languages/Cargo.toml | 1 - crates/languages/src/lib.rs | 8 - crates/languages/src/ocaml.rs | 314 ------------------ crates/project/src/project.rs | 15 +- extensions/ocaml/Cargo.toml | 16 + extensions/ocaml/extension.toml | 21 ++ .../languages}/ocaml-interface/brackets.scm | 0 .../languages}/ocaml-interface/config.toml | 0 .../languages}/ocaml-interface/highlights.scm | 0 .../languages}/ocaml-interface/indents.scm | 0 .../languages}/ocaml-interface/outline.scm | 0 .../ocaml/languages}/ocaml/brackets.scm | 0 .../ocaml/languages}/ocaml/config.toml | 0 .../ocaml/languages}/ocaml/highlights.scm | 0 .../ocaml/languages}/ocaml/indents.scm | 0 .../ocaml/languages}/ocaml/outline.scm | 0 extensions/ocaml/src/ocaml.rs | 219 ++++++++++++ 21 files changed, 289 insertions(+), 343 deletions(-) delete mode 100644 crates/languages/src/ocaml.rs create mode 100644 extensions/ocaml/Cargo.toml create mode 100644 extensions/ocaml/extension.toml rename {crates/languages/src => extensions/ocaml/languages}/ocaml-interface/brackets.scm (100%) rename {crates/languages/src => extensions/ocaml/languages}/ocaml-interface/config.toml (100%) rename {crates/languages/src => extensions/ocaml/languages}/ocaml-interface/highlights.scm (100%) rename {crates/languages/src => extensions/ocaml/languages}/ocaml-interface/indents.scm (100%) rename {crates/languages/src => extensions/ocaml/languages}/ocaml-interface/outline.scm (100%) rename {crates/languages/src => extensions/ocaml/languages}/ocaml/brackets.scm (100%) rename {crates/languages/src => extensions/ocaml/languages}/ocaml/config.toml (100%) rename {crates/languages/src => extensions/ocaml/languages}/ocaml/highlights.scm (100%) rename {crates/languages/src => extensions/ocaml/languages}/ocaml/indents.scm (100%) rename {crates/languages/src => extensions/ocaml/languages}/ocaml/outline.scm (100%) create mode 100644 extensions/ocaml/src/ocaml.rs diff --git a/Cargo.lock b/Cargo.lock index ab51b79c5ac92df09e5e05275d69e456bd72a73a..1bfbc9ca57a81d32bb323479a0b176f5b6a84b88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5469,7 +5469,6 @@ dependencies = [ "tree-sitter-json 0.20.0", "tree-sitter-markdown", "tree-sitter-nu", - "tree-sitter-ocaml", "tree-sitter-proto", "tree-sitter-python", "tree-sitter-regex", @@ -10492,15 +10491,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "tree-sitter-ocaml" -version = "0.20.4" -source = "git+https://github.com/tree-sitter/tree-sitter-ocaml?rev=4abfdc1c7af2c6c77a370aee974627be1c285b3b#4abfdc1c7af2c6c77a370aee974627be1c285b3b" -dependencies = [ - "cc", - "tree-sitter", -] - [[package]] name = "tree-sitter-proto" version = "0.0.2" @@ -12658,6 +12648,13 @@ dependencies = [ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "zed_ocaml" +version = "0.0.1" +dependencies = [ + "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "zed_php" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 5802072805d50b3c08ee149607682dd13d4941b8..97c399ed8f45cd14a10b35c3b013b2a937bf1029 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ members = [ "extensions/haskell", "extensions/html", "extensions/lua", + "extensions/ocaml", "extensions/php", "extensions/prisma", "extensions/purescript", @@ -329,7 +330,6 @@ tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" } tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" } -tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" } tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" } tree-sitter-python = "0.20.2" tree-sitter-regex = "0.20.0" diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index cc9df0f2e9f4724e9024b3d9d4c999c999d76a7b..2d4d16402056b62add68fb348aa7cc7ac247759f 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -48,6 +48,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ ("lua", &["lua"]), ("make", &["Makefile"]), ("nix", &["nix"]), + ("ocaml", &["ml", "mli"]), ("php", &["php"]), ("prisma", &["prisma"]), ("purescript", &["purs"]), diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index c1346f8ee3330ecd15c3f2e75a89ade33a0cded6..6d30a80bacc7c6ac19bd1ca4ccd9e0ab3c3b98d4 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1,11 +1,14 @@ -use crate::language_settings::{AllLanguageSettingsContent, LanguageSettingsContent}; use crate::{ - language_settings::all_language_settings, task_context::ContextProvider, CachedLspAdapter, - File, Language, LanguageConfig, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter, - LspAdapterDelegate, PARSER, PLAIN_TEXT, + language_settings::{ + all_language_settings, AllLanguageSettingsContent, LanguageSettingsContent, + }, + task_context::ContextProvider, + CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher, + LanguageServerName, LspAdapter, LspAdapterDelegate, PARSER, PLAIN_TEXT, }; use anyhow::{anyhow, Context as _, Result}; use collections::{hash_map, HashMap}; +use futures::TryFutureExt; use futures::{ channel::{mpsc, oneshot}, future::Shared, @@ -454,11 +457,12 @@ impl LanguageRegistry { ) } - pub fn language_for_file_path( + pub fn language_for_file_path<'a>( self: &Arc, - path: &Path, - ) -> impl Future>> { + path: &'a Path, + ) -> impl Future>> + 'a { self.language_for_file_internal(path, None, None) + .map_err(|error| error.context(format!("language for file path {}", path.display()))) } fn language_for_file_internal( diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 29913f424e68846ed79e30770f9da51bd664b7d7..d43c7339021e562be7033e4cae603adbeadda312 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -51,7 +51,6 @@ tree-sitter-jsdoc.workspace = true tree-sitter-json.workspace = true tree-sitter-markdown.workspace = true tree-sitter-nu.workspace = true -tree-sitter-ocaml.workspace = true tree-sitter-proto.workspace = true tree-sitter-python.workspace = true tree-sitter-regex.workspace = true diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 9a11a421d88c5cb1e5e6b505684f8e67c41c92ae..38d8fc50c3ff6038a03449f1b5f5019e2413fb93 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -19,7 +19,6 @@ mod elixir; mod go; mod json; mod nu; -mod ocaml; mod python; mod ruby; mod rust; @@ -70,11 +69,6 @@ pub fn init( ("json", tree_sitter_json::language()), ("markdown", tree_sitter_markdown::language()), ("nu", tree_sitter_nu::language()), - ("ocaml", tree_sitter_ocaml::language_ocaml()), - ( - "ocaml_interface", - tree_sitter_ocaml::language_ocaml_interface(), - ), ("proto", tree_sitter_proto::language()), ("python", tree_sitter_python::language()), ("regex", tree_sitter_regex::language()), @@ -278,8 +272,6 @@ pub fn init( vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))] ); language!("nu", vec![Arc::new(nu::NuLanguageServer {})]); - language!("ocaml", vec![Arc::new(ocaml::OCamlLspAdapter)]); - language!("ocaml-interface", vec![Arc::new(ocaml::OCamlLspAdapter)]); language!( "vue", vec![ diff --git a/crates/languages/src/ocaml.rs b/crates/languages/src/ocaml.rs deleted file mode 100644 index 74f5f602998efcc34ef36e15e5584090a5d5d7f7..0000000000000000000000000000000000000000 --- a/crates/languages/src/ocaml.rs +++ /dev/null @@ -1,314 +0,0 @@ -use std::{any::Any, ops::Range, path::PathBuf, sync::Arc}; - -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use language::{CodeLabel, LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; -use rope::Rope; - -const OPERATOR_CHAR: [char; 17] = [ - '~', '!', '?', '%', '<', ':', '.', '$', '&', '*', '+', '-', '/', '=', '>', '@', '^', -]; - -pub struct OCamlLspAdapter; - -#[async_trait(?Send)] -impl LspAdapter for OCamlLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("ocamllsp".into()) - } - - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new(())) - } - - async fn fetch_server_binary( - &self, - _: Box, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - Err(anyhow!( - "ocamllsp (ocaml-language-server) must be installed manually." - )) - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - Some(LanguageServerBinary { - path: "ocamllsp".into(), - env: None, - arguments: vec![], - }) - } - - fn can_be_reinstalled(&self) -> bool { - false - } - - async fn installation_test_binary(&self, _: PathBuf) -> Option { - None - } - - async fn label_for_completion( - &self, - completion: &lsp::CompletionItem, - language: &Arc, - ) -> Option { - let name = &completion.label; - let detail = completion.detail.as_ref().map(|s| s.replace('\n', " ")); - - match completion.kind.zip(detail) { - // Error of 'b : ('a, 'b) result - // Stack_overflow : exn - Some((CompletionItemKind::CONSTRUCTOR | CompletionItemKind::ENUM_MEMBER, detail)) => { - let (argument, return_t) = detail - .split_once("->") - .map_or((None, detail.as_str()), |(arg, typ)| { - (Some(arg.trim()), typ.trim()) - }); - - let constr_decl = argument.map_or(name.to_string(), |argument| { - format!("{} of {}", name, argument) - }); - - let constr_host = if return_t.ends_with("exn") { - "exception " - } else { - "type t = " - }; - - let source_host = Rope::from([constr_host, &constr_decl].join(" ")); - let mut source_highlight = { - let constr_host_len = constr_host.len() + 1; - - language.highlight_text( - &source_host, - Range { - start: constr_host_len, - end: constr_host_len + constr_decl.len(), - }, - ) - }; - - let signature_host: Rope = Rope::from(format!("let _ : {} = ()", return_t)); - - // We include the ': ' in the range as we use it later - let mut signature_highlight = - language.highlight_text(&signature_host, 6..8 + return_t.len()); - - if let Some(last) = source_highlight.last() { - let offset = last.0.end + 1; - - signature_highlight.iter_mut().for_each(|(r, _)| { - r.start += offset; - r.end += offset; - }); - }; - - Some(CodeLabel { - text: format!("{} : {}", constr_decl, return_t), - runs: { - source_highlight.append(&mut signature_highlight); - source_highlight - }, - filter_range: 0..name.len(), - }) - } - // version : string - // NOTE: (~|?) are omitted as we don't use them in the fuzzy filtering - Some((CompletionItemKind::FIELD, detail)) - if name.starts_with('~') || name.starts_with('?') => - { - let label = name.trim_start_matches(&['~', '?']); - let text = format!("{} : {}", label, detail); - - let signature_host = Rope::from(format!("let _ : {} = ()", detail)); - let signature_highlight = - &mut language.highlight_text(&signature_host, 6..8 + detail.len()); - - let offset = label.len() + 1; - for (r, _) in signature_highlight.iter_mut() { - r.start += offset; - r.end += offset; - } - - let mut label_highlight = vec![( - 0..label.len(), - language.grammar()?.highlight_id_for_name("property")?, - )]; - - Some(CodeLabel { - text, - runs: { - label_highlight.append(signature_highlight); - label_highlight - }, - filter_range: 0..label.len(), - }) - } - // version: string; - Some((CompletionItemKind::FIELD, detail)) => { - let (_record_t, field_t) = detail.split_once("->")?; - - let text = format!("{}: {};", name, field_t); - let source_host: Rope = Rope::from(format!("type t = {{ {} }}", text)); - - let runs: Vec<(Range, language::HighlightId)> = - language.highlight_text(&source_host, 11..11 + text.len()); - - Some(CodeLabel { - text, - runs, - filter_range: 0..name.len(), - }) - } - // let* : 'a t -> ('a -> 'b t) -> 'b t - Some((CompletionItemKind::VALUE, detail)) - if name.contains(OPERATOR_CHAR) - || (name.starts_with("let") && name.contains(OPERATOR_CHAR)) => - { - let text = format!("{} : {}", name, detail); - - let source_host = Rope::from(format!("let ({}) : {} = ()", name, detail)); - let mut runs = language.highlight_text(&source_host, 5..6 + text.len()); - - if runs.len() > 1 { - // ')' - runs.remove(1); - - for run in &mut runs[1..] { - run.0.start -= 1; - run.0.end -= 1; - } - } - - Some(CodeLabel { - text, - runs, - filter_range: 0..name.len(), - }) - } - // version : Version.t list -> Version.t option Lwt.t - Some((CompletionItemKind::VALUE, detail)) => { - let text = format!("{} : {}", name, detail); - - let source_host = Rope::from(format!("let {} = ()", text)); - let runs = language.highlight_text(&source_host, 4..4 + text.len()); - - Some(CodeLabel { - text, - runs, - filter_range: 0..name.len(), - }) - } - // status : string - Some((CompletionItemKind::METHOD, detail)) => { - let text = format!("{} : {}", name, detail); - - let method_host = Rope::from(format!("class c : object method {} end", text)); - let runs = language.highlight_text(&method_host, 24..24 + text.len()); - - Some(CodeLabel { - text, - runs, - filter_range: 0..name.len(), - }) - } - Some((kind, _)) => { - let highlight_name = match kind { - CompletionItemKind::MODULE | CompletionItemKind::INTERFACE => "title", - CompletionItemKind::KEYWORD => "keyword", - CompletionItemKind::TYPE_PARAMETER => "type", - _ => return None, - }; - - Some(CodeLabel { - text: name.clone(), - runs: vec![( - 0..name.len(), - language.grammar()?.highlight_id_for_name(highlight_name)?, - )], - filter_range: 0..name.len(), - }) - } - _ => None, - } - } - - async fn label_for_symbol( - &self, - name: &str, - kind: SymbolKind, - language: &Arc, - ) -> Option { - let (text, filter_range, display_range) = match kind { - SymbolKind::PROPERTY => { - let text = format!("type t = {{ {}: (); }}", name); - let filter_range: Range = 0..name.len(); - let display_range = 11..11 + name.len(); - (text, filter_range, display_range) - } - SymbolKind::FUNCTION - if name.contains(OPERATOR_CHAR) - || (name.starts_with("let") && name.contains(OPERATOR_CHAR)) => - { - let text = format!("let ({}) () = ()", name); - - let filter_range = 5..5 + name.len(); - let display_range = 0..filter_range.end + 1; - (text, filter_range, display_range) - } - SymbolKind::FUNCTION => { - let text = format!("let {} () = ()", name); - - let filter_range = 4..4 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - SymbolKind::CONSTRUCTOR => { - let text = format!("type t = {}", name); - let filter_range = 0..name.len(); - let display_range = 9..9 + name.len(); - (text, filter_range, display_range) - } - SymbolKind::MODULE => { - let text = format!("module {} = struct end", name); - let filter_range = 7..7 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - SymbolKind::CLASS => { - let text = format!("class {} = object end", name); - let filter_range = 6..6 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - SymbolKind::METHOD => { - let text = format!("class c = object method {} = () end", name); - let filter_range = 0..name.len(); - let display_range = 17..24 + name.len(); - (text, filter_range, display_range) - } - SymbolKind::STRING => { - let text = format!("type {} = T", name); - let filter_range = 5..5 + 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, - }) - } -} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d6e5f625167fc9feb02baa122b87f1c4a6613c07..5e0aed275797a88e55f3bf61b9711996da429fed 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -9904,18 +9904,29 @@ async fn populate_labels_for_symbols( ) { let mut symbols_by_language = HashMap::>, Vec>::default(); + let mut unknown_path = None; for symbol in symbols { let language = language_registry .language_for_file_path(&symbol.path.path) .await - .log_err() - .or_else(|| default_language.clone()); + .ok() + .or_else(|| { + unknown_path.get_or_insert(symbol.path.path.clone()); + default_language.clone() + }); symbols_by_language .entry(language) .or_default() .push(symbol); } + if let Some(unknown_path) = unknown_path { + log::info!( + "no language found for symbol path {}", + unknown_path.display() + ); + } + let mut label_params = Vec::new(); for (language, mut symbols) in symbols_by_language { label_params.clear(); diff --git a/extensions/ocaml/Cargo.toml b/extensions/ocaml/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..d93b988f387d827dde0fa750ceaf219853bf7323 --- /dev/null +++ b/extensions/ocaml/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_ocaml" +version = "0.0.1" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/ocaml.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.0.6" diff --git a/extensions/ocaml/extension.toml b/extensions/ocaml/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..c5af21a055d001483e99744e435cbcd6767db82e --- /dev/null +++ b/extensions/ocaml/extension.toml @@ -0,0 +1,21 @@ +id = "ocaml" +name = "OCaml" +description = "OCaml support." +version = "0.0.1" +schema_version = 1 +authors = ["Rashid Almheiri <69181766+huwaireb@users.noreply.github.com>"] +repository = "https://github.com/zed-industries/zed" + +[language_servers.ocamllsp] +name = "ocamllsp" +languages = ["OCaml", "OCaml Interface"] + +[grammars.ocaml] +repository = "https://github.com/tree-sitter/tree-sitter-ocaml" +commit = "0b12614ded3ec7ed7ab7933a9ba4f695ba4c342e" +path = "grammars/ocaml" + +[grammars.ocaml_interface] +repository = "https://github.com/tree-sitter/tree-sitter-ocaml" +commit = "0b12614ded3ec7ed7ab7933a9ba4f695ba4c342e" +path = "grammars/interface" diff --git a/crates/languages/src/ocaml-interface/brackets.scm b/extensions/ocaml/languages/ocaml-interface/brackets.scm similarity index 100% rename from crates/languages/src/ocaml-interface/brackets.scm rename to extensions/ocaml/languages/ocaml-interface/brackets.scm diff --git a/crates/languages/src/ocaml-interface/config.toml b/extensions/ocaml/languages/ocaml-interface/config.toml similarity index 100% rename from crates/languages/src/ocaml-interface/config.toml rename to extensions/ocaml/languages/ocaml-interface/config.toml diff --git a/crates/languages/src/ocaml-interface/highlights.scm b/extensions/ocaml/languages/ocaml-interface/highlights.scm similarity index 100% rename from crates/languages/src/ocaml-interface/highlights.scm rename to extensions/ocaml/languages/ocaml-interface/highlights.scm diff --git a/crates/languages/src/ocaml-interface/indents.scm b/extensions/ocaml/languages/ocaml-interface/indents.scm similarity index 100% rename from crates/languages/src/ocaml-interface/indents.scm rename to extensions/ocaml/languages/ocaml-interface/indents.scm diff --git a/crates/languages/src/ocaml-interface/outline.scm b/extensions/ocaml/languages/ocaml-interface/outline.scm similarity index 100% rename from crates/languages/src/ocaml-interface/outline.scm rename to extensions/ocaml/languages/ocaml-interface/outline.scm diff --git a/crates/languages/src/ocaml/brackets.scm b/extensions/ocaml/languages/ocaml/brackets.scm similarity index 100% rename from crates/languages/src/ocaml/brackets.scm rename to extensions/ocaml/languages/ocaml/brackets.scm diff --git a/crates/languages/src/ocaml/config.toml b/extensions/ocaml/languages/ocaml/config.toml similarity index 100% rename from crates/languages/src/ocaml/config.toml rename to extensions/ocaml/languages/ocaml/config.toml diff --git a/crates/languages/src/ocaml/highlights.scm b/extensions/ocaml/languages/ocaml/highlights.scm similarity index 100% rename from crates/languages/src/ocaml/highlights.scm rename to extensions/ocaml/languages/ocaml/highlights.scm diff --git a/crates/languages/src/ocaml/indents.scm b/extensions/ocaml/languages/ocaml/indents.scm similarity index 100% rename from crates/languages/src/ocaml/indents.scm rename to extensions/ocaml/languages/ocaml/indents.scm diff --git a/crates/languages/src/ocaml/outline.scm b/extensions/ocaml/languages/ocaml/outline.scm similarity index 100% rename from crates/languages/src/ocaml/outline.scm rename to extensions/ocaml/languages/ocaml/outline.scm diff --git a/extensions/ocaml/src/ocaml.rs b/extensions/ocaml/src/ocaml.rs new file mode 100644 index 0000000000000000000000000000000000000000..6b598730c377bd25b4b43805ac4fc54756be56ea --- /dev/null +++ b/extensions/ocaml/src/ocaml.rs @@ -0,0 +1,219 @@ +use std::ops::Range; +use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind}; +use zed::{CodeLabel, CodeLabelSpan}; +use zed_extension_api::{self as zed, Result}; + +const OPERATOR_CHAR: [char; 17] = [ + '~', '!', '?', '%', '<', ':', '.', '$', '&', '*', '+', '-', '/', '=', '>', '@', '^', +]; + +struct OcamlExtension; + +impl zed::Extension for OcamlExtension { + fn new() -> Self { + Self + } + + fn language_server_command( + &mut self, + _language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + let path = worktree.which("ocamllsp").ok_or_else(|| { + "ocamllsp (ocaml-language-server) must be installed manually.".to_string() + })?; + + Ok(zed::Command { + command: path, + args: Vec::new(), + env: Default::default(), + }) + } + + fn label_for_completion( + &self, + _language_server_id: &zed::LanguageServerId, + completion: Completion, + ) -> Option { + let name = &completion.label; + let detail = completion.detail.as_ref().map(|s| s.replace('\n', " ")); + + match completion.kind.zip(detail) { + Some((CompletionKind::Constructor | CompletionKind::EnumMember, detail)) => { + let (argument, return_t) = detail + .split_once("->") + .map_or((None, detail.as_str()), |(arg, typ)| { + (Some(arg.trim()), typ.trim()) + }); + + let type_decl = "type t = "; + let type_of = argument.map(|_| " of ").unwrap_or_default(); + let argument = argument.unwrap_or_default(); + let terminator = "\n"; + let let_decl = "let _ "; + let let_colon = ": "; + let let_suffix = " = ()"; + let code = format!( + "{type_decl}{name}{type_of}{argument}{terminator}{let_decl}{let_colon}{return_t}{let_suffix}" + ); + + let name_start = type_decl.len(); + let argument_end = name_start + name.len() + type_of.len() + argument.len(); + let colon_start = argument_end + terminator.len() + let_decl.len(); + let return_type_end = code.len() - let_suffix.len(); + Some(CodeLabel { + code, + spans: vec![ + CodeLabelSpan::code_range(name_start..argument_end), + CodeLabelSpan::code_range(colon_start..return_type_end), + ], + filter_range: (0..name.len()).into(), + }) + } + + Some((CompletionKind::Field, detail)) => { + let filter_range_start = if name.starts_with(&['~', '?']) { 1 } else { 0 }; + + let record_prefix = "type t = { "; + let record_suffix = "; }"; + let code = format!("{record_prefix}{name} : {detail}{record_suffix}"); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range( + record_prefix.len()..code.len() - record_suffix.len(), + )], + code, + filter_range: (filter_range_start..name.len()).into(), + }) + } + + Some((CompletionKind::Value, detail)) => { + let let_prefix = "let "; + let suffix = " = ()"; + let (l_paren, r_paren) = if name.contains(OPERATOR_CHAR) { + ("( ", " )") + } else { + ("", "") + }; + let code = format!("{let_prefix}{l_paren}{name}{r_paren} : {detail}{suffix}"); + + let name_start = let_prefix.len() + l_paren.len(); + let name_end = name_start + name.len(); + let type_annotation_start = name_end + r_paren.len(); + let type_annotation_end = code.len() - suffix.len(); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::code_range(name_start..name_end), + CodeLabelSpan::code_range(type_annotation_start..type_annotation_end), + ], + filter_range: (0..name.len()).into(), + code, + }) + } + + Some((CompletionKind::Method, detail)) => { + let method_decl = "class c : object method "; + let end = " end"; + let code = format!("{method_decl}{name} : {detail}{end}"); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range( + method_decl.len()..code.len() - end.len(), + )], + code, + filter_range: (0..name.len()).into(), + }) + } + + Some((kind, _)) => { + let highlight_name = match kind { + CompletionKind::Module | CompletionKind::Interface => "title", + CompletionKind::Keyword => "keyword", + CompletionKind::TypeParameter => "type", + _ => return None, + }; + + Some(CodeLabel { + spans: vec![(CodeLabelSpan::literal(name, Some(highlight_name.to_string())))], + filter_range: (0..name.len()).into(), + code: String::new(), + }) + } + _ => None, + } + } + + fn label_for_symbol( + &self, + _language_server_id: &zed::LanguageServerId, + symbol: Symbol, + ) -> Option { + let name = &symbol.name; + + let (code, filter_range, display_range) = match symbol.kind { + SymbolKind::Property => { + let code = format!("type t = {{ {}: (); }}", name); + let filter_range: Range = 0..name.len(); + let display_range = 11..11 + name.len(); + (code, filter_range, display_range) + } + SymbolKind::Function + if name.contains(OPERATOR_CHAR) + || (name.starts_with("let") && name.contains(OPERATOR_CHAR)) => + { + let code = format!("let ( {name} ) () = ()"); + + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end + 1; + (code, filter_range, display_range) + } + SymbolKind::Function => { + let code = format!("let {name} () = ()"); + + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (code, filter_range, display_range) + } + SymbolKind::Constructor => { + let code = format!("type t = {name}"); + let filter_range = 0..name.len(); + let display_range = 9..9 + name.len(); + (code, filter_range, display_range) + } + SymbolKind::Module => { + let code = format!("module {name} = struct end"); + let filter_range = 7..7 + name.len(); + let display_range = 0..filter_range.end; + (code, filter_range, display_range) + } + SymbolKind::Class => { + let code = format!("class {name} = object end"); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (code, filter_range, display_range) + } + SymbolKind::Method => { + let code = format!("class c = object method {name} = () end"); + let filter_range = 0..name.len(); + let display_range = 17..24 + name.len(); + (code, filter_range, display_range) + } + SymbolKind::String => { + let code = format!("type {name} = T"); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (code, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(display_range)], + filter_range: filter_range.into(), + }) + } +} + +zed::register_extension!(OcamlExtension);