Add OCaml support (#6929)

Rashid Almheiri created

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

<details><summary>Zed</summary>
<img width="1800" alt="Screenshot 2024-02-01 at 03 33 20"
src="https://github.com/zed-industries/zed/assets/69181766/e17c184e-203e-40c3-a08f-4de46226b79c">
</details>

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.

Change summary

Cargo.lock                                              |  10 
Cargo.toml                                              |  17 
assets/icons/file_icons/file_types.json                 |   5 
assets/icons/file_icons/ocaml.svg                       |   2 
crates/lsp/src/lsp.rs                                   |  33 +
crates/zed/Cargo.toml                                   |   1 
crates/zed/src/languages.rs                             |  11 
crates/zed/src/languages/ocaml-interface/brackets.scm   |   6 
crates/zed/src/languages/ocaml-interface/config.toml    |  13 
crates/zed/src/languages/ocaml-interface/highlights.scm |   1 
crates/zed/src/languages/ocaml-interface/indents.scm    |  21 
crates/zed/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, 781 insertions(+), 9 deletions(-)

Detailed changes

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",

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" }

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"
         },

assets/icons/file_icons/ocaml.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.73843 11.709C6.70584 11.6334 6.60683 11.4367 6.55712 11.3736C6.44917 11.2362 6.42396 11.2258 6.39221 11.0523C6.33703 10.7501 6.19094 10.202 6.01879 9.82381C5.92987 9.62863 5.78201 9.46467 5.64665 9.32312C5.52847 9.19895 5.26214 8.99002 5.2157 9.00037C4.7807 9.09487 4.64576 9.55902 4.44115 9.92673C4.32795 10.1301 4.208 10.3031 4.1188 10.5195C4.03641 10.7184 4.04373 10.9387 3.90268 11.1095C3.75801 11.2849 3.66398 11.4715 3.59311 11.6981C3.57968 11.7412 3.54148 12.1939 3.5 12.3006L4.14649 12.2511C4.74896 12.2958 4.57496 12.547 5.51526 12.4922L7 12.4423C6.95398 12.2942 6.89056 12.1228 6.86613 12.067C6.82472 11.9732 6.77267 11.7897 6.73843 11.709Z" fill="black"/>

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?;

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

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

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 },
+]

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

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

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<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "ocamllsp (ocaml-language-server) must be installed manually."
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "ocamllsp".into(),
+            arguments: vec![],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+        language: &Arc<language::Language>,
+    ) -> Option<CodeLabel> {
+        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<usize>, 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<language::Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            SymbolKind::PROPERTY => {
+                let text = format!("type t = {{ {}: (); }}", name);
+                let filter_range: Range<usize> = 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,
+        })
+    }
+}

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)

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 }
+]

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

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

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

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.