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