393 lines
16 KiB
EmacsLisp
393 lines
16 KiB
EmacsLisp
;;; 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 <https://www.gnu.org/licenses/>.
|
||
|
||
;;; 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-<up>") #'lsp-ocaml-type-enclosing-go-up)
|
||
(define-key keymap (kbd "C-<down>") #'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-<right>") #'lsp-ocaml-type-enclosing-increase-verbosity)
|
||
(define-key keymap (kbd "C-<left>") #'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
|