Skip to content

Commit 7c0df12

Browse files
authored
OCaml: Add type enclosing (#4741)
* OCaml: Add type enclosing and documentation This also creates a transient keymap allowing to: - increase/decrease the verbosity of the displayed type - copy the computed type - Increase/Decrease index - Show the region that is currently being typed * OCaml: Remove previous function that allowed to retrieve only the signature This function was useful because textDocument/hover returned the type and documentation of the identifier at point making it hard to kill the type only Now that ocamllsp/typeEnclosing is used instead there is no reason to keep this poorly written function (I was young and innocent) * OCaml: Add information in the manual language docs for OCaml CHANGELOG updated
1 parent 2f98fe1 commit 7c0df12

File tree

5 files changed

+231
-59
lines changed

5 files changed

+231
-59
lines changed

CHANGELOG.org

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* Add support for [[https://github.com/c3lang/c3c][c3 language]] (requires [[https://github.com/c3lang/c3-ts-mode][c3-ts-mode]] and [[https://github.com/pherrymason/c3-lsp][c3lsp]]).
3636
* Drop support for emacs 27.1 and 27.2
3737
* Add ~lsp-nix-nixd-server-arguments~ to allow passing arguments to the ~nixd~ LSP.
38+
* Improve the lsp-ocaml client (see [[https://github.com/emacs-lsp/lsp-mode/issues/4731][#4731]] for the follow-up issue. MRs: [[https://github.com/emacs-lsp/lsp-mode/pull/4741][#4741]], [[https://github.com/emacs-lsp/lsp-mode/pull/4732][#4732]])
3839

3940
** 9.0.0
4041
* Add language server config for QML (Qt Modeling Language) using qmlls.

clients/lsp-ocaml.el

Lines changed: 182 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -88,51 +88,28 @@
8888
:type '(choice (symbol :tag "Default behaviour" 'cut)
8989
(symbol :tag "Display all the lines with spaces" 'space)))
9090

91-
(cl-defmethod lsp-clients-extract-signature-on-hover (contents (_server-id (eql ocaml-lsp-server)) &optional storable)
92-
"Extract a representative line from OCaml's CONTENTS, to show in the echo area.
93-
This function splits the content between the signature
94-
and the documentation to display the signature
95-
and truncate it if it's too wide.
96-
The STORABLE argument is used if you want to use this
97-
function to get the type and, for example, kill and yank it.
98-
99-
An example of function using STORABLE is:
100-
101-
(defun mdrp/lsp-get-type-and-kill ()
102-
(interactive)
103-
(let ((contents (-some->> (lsp--text-document-position-params)
104-
(lsp--make-request \"textDocument/hover\")
105-
(lsp--send-request)
106-
(lsp:hover-contents))))
107-
(let ((contents (and contents
108-
(lsp--render-on-hover-content
109-
contents
110-
t))))
111-
(let ((contents
112-
(pcase (lsp-workspaces)
113-
(`(,workspace)
114-
(lsp-clients-extract-signature-on-hover
115-
contents
116-
(lsp--workspace-server-id workspace)
117-
t))
118-
(lsp-clients-extract-signature-on-hover
119-
contents
120-
nil)
121-
)))
122-
(message \"Copied %s to kill-ring\" contents)
123-
(kill-new contents)))))"
124-
(let ((type (s-trim (lsp--render-element (lsp-make-marked-string
125-
:language "ocaml"
126-
:value (car (s-split "---" (lsp--render-element contents))))))))
127-
(if (equal nil storable)
128-
(if (eq lsp-cut-signature 'cut)
129-
(car (s-lines type))
130-
;; else lsp-cut-signature is 'space
131-
(let ((ntype (s-replace "\n" " " type)))
132-
(if (>= (length ntype) (frame-width))
133-
(concat (substring ntype 0 (- (frame-width) 4)) "...")
134-
ntype)))
135-
type)))
91+
(defcustom lsp-ocaml-markupkind 'markdown
92+
"Preferred markup format."
93+
:group 'lsp-ocaml-lsp-server
94+
:type '(choice (symbol :tag "Markdown" 'markdown)
95+
(symbol :tag "Plain text" 'plaintext)))
96+
97+
(defcustom lsp-ocaml-enclosing-type-verbosity 1
98+
"Number of expansions of aliases in answers."
99+
:group 'lsp-ocaml-lsp-server
100+
:type 'int)
101+
102+
(defcustom lsp-ocaml-enclosing-type-cycle nil
103+
"When growing up or down the enclosings of a type, cycle when reaching one bound."
104+
:group 'lsp-ocaml-server
105+
:type 'boolean)
106+
107+
;;; -------------------
108+
;;; OCaml-lsp faces
109+
;;; -------------------
110+
111+
(defface lsp-ocaml-highlight-region-face '((t (:inherit region)))
112+
"Face used to highlight a region.")
136113

137114
;;; -------------------
138115
;;; OCaml-lsp extensions interface
@@ -151,12 +128,42 @@ https://github.com/ocaml/ocaml-lsp/blob/master/ocaml-lsp-server/docs/ocamllsp/sw
151128
uris
152129
(lsp--warn "Your version of ocaml-lsp doesn't support the switchImplIntf extension")))
153130

131+
(defun lsp-ocaml--type-enclosing (verbosity index)
132+
"Get the type of the identifier at point.
133+
134+
VERBOSITY and INDEX use is described in the OCaml-lsp protocol documented here
135+
https://github.com/ocaml/ocaml-lsp/blob/master/ocaml-lsp-server/docs/ocamllsp/typeEnclosing-spec.md"
136+
(-if-let* ((params (lsp-make-ocaml-lsp-type-enclosing-params
137+
:uri (lsp--buffer-uri)
138+
:at (lsp--cur-position)
139+
:index index
140+
:verbosity verbosity))
141+
(result (lsp-request "ocamllsp/typeEnclosing" params)))
142+
result
143+
(lsp--warn "Your version of ocaml-lsp doesn't support the typeEnclosing extension")))
144+
145+
(defun lsp-ocaml--get-documentation (identifier content-format)
146+
"Get the documentation of IDENTIFIER or the identifier at point if IDENTIFIER is nil.
147+
148+
CONTENT-FORMAT is `Markdown' or `Plaintext'.
149+
OCaml-lsp protocol documented here
150+
https://github.com/ocaml/ocaml-lsp/blob/master/ocaml-lsp-server/docs/ocamllsp/getDocumentation-spec.md"
151+
(-if-let* ((position (if identifier nil (lsp--cur-position)))
152+
((&TextDocumentPositionParams :text-document :position) (lsp--text-document-position-params identifier position))
153+
(params (lsp-make-ocaml-lsp-get-documentation-params
154+
:textDocument text-document
155+
:position position
156+
:contentFormat content-format)))
157+
;; Don't exit if the request returns nil, an identifier can have no documentation
158+
(lsp-request "ocamllsp/getDocumentation" params)
159+
(lsp--warn "Your version of ocaml-lsp doesn't support the getDocumentation extension")))
160+
154161
;;; -------------------
155162
;;; OCaml-lsp general utilities
156163
;;; -------------------
157164

158165
(defun lsp-ocaml--has-one-element-p (lst)
159-
"Returns t if LST contains only one element."
166+
"Return t if LST is a singleton."
160167
(and lst (= (length lst) 1)))
161168

162169
;;; -------------------
@@ -193,6 +200,128 @@ If OTHER-WINDOW is not nil, open the buffer in an other window."
193200
(selected-file (completing-read "Choose an alternate file " filenames)))
194201
(nth (cl-position selected-file filenames :test #'string=) uris)))))
195202

203+
;;; -------------------
204+
;;; OCaml-lsp type enclosing utilities
205+
;;; ------------------
206+
207+
(defvar-local lsp-ocaml--type-enclosing-verbosity lsp-ocaml-enclosing-type-verbosity)
208+
(defvar-local lsp-ocaml--type-enclosing-index 0)
209+
(defvar-local lsp-ocaml--type-enclosing-saved-type nil)
210+
(defvar-local lsp-ocaml--type-enclosing-type-enclosings nil)
211+
212+
(defun lsp-ocaml--init-type-enclosing-config ()
213+
"Create a new config for the type enclosing requests."
214+
(setq lsp-ocaml--type-enclosing-verbosity lsp-ocaml-enclosing-type-verbosity)
215+
(setq lsp-ocaml--type-enclosing-index 0)
216+
(setq lsp-ocaml--type-enclosing-saved-type nil)
217+
(setq lsp-ocaml--type-enclosing-type-enclosings nil))
218+
219+
(defun lsp-ocaml--highlight-current-type (range)
220+
"Highlight RANGE.
221+
222+
RANGE is (:start (:character .. :line ..)) :end (:character .. :line ..)"
223+
(remove-overlays nil nil 'face 'lsp-ocaml-highlight-region-face)
224+
(let* ((point-min (lsp--position-to-point (cl-getf range :start)))
225+
(point-max (lsp--position-to-point (cl-getf range :end)))
226+
(overlay (make-overlay point-min point-max)))
227+
(overlay-put overlay 'face 'lsp-ocaml-highlight-region-face)
228+
(unwind-protect (sit-for 10) (delete-overlay overlay))))
229+
230+
(defun lsp-ocaml--display-type (markupkind type doc)
231+
"Display TYPE in MARKUPKIND with its DOC attached.
232+
233+
If TYPE is a single-line that represents a module type, reformat it."
234+
(let* (;; Regroup the type and documentation at point
235+
(single-linep (not (string-match-p "\n" type)))
236+
(new-type (if single-linep (string-replace " val " "\n val " type) type))
237+
(new-type (if single-linep (string-replace " end" "\nend" new-type) type))
238+
(contents `(:kind ,markupkind
239+
:value ,(mapconcat #'identity `("```ocaml" ,new-type "```" "***" ,doc) "\n"))))
240+
(lsp--display-contents contents)))
241+
242+
;;; -------------------
243+
;;; OCaml-lsp type enclosing transient map
244+
;;; -------------------
245+
246+
(defvar lsp-ocaml-type-enclosing-map
247+
(let ((keymap (make-sparse-keymap)))
248+
(define-key keymap (kbd "C-<up>") #'lsp-ocaml-type-enclosing-go-up)
249+
(define-key keymap (kbd "C-<down>") #'lsp-ocaml-type-enclosing-go-down)
250+
(define-key keymap (kbd "C-w") #'lsp-ocaml-type-enclosing-copy)
251+
(define-key keymap (kbd "C-t") #'lsp-ocaml-type-enclosing-increase-verbosity)
252+
(define-key keymap (kbd "C-<right>") #'lsp-ocaml-type-enclosing-increase-verbosity)
253+
(define-key keymap (kbd "C-<left>") #'lsp-ocaml-type-enclosing-decrease-verbosity)
254+
keymap)
255+
"Keymap for OCaml-lsp type enclosing transient mode.")
256+
257+
(defun lsp-ocaml-type-enclosing-go-up ()
258+
"Go up the type's enclosing."
259+
(interactive)
260+
(when lsp-ocaml--type-enclosing-type-enclosings
261+
(setq lsp-ocaml--type-enclosing-index
262+
(if lsp-ocaml-enclosing-type-cycle
263+
(mod (1+ lsp-ocaml--type-enclosing-index)
264+
(length lsp-ocaml--type-enclosing-type-enclosings))
265+
(min (1+ lsp-ocaml--type-enclosing-index)
266+
(1- (length lsp-ocaml--type-enclosing-type-enclosings))))))
267+
(lsp-ocaml--get-and-display-type-enclosing))
268+
269+
(defun lsp-ocaml-type-enclosing-go-down ()
270+
"Go down the type's enclosing."
271+
(interactive)
272+
(when lsp-ocaml--type-enclosing-type-enclosings
273+
(setq lsp-ocaml--type-enclosing-index
274+
(if lsp-ocaml-enclosing-type-cycle
275+
(mod (1- lsp-ocaml--type-enclosing-index)
276+
(length lsp-ocaml--type-enclosing-type-enclosings))
277+
(max (1- lsp-ocaml--type-enclosing-index) 0))))
278+
(lsp-ocaml--get-and-display-type-enclosing))
279+
280+
(defun lsp-ocaml-type-enclosing-decrease-verbosity ()
281+
"Decreases the number of expansions of aliases in answer."
282+
(interactive)
283+
(let ((verbosity (max 0 (1- lsp-ocaml--type-enclosing-verbosity))))
284+
(setq lsp-ocaml--type-enclosing-verbosity verbosity))
285+
(lsp-ocaml--get-and-display-type-enclosing))
286+
287+
(defun lsp-ocaml-type-enclosing-increase-verbosity ()
288+
"Increases the number of expansions of aliases in answer."
289+
(interactive)
290+
(let ((verbosity (1+ lsp-ocaml--type-enclosing-verbosity)))
291+
(setq lsp-ocaml--type-enclosing-verbosity verbosity))
292+
(lsp-ocaml--get-and-display-type-enclosing t))
293+
294+
(defun lsp-ocaml-type-enclosing-copy ()
295+
"Copy the type of the saved enclosing type to the `kill-ring'."
296+
(interactive)
297+
(when lsp-ocaml--type-enclosing-saved-type
298+
(message "Copied `%s' to kill-ring"
299+
lsp-ocaml--type-enclosing-saved-type)
300+
(kill-new lsp-ocaml--type-enclosing-saved-type)))
301+
302+
(defun lsp-ocaml--get-and-display-type-enclosing (&optional increased-verbosity)
303+
"Compute the type enclosing request.
304+
305+
If INCREASED-VERBOSITY is t, if the computed type is the same as the previous
306+
one, decrease the verbosity.
307+
This allows to make sure that we don't increase infinitely the verbosity."
308+
(-let* ((verbosity lsp-ocaml--type-enclosing-verbosity)
309+
(index lsp-ocaml--type-enclosing-index)
310+
(type_result (lsp-ocaml--type-enclosing verbosity index))
311+
((&ocaml-lsp:TypeEnclosingResult :index :type :enclosings) type_result)
312+
;; Get documentation informations
313+
(markupkind (symbol-name lsp-ocaml-markupkind))
314+
(doc_result (lsp-ocaml--get-documentation nil markupkind))
315+
(doc (cl-getf (cl-getf doc_result :doc) :value)))
316+
(when (and increased-verbosity
317+
(string= type lsp-ocaml--type-enclosing-saved-type))
318+
(setq lsp-ocaml--type-enclosing-verbosity (1- verbosity)))
319+
(setq lsp-ocaml--type-enclosing-saved-type type)
320+
(setq lsp-ocaml--type-enclosing-type-enclosings enclosings)
321+
(lsp-ocaml--display-type markupkind type doc)
322+
(lsp-ocaml--highlight-current-type (aref enclosings index))
323+
type))
324+
196325
;;; -------------------
197326
;;; OCaml-lsp extensions
198327
;;; -------------------
@@ -206,6 +335,13 @@ If OTHER-WINDOW is not nil, open the buffer in an other window."
206335
(unless (lsp-ocaml--load-uri uri nil)
207336
(message "No alternate file %s could be found for %s" (f-filename uri) (buffer-name)))))
208337

338+
(defun lsp-ocaml-type-enclosing ()
339+
"Returns the type of the indent at point."
340+
(interactive)
341+
(lsp-ocaml--init-type-enclosing-config)
342+
(when-let* ((type (lsp-ocaml--get-and-display-type-enclosing)))
343+
(set-transient-map lsp-ocaml-type-enclosing-map t)))
344+
209345
(lsp-consistency-check lsp-ocaml)
210346

211347
(provide 'lsp-ocaml)

docs/manual-language-docs/lsp-ocaml.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,34 @@ root_file: docs/manual-language-docs/lsp-ocaml.md
88

99
### Commands
1010

11+
#### `lsp-ocaml-type-enclosing`
12+
13+
Gets the type of ident under the cursor. It will highlight the ident and display its type.
14+
15+
When this function is called it will create a transient keymap `lsp-ocaml-type-enclosing-map` that allows to do the following things:
16+
- Increase/decrease the number of aliases expansions. As an example, suppose we want to type `h` in the following expression:
17+
```ocaml
18+
type t = A | B
19+
let h : t = A
20+
```
21+
- The lowest verbosity will give `type t`
22+
- The next verbosity will give `type t = A | B`
23+
- Go up/down the enclosing type (bound to `C-<up>/<down>` by default). As an example:
24+
```ocaml
25+
module A = struct
26+
let h : t = A
27+
let f () = ()
28+
29+
(** Test doc *)
30+
let g (f: 'a -> 'b) a = f a
31+
end
32+
```
33+
- Typing on the last `a` will show `'a`
34+
- Going up will highlight `f a` of type `'b`
35+
- Going up will highlight `(f: 'a -> 'b) a = f a` of type `('a -> 'b) -> 'a -> 'b`
36+
- Going up will highlight the whole module and display its entire type
37+
- Copy the current type (bound to `C-w` by default)
38+
1139
#### `lsp-ocaml-find-alternate-file`
1240

1341
Find the interface corresponding to an implementation or the implementation corresponding to an interface.

lsp-mode.el

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5457,25 +5457,29 @@ If EXCLUDE-DECLARATION is non-nil, request the server to include declarations."
54575457
(define-derived-mode lsp-help-mode help-mode "LspHelp"
54585458
"Major mode for displaying lsp help.")
54595459

5460+
(defun lsp--display-contents (contents)
5461+
"Display CONTENTS in a dedicated buffer."
5462+
(if (and contents (not (equal contents "")))
5463+
(let ((lsp-help-buf-name "*lsp-help*"))
5464+
(with-current-buffer (get-buffer-create lsp-help-buf-name)
5465+
(delay-mode-hooks
5466+
(lsp-help-mode)
5467+
(with-help-window lsp-help-buf-name
5468+
(insert
5469+
(mapconcat 'string-trim-right
5470+
(split-string (lsp--render-on-hover-content contents t) "\n")
5471+
"\n"))))
5472+
(run-mode-hooks)))
5473+
(lsp--info "No content at point.")))
5474+
54605475
(defun lsp-describe-thing-at-point ()
54615476
"Display the type signature and documentation of the thing at point."
54625477
(interactive)
54635478
(let ((contents (-some->> (lsp--text-document-position-params)
54645479
(lsp--make-request "textDocument/hover")
54655480
(lsp--send-request)
54665481
(lsp:hover-contents))))
5467-
(if (and contents (not (equal contents "")))
5468-
(let ((lsp-help-buf-name "*lsp-help*"))
5469-
(with-current-buffer (get-buffer-create lsp-help-buf-name)
5470-
(delay-mode-hooks
5471-
(lsp-help-mode)
5472-
(with-help-window lsp-help-buf-name
5473-
(insert
5474-
(mapconcat 'string-trim-right
5475-
(split-string (lsp--render-on-hover-content contents t) "\n")
5476-
"\n"))))
5477-
(run-mode-hooks)))
5478-
(lsp--info "No content at point."))))
5482+
(lsp--display-contents contents)))
54795483

54805484
(defun lsp--point-in-bounds-p (bounds)
54815485
"Return whether the current point is within BOUNDS."

lsp-protocol.el

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,10 @@ See `-let' for a description of the destructuring mechanism."
433433
(lsp-interface (csharp-ls:CSharpMetadata (:textDocument))
434434
(csharp-ls:CSharpMetadataResponse (:source :projectName :assemblyName :symbolName)))
435435

436-
(lsp-interface (ocaml-lsp:SwitchImplIntfParams (:uri) nil))
436+
(lsp-interface (ocaml-lsp:SwitchImplIntfParams (:uri) nil)
437+
(ocaml-lsp:TypeEnclosingParams (:uri :at :index :verbosity) nil)
438+
(ocaml-lsp:TypeEnclosingResult (:index :enclosings :type) nil)
439+
(ocaml-lsp:GetDocumentationParams (:textDocument :position :contentFormat) nil))
437440

438441
(lsp-interface (rls:Cmd (:args :binary :env :cwd) nil))
439442

0 commit comments

Comments
 (0)