Extract Ocaml language support into an extension (#10450)

Max Brunsfeld and Marshall created

Release Notes:

- Extracted Ocaml language support into an extension

---------

Co-authored-by: Marshall <marshall@zed.dev>

Change summary

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 
extensions/ocaml/languages/ocaml-interface/brackets.scm   |   0 
extensions/ocaml/languages/ocaml-interface/config.toml    |   0 
extensions/ocaml/languages/ocaml-interface/highlights.scm |   0 
extensions/ocaml/languages/ocaml-interface/indents.scm    |   0 
extensions/ocaml/languages/ocaml-interface/outline.scm    |   0 
extensions/ocaml/languages/ocaml/brackets.scm             |   0 
extensions/ocaml/languages/ocaml/config.toml              |   0 
extensions/ocaml/languages/ocaml/highlights.scm           |   0 
extensions/ocaml/languages/ocaml/indents.scm              |   0 
extensions/ocaml/languages/ocaml/outline.scm              |   0 
extensions/ocaml/src/ocaml.rs                             | 219 ++++++
21 files changed, 289 insertions(+), 343 deletions(-)

Detailed changes

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"

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"

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

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<Self>,
-        path: &Path,
-    ) -> impl Future<Output = Result<Arc<Language>>> {
+        path: &'a Path,
+    ) -> impl Future<Output = Result<Arc<Language>>> + '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(

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

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![

crates/languages/src/ocaml.rs 🔗

@@ -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<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(),
-            env: None,
-            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..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/project/src/project.rs 🔗

@@ -9904,18 +9904,29 @@ async fn populate_labels_for_symbols(
 ) {
     let mut symbols_by_language = HashMap::<Option<Arc<Language>>, Vec<CoreSymbol>>::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();

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"

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"

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<zed::Command> {
+        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<CodeLabel> {
+        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<CodeLabel> {
+        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<usize> = 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);