From 998f6cf80d3de4c289869edfa20e847605465776 Mon Sep 17 00:00:00 2001 From: Rashid Almheiri <69181766+huwaireb@users.noreply.github.com> Date: Fri, 2 Feb 2024 18:58:07 +0400 Subject: [PATCH] Add OCaml support (#6929) This pull request implements support for the [OCaml Language](https://ocaml.org/). ### Additions - [x] [tree-sitter-ocaml](https://github.com/tree-sitter/tree-sitter-ocaml) grammar - [x] Highlight, Indents, Outline queries - [x] A new file icon for .ml(i) files. Based on [ocaml/ocaml-logo](https://github.com/ocaml/ocaml-logo/blob/master/Colour/SVG/colour-transparent-icon.svg) - [x] LSP Integration with [ocaml-language-server](https://github.com/ocaml/ocaml-lsp) - [x] Completion Labels - [x] Symbol Labels ### Bug Fixes - [x] Improper parsing of LSP headers. ### Missing [will file a separate issue] - Documentation on completionItem, requires: `completionItem/resolve` with support for `documentation` as a provider. ### Screenshots
Zed Screenshot 2024-02-01 at 03 33 20
Release Notes: - Added OCaml Support ([#5316](https://github.com/zed-industries/zed/issues/5316)). > [!NOTE] > Partially completes #5316 > To complete #5316: > 1. addition of a reason tree-sitter grammar. > 2. opam/esy integration, however it may be better as it's own plugin. --- Cargo.lock | 10 + Cargo.toml | 17 +- assets/icons/file_icons/file_types.json | 5 + assets/icons/file_icons/ocaml.svg | 5 + crates/lsp/src/lsp.rs | 33 +- crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 11 + .../languages/ocaml-interface/brackets.scm | 6 + .../src/languages/ocaml-interface/config.toml | 13 + .../languages/ocaml-interface/highlights.scm | 1 + .../src/languages/ocaml-interface/indents.scm | 21 ++ .../src/languages/ocaml-interface/outline.scm | 48 +++ crates/zed/src/languages/ocaml.rs | 317 ++++++++++++++++++ crates/zed/src/languages/ocaml/brackets.scm | 12 + crates/zed/src/languages/ocaml/config.toml | 18 + crates/zed/src/languages/ocaml/highlights.scm | 142 ++++++++ crates/zed/src/languages/ocaml/indents.scm | 43 +++ crates/zed/src/languages/ocaml/outline.scm | 59 ++++ docs/src/languages/ocaml.md | 31 ++ 19 files changed, 784 insertions(+), 9 deletions(-) create mode 100644 assets/icons/file_icons/ocaml.svg create mode 100644 crates/zed/src/languages/ocaml-interface/brackets.scm create mode 100644 crates/zed/src/languages/ocaml-interface/config.toml create mode 120000 crates/zed/src/languages/ocaml-interface/highlights.scm create mode 100644 crates/zed/src/languages/ocaml-interface/indents.scm create mode 100644 crates/zed/src/languages/ocaml-interface/outline.scm create mode 100644 crates/zed/src/languages/ocaml.rs create mode 100644 crates/zed/src/languages/ocaml/brackets.scm create mode 100644 crates/zed/src/languages/ocaml/config.toml create mode 100644 crates/zed/src/languages/ocaml/highlights.scm create mode 100644 crates/zed/src/languages/ocaml/indents.scm create mode 100644 crates/zed/src/languages/ocaml/outline.scm create mode 100644 docs/src/languages/ocaml.md diff --git a/Cargo.lock b/Cargo.lock index 4ae19e764c..5d735cac63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8960,6 +8960,15 @@ 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-php" version = "0.21.1" @@ -10418,6 +10427,7 @@ dependencies = [ "tree-sitter-markdown", "tree-sitter-nix", "tree-sitter-nu", + "tree-sitter-ocaml", "tree-sitter-php", "tree-sitter-proto", "tree-sitter-purescript", diff --git a/Cargo.toml b/Cargo.toml index f46d32a53c..5a276ef8b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,11 +100,14 @@ ctor = "0.2.6" derive_more = "0.99.17" env_logger = "0.9" futures = "0.3" -git2 = { version = "0.15", default-features = false} +git2 = { version = "0.15", default-features = false } globset = "0.4" indoc = "1" # We explicitly disable a http2 support in isahc. -isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] } +isahc = { version = "1.7.2", default-features = false, features = [ + "static-curl", + "text-decoding", +] } lazy_static = "1.4.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = "2.1.1" @@ -122,7 +125,10 @@ schemars = "0.8" serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } -serde_json_lenient = { version = "0.1", features = ["preserve_order", "raw_value"] } +serde_json_lenient = { version = "0.1", features = [ + "preserve_order", + "raw_value", +] } serde_repr = "0.1" smallvec = { version = "1.6", features = ["union"] } smol = "1.2" @@ -137,7 +143,7 @@ tree-sitter = { version = "0.20", features = ["wasm"] } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } tree-sitter-c = "0.20.1" tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "dd5e59721a5f8dae34604060833902b882023aaf" } -tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } +tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" } tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" } @@ -157,8 +163,9 @@ tree-sitter-lua = "0.0.14" tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa" } +tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" } tree-sitter-php = "0.21.1" -tree-sitter-proto = {git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" } +tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" } tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" } tree-sitter-python = "0.20.2" tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" } diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 8cf7b314c9..29967deb8d 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -69,6 +69,8 @@ "mdb": "storage", "mdf": "storage", "mdx": "document", + "ml": "ocaml", + "mli": "ocaml", "mp3": "audio", "mp4": "video", "myd": "storage", @@ -179,6 +181,9 @@ "log": { "icon": "icons/file_icons/info.svg" }, + "ocaml": { + "icon": "icons/file_icons/ocaml.svg" + }, "phoenix": { "icon": "icons/file_icons/phoenix.svg" }, diff --git a/assets/icons/file_icons/ocaml.svg b/assets/icons/file_icons/ocaml.svg new file mode 100644 index 0000000000..457844f49a --- /dev/null +++ b/assets/icons/file_icons/ocaml.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index f9cd138217..ee5fb3a1d2 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -332,11 +332,36 @@ impl LanguageServer { }; let header = std::str::from_utf8(&buffer)?; - let message_len: usize = header + let mut segments = header.lines(); + + let message_len: usize = segments + .next() + .context("unable to find the first line of the LSP message header")? .strip_prefix(CONTENT_LEN_HEADER) - .ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))? - .trim_end() - .parse()?; + .context("invalid LSP message header")? + .parse() + .with_context(|| { + format!( + "failed to parse Content-Length of LSP message header: {}", + header + ) + })?; + + if let Some(second_segment) = segments.next() { + match second_segment { + "" => (), // Header end + header_field if header_field.starts_with("Content-Type:") => { + stdout.read_until(b'\n', &mut buffer).await?; + } + _ => { + anyhow::bail!( + "expected a Content-Type header field or a header ending CRLF, got {second_segment:?}" + ); + } + } + } else { + anyhow::bail!("unable to find the second line of the LSP message header"); + } buffer.resize(message_len, 0); stdout.read_exact(&mut buffer).await?; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5b202aef43..61df53a9fb 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -130,6 +130,7 @@ tree-sitter-lua.workspace = true tree-sitter-markdown.workspace = true tree-sitter-nix.workspace = true tree-sitter-nu.workspace = true +tree-sitter-ocaml.workspace = true tree-sitter-php.workspace = true tree-sitter-proto.workspace = true tree-sitter-purescript.workspace = true diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index add9f9c192..15bd180a2e 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -25,6 +25,7 @@ mod json; mod language_plugin; mod lua; mod nu; +mod ocaml; mod php; mod purescript; mod python; @@ -299,6 +300,16 @@ pub fn init( tree_sitter_nu::language(), vec![Arc::new(nu::NuLanguageServer {})], ); + language( + "ocaml", + tree_sitter_ocaml::language_ocaml(), + vec![Arc::new(ocaml::OCamlLspAdapter)], + ); + language( + "ocaml-interface", + tree_sitter_ocaml::language_ocaml_interface(), + vec![Arc::new(ocaml::OCamlLspAdapter)], + ); language( "vue", tree_sitter_vue::language(), diff --git a/crates/zed/src/languages/ocaml-interface/brackets.scm b/crates/zed/src/languages/ocaml-interface/brackets.scm new file mode 100644 index 0000000000..0929a696fd --- /dev/null +++ b/crates/zed/src/languages/ocaml-interface/brackets.scm @@ -0,0 +1,6 @@ +("(" @open ")" @close) +("{" @open "}" @close) +("<" @open ">" @close) + +("sig" @open "end" @close) +("object" @open "end" @close) diff --git a/crates/zed/src/languages/ocaml-interface/config.toml b/crates/zed/src/languages/ocaml-interface/config.toml new file mode 100644 index 0000000000..f7401f774c --- /dev/null +++ b/crates/zed/src/languages/ocaml-interface/config.toml @@ -0,0 +1,13 @@ +name = "OCaml Interface" +path_suffixes = ["mli"] +block_comment = ["(* ", "*)"] +autoclose_before = ";,=)}" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "<", end = ">", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "sig", end = " end", close = true, newline = true }, + # HACK: For some reason `object` alone does not work + { start = "object ", end = "end", close = true, newline = true }, +] diff --git a/crates/zed/src/languages/ocaml-interface/highlights.scm b/crates/zed/src/languages/ocaml-interface/highlights.scm new file mode 120000 index 0000000000..e6f0d00d1d --- /dev/null +++ b/crates/zed/src/languages/ocaml-interface/highlights.scm @@ -0,0 +1 @@ +../ocaml/highlights.scm \ No newline at end of file diff --git a/crates/zed/src/languages/ocaml-interface/indents.scm b/crates/zed/src/languages/ocaml-interface/indents.scm new file mode 100644 index 0000000000..0de50a48bb --- /dev/null +++ b/crates/zed/src/languages/ocaml-interface/indents.scm @@ -0,0 +1,21 @@ +[ + (type_binding) + + (value_specification) + (method_specification) + + (external) + (field_declaration) +] @indent + +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent + +(_ "object" @start "end" @end) @indent + +(signature + "sig" @start + "end" @end) @indent + +";;" @outdent diff --git a/crates/zed/src/languages/ocaml-interface/outline.scm b/crates/zed/src/languages/ocaml-interface/outline.scm new file mode 100644 index 0000000000..b8539d4cd0 --- /dev/null +++ b/crates/zed/src/languages/ocaml-interface/outline.scm @@ -0,0 +1,48 @@ +(module_type_definition + "module" @context + "type" @context + name: (_) @name) @item + +(module_definition + "module" @context + (module_binding name: (_) @name)) @item + +(type_definition + "type" @context + (type_binding name: (_) @name)) @item + +(class_definition + "class" @context + (class_binding + "virtual"? @context + name: (_) @name)) @item + +(class_type_definition + "class" @context + "type" @context + (class_type_binding + "virtual"? @context + name: (_) @name)) @item + +(instance_variable_definition + "val" @context + "method"? @context + name: (_) @name) @item + +(method_specification + "method" @context + "virtual"? @context + (method_name) @name) @item + +(value_specification + "val" @context + (value_name) @name) @item + +(external + "external" @context + (value_name) @name) @item + +(exception_definition + "exception" @context + (constructor_declaration + (constructor_name) @name)) @item diff --git a/crates/zed/src/languages/ocaml.rs b/crates/zed/src/languages/ocaml.rs new file mode 100644 index 0000000000..9878b89e33 --- /dev/null +++ b/crates/zed/src/languages/ocaml.rs @@ -0,0 +1,317 @@ +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] +impl LspAdapter for OCamlLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("ocamllsp".into()) + } + + fn short_name(&self) -> &'static str { + "ocaml" + } + + 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(), + 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..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/zed/src/languages/ocaml/brackets.scm b/crates/zed/src/languages/ocaml/brackets.scm new file mode 100644 index 0000000000..8aa7be2eaf --- /dev/null +++ b/crates/zed/src/languages/ocaml/brackets.scm @@ -0,0 +1,12 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("[|" @open "|]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) + +("begin" @open "end" @close) +("struct" @open "end" @close) +("sig" @open "end" @close) +("object" @open "end" @close) +("do" @open "done" @close) diff --git a/crates/zed/src/languages/ocaml/config.toml b/crates/zed/src/languages/ocaml/config.toml new file mode 100644 index 0000000000..522db6dae1 --- /dev/null +++ b/crates/zed/src/languages/ocaml/config.toml @@ -0,0 +1,18 @@ +name = "OCaml" +path_suffixes = ["ml"] +block_comment = ["(* ", "*)"] +autoclose_before = ";,=)}]" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "<", end = ">", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "[|", end = "|", close = true, newline = true, not_in = ["string"] }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "begin", end = " end", close = true, newline = true }, + { start = "struct", end = " end", close = true, newline = true }, + { start = "sig", end = " end", close = true, newline = true }, + # HACK: For some reason `object` alone does not work + { start = "object ", end = "end", close = true, newline = true }, + { start = "do", end = " done", close = true, newline = true } +] diff --git a/crates/zed/src/languages/ocaml/highlights.scm b/crates/zed/src/languages/ocaml/highlights.scm new file mode 100644 index 0000000000..e5125b912e --- /dev/null +++ b/crates/zed/src/languages/ocaml/highlights.scm @@ -0,0 +1,142 @@ +; Modules +;-------- + +[(module_name) (module_type_name)] @title + +; Types +;------ + +[(class_name) (class_type_name) (type_constructor)] @type + +[(constructor_name) (tag)] @constructor + +; Functions +;---------- + +(let_binding + pattern: (value_name) @function + (parameter)) + +(let_binding + pattern: (value_name) @function + body: [(fun_expression) (function_expression)]) + +(value_specification (value_name) @function) + +(external (value_name) @function) + +(method_name) @function + +(infix_expression + left: (value_path (value_name) @function) + operator: (concat_operator) @operator + (#eq? @operator "@@")) + +(infix_expression + operator: (rel_operator) @operator + right: (value_path (value_name) @function) + (#eq? @operator "|>")) + +(application_expression + function: (value_path (value_name) @function)) + +; Variables +;---------- + +[(type_variable) (value_pattern)] @variable + +; Properties +;----------- + +[(label_name) (field_name) (instance_variable_name)] @property + +; Constants +;---------- + +(boolean) @boolean + +[(number) (signed_number)] @number + +[(string) (character)] @string + +(quoted_string "{" @string "}" @string) @string +(quoted_string_content) @string + + +(escape_sequence) @string.escape + +[ + (conversion_specification) + (pretty_printing_indication) +] @punctuation.special + +; Operators +;---------- + +(match_expression (match_operator) @keyword) + +(value_definition [(let_operator) (let_and_operator)] @keyword) + +[ + (prefix_operator) + (sign_operator) + (pow_operator) + (mult_operator) + (add_operator) + (concat_operator) + (rel_operator) + (and_operator) + (or_operator) + (assign_operator) + (hash_operator) + (indexing_operator) + (let_operator) + (let_and_operator) + (match_operator) +] @operator + +["*" "#" "::" "<-"] @operator + +; Keywords +;--------- + +[ + "and" "as" "assert" "begin" "class" "constraint" "do" "done" "downto" "else" + "end" "exception" "external" "for" "fun" "function" "functor" "if" "in" + "include" "inherit" "initializer" "lazy" "let" "match" "method" "module" + "mutable" "new" "nonrec" "object" "of" "open" "private" "rec" "sig" "struct" + "then" "to" "try" "type" "val" "virtual" "when" "while" "with" +] @keyword + +; Punctuation +;------------ + +["(" ")" "[" "]" "{" "}" "[|" "|]" "[<" "[>"] @punctuation.bracket + +(object_type ["<" ">"] @punctuation.bracket) + +[ + "," "." ";" ":" "=" "|" "~" "?" "+" "-" "!" ">" "&" + "->" ";;" ":>" "+=" ":=" ".." +] @punctuation.delimiter + +; Attributes +;----------- + +[ + (attribute) + (item_attribute) + (floating_attribute) + (extension) + (item_extension) + (quoted_extension) + (quoted_item_extension) + "%" +] @attribute + +(attribute_id) @tag + +; Comments +;--------- + +[(comment) (line_number_directive) (directive) (shebang)] @comment diff --git a/crates/zed/src/languages/ocaml/indents.scm b/crates/zed/src/languages/ocaml/indents.scm new file mode 100644 index 0000000000..807495dae1 --- /dev/null +++ b/crates/zed/src/languages/ocaml/indents.scm @@ -0,0 +1,43 @@ +[ + (let_binding) + (type_binding) + + (method_definition) + + (external) + (value_specification) + (method_specification) + + (match_case) + + (function_expression) + + (field_declaration) + (field_expression) +] @indent + +(_ "[" "]" @end) @indent +(_ "[|" "|]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent + +(_ "object" @start "end" @end) @indent + +(structure + "struct" @start + "end" @end) @indent + +(signature + "sig" @start + "end" @end) @indent + +(parenthesized_expression + "begin" @start + "end") @indent + +(do_clause + "do" @start + "done" @end) @indent + +";;" @outdent diff --git a/crates/zed/src/languages/ocaml/outline.scm b/crates/zed/src/languages/ocaml/outline.scm new file mode 100644 index 0000000000..16f449664a --- /dev/null +++ b/crates/zed/src/languages/ocaml/outline.scm @@ -0,0 +1,59 @@ +(_structure_item/value_definition + "let" @context + (let_binding + pattern: (_) @name)) @item + +(_structure_item/exception_definition + "exception" @context + (constructor_declaration + (constructor_name) @name)) @item + +(_structure_item/module_definition + "module" @context + (module_binding + name: (module_name) @name)) @item + +(module_type_definition + "module" @context + "type" @context + name: (_) @name) @item + +(type_definition + "type" @context + (type_binding name: (_) @name)) @item + +(value_specification + "val" @context + (value_name) @name) @item + +(class_definition + "class" @context + (class_binding + "virtual"? @context + name: (_) @name)) @item + +(class_type_definition + "class" @context + "type" @context + (class_type_binding + "virtual"? @context + name: (_) @name)) @item + +(instance_variable_definition + "val" @context + "method"? @context + name: (_) @name) @item + +(method_specification + "method" @context + "virtual"? @context + (method_name) @name) @item + +(method_definition + "method" @context + "virtual"? @context + name: (_) @name) @item + +(external + "external" @context + (value_name) @name) @item diff --git a/docs/src/languages/ocaml.md b/docs/src/languages/ocaml.md new file mode 100644 index 0000000000..553b5dbdd3 --- /dev/null +++ b/docs/src/languages/ocaml.md @@ -0,0 +1,31 @@ +# OCaml + +- Tree Sitter: [tree-sitter-ocaml](https://github.com/tree-sitter/tree-sitter-ocaml) +- Language Server: [ocamllsp](https://github.com/ocaml/ocaml-lsp) + +## Setup Instructions +If you have the development environment already setup, you can skip to [Launching Zed](#launching-zed) + +### Using OPAM +Opam is the official package manager for OCaml and is highly recommended for getting started with OCaml. To get started using Opam, please follow the instructions provided [here](https://opam.ocaml.org/doc/Install.html). + +Once you install opam and setup a switch with your development environment as per the instructions, you can proceed. + +### Launching Zed +By now you should have `ocamllsp` installed, you can verify so by running + +```sh +$ ocamllsp --help +``` + +in your terminal. If you get a help message, you're good to go. If not, please revisit the installation instructions for `ocamllsp` and ensure it's properly installed. + +With that aside, we can now launch Zed. Given how the OCaml package manager works, we require you to run Zed from the terminal, so please make sure you install the [Zed cli](https://zed.dev/features#cli) if you haven't already. + +Once you have the cli, simply from a terminal, navigate to your project and run + +```sh +$ zed . +``` + +Voila! You should have Zed running with OCaml support, no additional setup required.