;;; lsp-ocaml.el --- description -*- lexical-binding: t; -*-
;; Copyright (C) 2020-2026 emacs-lsp maintainers
;; Author: emacs-lsp maintainers
;; Keywords: lsp, ocaml
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see .
;;; Commentary:
;; LSP Clients for the Ocaml Programming Language.
;;; Code:
(require 'lsp-mode)
(require 'find-file)
(defgroup lsp-ocaml nil
"LSP support for OCaml, using ocaml-language-server."
:group 'lsp-mode
:link '(url-link "https://github.com/freebroccolo/ocaml-language-server"))
(define-obsolete-variable-alias
'lsp-ocaml-ocaml-lang-server-command
'lsp-ocaml-lang-server-command
"lsp-mode 6.1")
(defcustom lsp-ocaml-lang-server-command
'("ocaml-language-server" "--stdio")
"Command to start ocaml-language-server."
:group 'lsp-ocaml
:type '(choice
(string :tag "Single string value")
(repeat :tag "List of string values"
string)))
(lsp-register-client
(make-lsp-client :new-connection (lsp-stdio-connection
(lambda () lsp-ocaml-lang-server-command))
:major-modes '(reason-mode caml-mode neocaml-mode neocaml-interface-mode tuareg-mode)
:priority -1
:server-id 'ocaml-ls))
(defgroup lsp-ocaml-lsp-server nil
"LSP support for OCaml, using ocaml-lsp-server."
:group 'lsp-mode
:link '(url-link "https://github.com/ocaml/ocaml-lsp"))
(define-obsolete-variable-alias 'lsp-merlin 'lsp-ocaml-lsp-server "lsp-mode 6.1")
(define-obsolete-variable-alias 'lsp-merlin-command 'lsp-ocaml-lsp-server-command "lsp-mode 6.1")
;;; -------------------
;;; OCaml-lsp custom variables
;;; -------------------
(defcustom lsp-ocaml-lsp-server-command
'("opam" "exec" "--" "ocamllsp")
"Command to start ocaml-lsp-server."
:group 'lsp-ocaml-lsp-server
:type '(choice
(string :tag "Single string value")
(repeat :tag "List of string values"
string)))
(lsp-register-client
(make-lsp-client
:new-connection
(lsp-stdio-connection (lambda () lsp-ocaml-lsp-server-command))
:major-modes '(reason-mode caml-mode neocaml-mode neocaml-interface-mode tuareg-mode)
:priority 0
:server-id 'ocaml-lsp-server))
(defcustom lsp-cut-signature 'space
"If non-nil, signatures returned on hover will not be split on newline."
:group 'lsp-ocaml-lsp-server
:type '(choice (symbol :tag "Default behaviour" 'cut)
(symbol :tag "Display all the lines with spaces" 'space)))
(defcustom lsp-ocaml-markupkind 'markdown
"Preferred markup format."
:group 'lsp-ocaml-lsp-server
:type '(choice (symbol :tag "Markdown" 'markdown)
(symbol :tag "Plain text" 'plaintext)))
(defcustom lsp-ocaml-enclosing-type-verbosity 1
"Number of expansions of aliases in answers."
:group 'lsp-ocaml-lsp-server
:type 'int)
(defcustom lsp-ocaml-enclosing-type-cycle nil
"When growing up or down the enclosings of a type, cycle when reaching one bound."
:group 'lsp-ocaml-server
:type 'boolean)
;;; -------------------
;;; OCaml-lsp faces
;;; -------------------
(defface lsp-ocaml-highlight-region-face '((t (:inherit region)))
"Face used to highlight a region.")
;;; -------------------
;;; OCaml-lsp extensions interface
;;; -------------------
;;; The following functions are used to create an interface between custom OCaml-lsp requests and lsp-mode
(defun lsp-ocaml--switch-impl-intf ()
"Switch to the file(s) that the current file can switch to.
OCaml-lsp custom protocol documented here
https://github.com/ocaml/ocaml-lsp/blob/master/ocaml-lsp-server/docs/ocamllsp/switchImplIntf-spec.md"
(-if-let* ((params (make-vector 1 (lsp--buffer-uri)))
(uris (lsp-request "ocamllsp/switchImplIntf" params)))
uris
(lsp--warn "Your version of ocaml-lsp doesn't support the switchImplIntf extension")))
(defun lsp-ocaml--type-enclosing (verbosity index)
"Get the type of the identifier at point.
VERBOSITY and INDEX use is described in the OCaml-lsp protocol documented here
https://github.com/ocaml/ocaml-lsp/blob/master/ocaml-lsp-server/docs/ocamllsp/typeEnclosing-spec.md"
(-if-let* ((params (lsp-make-ocaml-lsp-type-enclosing-params
:uri (lsp--buffer-uri)
:at (lsp--cur-position)
:index index
:verbosity verbosity))
(result (lsp-request "ocamllsp/typeEnclosing" params)))
result
(lsp--warn "Your version of ocaml-lsp doesn't support the typeEnclosing extension")))
(defun lsp-ocaml--get-documentation (identifier content-format)
"Get the documentation of IDENTIFIER or the identifier at point if IDENTIFIER is nil.
CONTENT-FORMAT is `Markdown' or `Plaintext'.
OCaml-lsp protocol documented here
https://github.com/ocaml/ocaml-lsp/blob/master/ocaml-lsp-server/docs/ocamllsp/getDocumentation-spec.md"
(-if-let* ((position (if identifier nil (lsp--cur-position)))
((&TextDocumentPositionParams :text-document :position) (lsp--text-document-position-params identifier position))
(params (lsp-make-ocaml-lsp-get-documentation-params
:textDocument text-document
:position position
:contentFormat content-format)))
;; Don't exit if the request returns nil, an identifier can have no documentation
(lsp-request "ocamllsp/getDocumentation" params)
(lsp--warn "Your version of ocaml-lsp doesn't support the getDocumentation extension")))
(defun lsp-ocaml--infer-intf ()
"Infer the interface of the given URI.
The URI should correspond to an implementation file, not an interface one.
OCaml-lsp protocol is documented here:
https://github.com/ocaml/ocaml-lsp/blob/master/ocaml-lsp-server/docs/ocamllsp/inferIntf-spec.md"
(-if-let* ((params (make-vector 1 (lsp--buffer-uri)))
(result (lsp-request "ocamllsp/inferIntf" params)))
result
(lsp--warn "Your version of ocaml-lsp doesn't support the inferIntf extension")))
;;; -------------------
;;; OCaml-lsp general utilities
;;; -------------------
(defun lsp-ocaml--has-one-element-p (lst)
"Return t if LST is a singleton."
(and lst (= (length lst) 1)))
;;; -------------------
;;; OCaml-lsp URI utilities
;;; -------------------
(defun lsp-ocaml--is-interface (uri)
"Return non-nil if the given URI is an interface, nil otherwise."
(let ((path (lsp--uri-to-path uri)))
(string-match-p "\\.\\(mli\\|rei\\|eliomi\\)\\'" path)))
(defun lsp-ocaml--on-interface ()
"Return non-nil if the current URI is an interface, nil otherwise."
(lsp-ocaml--is-interface (lsp--buffer-uri)))
(defun lsp-ocaml--load-uri (uri &optional other-window)
"Check if URI exists and open its buffer or create a new one.
If OTHER-WINDOW is not nil, open the buffer in an other window."
(let ((path (lsp--uri-to-path uri)))
(cond
;; A buffer already exists with PATH
((bufferp (get-file-buffer path))
(ff-switch-to-buffer (get-file-buffer path) other-window)
path)
;; PATH is an existing file
((file-exists-p path)
(ff-find-file path other-window nil)
path)
;; PATH is not an existing file
(t
nil))))
(defun lsp-ocaml--find-alternate-uri ()
"Return the URI corresponding to the alternate file if there's only one or prompt for a choice."
(let ((uris (lsp-ocaml--switch-impl-intf)))
(if (lsp-ocaml--has-one-element-p uris)
(car uris)
(let* ((filenames (mapcar #'f-filename uris))
(selected-file (completing-read "Choose an alternate file " filenames)))
(nth (cl-position selected-file filenames :test #'string=) uris)))))
;;; -------------------
;;; OCaml-lsp type enclosing utilities
;;; ------------------
(defvar-local lsp-ocaml--type-enclosing-verbosity lsp-ocaml-enclosing-type-verbosity)
(defvar-local lsp-ocaml--type-enclosing-index 0)
(defvar-local lsp-ocaml--type-enclosing-saved-type nil)
(defvar-local lsp-ocaml--type-enclosing-type-enclosings nil)
(defun lsp-ocaml--init-type-enclosing-config ()
"Create a new config for the type enclosing requests."
(setq lsp-ocaml--type-enclosing-verbosity lsp-ocaml-enclosing-type-verbosity)
(setq lsp-ocaml--type-enclosing-index 0)
(setq lsp-ocaml--type-enclosing-saved-type nil)
(setq lsp-ocaml--type-enclosing-type-enclosings nil))
(defun lsp-ocaml--highlight-current-type (range)
"Highlight RANGE.
RANGE is (:start (:character .. :line ..)) :end (:character .. :line ..)"
(remove-overlays nil nil 'face 'lsp-ocaml-highlight-region-face)
(let* ((point-min (lsp--position-to-point (cl-getf range :start)))
(point-max (lsp--position-to-point (cl-getf range :end)))
(overlay (make-overlay point-min point-max)))
(overlay-put overlay 'face 'lsp-ocaml-highlight-region-face)
(unwind-protect (sit-for 10) (delete-overlay overlay))))
(defun lsp-ocaml--display-type (markupkind type doc)
"Display TYPE in MARKUPKIND with its DOC attached.
If TYPE is a single-line that represents a module type, reformat it."
(let* (;; Regroup the type and documentation at point
(single-linep (not (string-match-p "\n" type)))
(new-type (if single-linep (string-replace " val " "\n val " type) type))
(new-type (if single-linep (string-replace " end" "\nend" new-type) type))
(contents `(:kind ,markupkind
:value ,(mapconcat #'identity `("```ocaml" ,new-type "```" "***" ,doc) "\n"))))
(lsp--display-contents contents)))
;;; -------------------
;;; OCaml-lsp type enclosing transient map
;;; -------------------
(defvar lsp-ocaml-type-enclosing-map
(let ((keymap (make-sparse-keymap)))
(define-key keymap (kbd "C-") #'lsp-ocaml-type-enclosing-go-up)
(define-key keymap (kbd "C-") #'lsp-ocaml-type-enclosing-go-down)
(define-key keymap (kbd "C-w") #'lsp-ocaml-type-enclosing-copy)
(define-key keymap (kbd "C-t") #'lsp-ocaml-type-enclosing-increase-verbosity)
(define-key keymap (kbd "C-") #'lsp-ocaml-type-enclosing-increase-verbosity)
(define-key keymap (kbd "C-") #'lsp-ocaml-type-enclosing-decrease-verbosity)
keymap)
"Keymap for OCaml-lsp type enclosing transient mode.")
(defun lsp-ocaml-type-enclosing-go-up ()
"Go up the type's enclosing."
(interactive)
(when lsp-ocaml--type-enclosing-type-enclosings
(setq lsp-ocaml--type-enclosing-index
(if lsp-ocaml-enclosing-type-cycle
(mod (1+ lsp-ocaml--type-enclosing-index)
(length lsp-ocaml--type-enclosing-type-enclosings))
(min (1+ lsp-ocaml--type-enclosing-index)
(1- (length lsp-ocaml--type-enclosing-type-enclosings))))))
(lsp-ocaml--get-and-display-type-enclosing))
(defun lsp-ocaml-type-enclosing-go-down ()
"Go down the type's enclosing."
(interactive)
(when lsp-ocaml--type-enclosing-type-enclosings
(setq lsp-ocaml--type-enclosing-index
(if lsp-ocaml-enclosing-type-cycle
(mod (1- lsp-ocaml--type-enclosing-index)
(length lsp-ocaml--type-enclosing-type-enclosings))
(max (1- lsp-ocaml--type-enclosing-index) 0))))
(lsp-ocaml--get-and-display-type-enclosing))
(defun lsp-ocaml-type-enclosing-decrease-verbosity ()
"Decreases the number of expansions of aliases in answer."
(interactive)
(let ((verbosity (max 0 (1- lsp-ocaml--type-enclosing-verbosity))))
(setq lsp-ocaml--type-enclosing-verbosity verbosity))
(lsp-ocaml--get-and-display-type-enclosing))
(defun lsp-ocaml-type-enclosing-increase-verbosity ()
"Increases the number of expansions of aliases in answer."
(interactive)
(let ((verbosity (1+ lsp-ocaml--type-enclosing-verbosity)))
(setq lsp-ocaml--type-enclosing-verbosity verbosity))
(lsp-ocaml--get-and-display-type-enclosing t))
(defun lsp-ocaml-type-enclosing-copy ()
"Copy the type of the saved enclosing type to the `kill-ring'."
(interactive)
(when lsp-ocaml--type-enclosing-saved-type
(message "Copied `%s' to kill-ring"
lsp-ocaml--type-enclosing-saved-type)
(kill-new lsp-ocaml--type-enclosing-saved-type)))
(defun lsp-ocaml--get-and-display-type-enclosing (&optional increased-verbosity)
"Compute the type enclosing request.
If INCREASED-VERBOSITY is t, if the computed type is the same as the previous
one, decrease the verbosity.
This allows to make sure that we don't increase infinitely the verbosity."
(-let* ((verbosity lsp-ocaml--type-enclosing-verbosity)
(index lsp-ocaml--type-enclosing-index)
(type_result (lsp-ocaml--type-enclosing verbosity index))
((&ocaml-lsp:TypeEnclosingResult :index :type :enclosings) type_result)
;; Get documentation information
(markupkind (symbol-name lsp-ocaml-markupkind))
(doc_result (lsp-ocaml--get-documentation nil markupkind))
(doc (cl-getf (cl-getf doc_result :doc) :value)))
(when (and increased-verbosity
(string= type lsp-ocaml--type-enclosing-saved-type))
(setq lsp-ocaml--type-enclosing-verbosity (1- verbosity)))
(setq lsp-ocaml--type-enclosing-saved-type type)
(setq lsp-ocaml--type-enclosing-type-enclosings enclosings)
(lsp-ocaml--display-type markupkind type doc)
(lsp-ocaml--highlight-current-type (aref enclosings index))
type))
;;; -------------------
;;; OCaml-lsp extensions
;;; -------------------
;;; The following functions are interactive implementations of the OCaml-lsp requests
(defun lsp-ocaml-infer-interface ()
"Infer the interface for the current file."
(interactive)
(let* ((current-uri (lsp--buffer-uri))
(intf-uri (if (lsp-ocaml--is-interface current-uri)
current-uri
(lsp-ocaml--find-alternate-uri)))
(impl-uri (if (lsp-ocaml--is-interface current-uri)
(lsp-ocaml--find-alternate-uri)
current-uri))
(intf-path (lsp--uri-to-path intf-uri))
(impl-path (lsp--uri-to-path impl-uri)))
(if (lsp-ocaml--load-uri impl-uri) ; the impl file needs to be loaded
(when (y-or-n-p
(format "Try to generate an interface for %s? " impl-path))
(let ((result (lsp-ocaml--infer-intf)))
(with-current-buffer (get-buffer-create intf-path)
(when (or (= (buffer-size) 0)
(y-or-n-p "The buffer is not empty, overwrite it? "))
(erase-buffer)
(insert result)
;; Create the file if it doesn’t exist
(unless (file-exists-p intf-path)
(write-file intf-path)))))))))
(defun lsp-ocaml-find-alternate-file ()
"Return the URI corresponding to the alternate file if there's only one or prompt for a choice."
(interactive)
(let ((uri (lsp-ocaml--find-alternate-uri)))
(unless (lsp-ocaml--load-uri uri nil)
(message "No alternate file %s could be found for %s" (f-filename uri) (buffer-name)))))
(defun lsp-ocaml-type-enclosing ()
"Returns the type of the indent at point."
(interactive)
(lsp-ocaml--init-type-enclosing-config)
(when-let* ((type (lsp-ocaml--get-and-display-type-enclosing)))
(set-transient-map lsp-ocaml-type-enclosing-map t)))
(lsp-consistency-check lsp-ocaml)
(provide 'lsp-ocaml)
;;; lsp-ocaml.el ends here