Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type Enclosing Command #14

Merged
merged 13 commits into from
Jan 14, 2025
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ navigating through errors:

![Error navigation example](media/error-navigation.gif)

### Type Enclosings

In `ocaml-eglot` one can display the type of the expression below the cursor and
navigate the enclosing nodes while increasing or decreasing verbosity:

- `ocaml-eglot-type-enclosing` (<kbd>C-c</kbd> <kbd>C-t</kbd>)
Display the type of the selection and start a "type enclosing" session.

During a "type enclosing" session the following commands are available:

- `ocaml-eglot-type-enclosing-increase-verbosity` (<kbd>C-c</kbd>
<kbd>C-t</kbd> or <kbd>C-→</kbd>): to increase the verbosity of the
type observed
- `ocaml-eglot-type-enclosing-decrease-verbosity` (<kbd>C-←</kbd>): to
decrease verbosity of the type observed
- `ocaml-eglot-type-enclosing-grow` (<kbd>C-↑</kbd>): to grow the
expression
- `ocaml-eglot-type-enclosing-shrink` (<kbd>C-↓</kbd>): to shrink the
expression
- `ocaml-eglot-type-enclosing-copy` (<kbd>C-w</kbd>): to copy the
type expression to the _kill-ring_ (clipboard)

![Type Enclosings example](media/type-enclosing.gif)

### Jump to definition/declaration

OCaml-eglot provides a shortcut to quickly jump to the definition or
Expand Down
Binary file added media/type-enclosing.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 20 additions & 1 deletion ocaml-eglot-req.el
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
;;; ocaml-eglot-req.el --- LSP custom request -*- coding: utf-8; lexical-binding: t -*-

;; Copyright (C) 2024 Xavier Van de Woestyne
;; Copyright (C) 2024-2025 Xavier Van de Woestyne
;; Licensed under the MIT license.

;; Author: Xavier Van de Woestyne <[email protected]>
Expand Down Expand Up @@ -95,6 +95,17 @@ A potential IDENTIFIER can be given and MARKUP-KIND can be parametrized."
(if identifier (append params `(:identifier, identifier))
params)))

(defun ocaml-eglot-req--TypeEnclosingParams (at index verbosity)
"Compute the `TypeEnclosingParams'.
AT is the range or the position.
INDEX is the index of the enclosing.
VERBOSITY is a potential verbosity index."
(append (list :textDocument (ocaml-eglot-req--TextDocumentIdentifier))
(ocaml-eglot-req--TextDocumentIdentifier)
`(:at, at)
`(:index, index)
`(:verbosity, verbosity)))

;;; Concrete requests

(defun ocaml-eglot-req--jump ()
Expand Down Expand Up @@ -156,5 +167,13 @@ under the cursor. The MARKUP-KIND can also be configured."
(let ((params (ocaml-eglot-req--TextDocumentPositionParams)))
(ocaml-eglot-req--send :textDocument/declaration params)))

(defun ocaml-eglot-req--type-enclosings (at index verbosity)
"Execute the `ocamllsp/typeEnclosing' request for the current point.
AT is the range or the position.
INDEX is the index of the enclosing.
VERBOSITY is a potential verbosity index."
(let ((params (ocaml-eglot-req--TypeEnclosingParams at index verbosity)))
(ocaml-eglot-req--send :ocamllsp/typeEnclosing params)))

(provide 'ocaml-eglot-req)
;;; ocaml-eglot-req.el ends here
161 changes: 161 additions & 0 deletions ocaml-eglot-type-enclosing.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
;;; ocaml-eglot-type-enclosing.el --- Type Enclosing feature -*- coding: utf-8; lexical-binding: t -*-

;; Copyright (C) 2024-2025 Xavier Van de Woestyne
;; Licensed under the MIT license.

;; Author: Xavier Van de Woestyne <[email protected]>
;; Created: 10 January 2025
;; SPDX-License-Identifier: MIT

;;; Commentary:

;; Plumbing needed to implement the primitives related to type
;; enclosing commands.

;;; Code:

(require 'cl-lib)
(require 'ocaml-eglot-util)
(require 'ocaml-eglot-req)

;;; Customizable variables

(defcustom ocaml-eglot-type-buffer-name "*ocaml-eglot-types*"
"The name of the buffer storing types."
:group 'ocaml-eglot
:type 'string)

;;; Internal variables

(defvar-local ocaml-eglot-type-enclosing-types nil
"Current list of enclosings related to types.")

(defvar-local ocaml-eglot-type-enclosing-current-type nil
"Current type for the current enclosing.")

(defvar-local ocaml-eglot-type-enclosing-offset 0
"The offset of the requested enclosings.")

(defvar-local ocaml-eglot-type-enclosing-verbosity 0
"The verbosity of the current enclosing request.")

;;; Key mapping for type enclosing

(defvar ocaml-eglot-type-enclosing-map
(let ((keymap (make-sparse-keymap)))
(define-key keymap (kbd "C-<up>") #'ocaml-eglot-type-enclosing-grow)
(define-key keymap (kbd "C-<down>") #'ocaml-eglot-type-enclosing-shrink)
(define-key keymap (kbd "C-w") #'ocaml-eglot-type-enclosing-copy)
(define-key keymap (kbd "C-c C-t") #'ocaml-eglot-type-enclosing-increase-verbosity)
(define-key keymap (kbd "C-<right>") #'ocaml-eglot-type-enclosing-increase-verbosity)
(define-key keymap (kbd "C-<left>") #'ocaml-eglot-type-enclosing-decrease-verbosity)
keymap)
"Keymap for OCaml-eglot's type enclosing transient mode.")

;;; Internal functions

(defun ocaml-eglot-type-enclosing-copy ()
"Copy the type of the current enclosing to the Kill-ring."
(interactive)
(when ocaml-eglot-type-enclosing-current-type
(eglot--message "Copied `%s' to kill-ring"
ocaml-eglot-type-enclosing-current-type)
(kill-new ocaml-eglot-type-enclosing-current-type)))

(defun ocaml-eglot-type-enclosing--with-fixed-offset ()
"Compute the type enclosing for a dedicated offset."
(let* ((verbosity ocaml-eglot-type-enclosing-verbosity)
(index ocaml-eglot-type-enclosing-offset)
(at (ocaml-eglot-util--current-position-or-range))
(result (ocaml-eglot-req--type-enclosings at index verbosity))
(type (cl-getf result :type)))
(setq ocaml-eglot-type-enclosing-current-type type)
(ocaml-eglot-type-enclosing--display type t)))

(defun ocaml-eglot-type-enclosing-increase-verbosity ()
"Increase the verbosity of the current request."
(interactive)
(setq ocaml-eglot-type-enclosing-verbosity
(1+ ocaml-eglot-type-enclosing-verbosity))
(ocaml-eglot-type-enclosing--with-fixed-offset))

(defun ocaml-eglot-type-enclosing-decrease-verbosity ()
"Decrease the verbosity of the current request."
(interactive)
(when (> ocaml-eglot-type-enclosing-verbosity 0)
(setq ocaml-eglot-type-enclosing-verbosity
(1- ocaml-eglot-type-enclosing-verbosity)))
(ocaml-eglot-type-enclosing--with-fixed-offset))

(defun ocaml-eglot-type-enclosing-grow ()
"Growing of the type enclosing."
(interactive)
(when ocaml-eglot-type-enclosing-types
(setq ocaml-eglot-type-enclosing-offset
(mod (1+ ocaml-eglot-type-enclosing-offset)
(length ocaml-eglot-type-enclosing-types)))
(ocaml-eglot-type-enclosing--with-fixed-offset)))

(defun ocaml-eglot-type-enclosing-shrink ()
"Display the type enclosing of a smaller enclosing if possible."
(interactive)
(when ocaml-eglot-type-enclosing-types
(setq ocaml-eglot-type-enclosing-offset
(mod (1- ocaml-eglot-type-enclosing-offset)
(length ocaml-eglot-type-enclosing-types)))
(ocaml-eglot-type-enclosing--with-fixed-offset)))

(defun ocaml-eglot-type-enclosing--type-buffer (type-expr)
"Create buffer with content TYPE-EXPR of the enclosing type buffer."
; We store the current major mode to be used in the type buffer for
; syntax highlighting.
(let ((curr-dir default-directory)
(current-major-mode major-mode))
voodoos marked this conversation as resolved.
Show resolved Hide resolved
(with-current-buffer (get-buffer-create ocaml-eglot-type-buffer-name)
(funcall current-major-mode)
(read-only-mode 0)
(erase-buffer)
(insert type-expr)
(goto-char (point-min))
(read-only-mode 1)
(setq default-directory curr-dir))))

(defun ocaml-eglot-type-enclosing--display (type-expr &optional current)
"Display the type-enclosing for TYPE-EXPR in a dedicated buffer.
If CURRENT is set, the range of the enclosing will be highlighted."
(ocaml-eglot-type-enclosing--type-buffer type-expr)
(if (ocaml-eglot-util--text-less-than type-expr 8)
(message "%s" (with-current-buffer ocaml-eglot-type-buffer-name
(font-lock-fontify-region (point-min) (point-max))
(buffer-string)))
(display-buffer ocaml-eglot-type-buffer-name))
(when (and current (> (length ocaml-eglot-type-enclosing-types) 0))
(let ((current (aref ocaml-eglot-type-enclosing-types
ocaml-eglot-type-enclosing-offset)))
(ocaml-eglot-util--highlight-range current
'ocaml-eglot-highlight-region-face))))

(defun ocaml-eglot-type-enclosing--reset ()
"Reset local variables defined by the enclosing query."
(setq ocaml-eglot-type-enclosing-current-type nil)
(setq ocaml-eglot-type-enclosing-verbosity 0)
(setq ocaml-eglot-type-enclosing-types nil)
(setq ocaml-eglot-type-enclosing-offset 0))

(defun ocaml-eglot-type-enclosing--call ()
"Print the type of the expression under point."
(ocaml-eglot-type-enclosing--reset)
(let* ((verbosity ocaml-eglot-type-enclosing-verbosity)
(index ocaml-eglot-type-enclosing-offset)
(at (ocaml-eglot-util--current-position-or-range))
(result (ocaml-eglot-req--type-enclosings at index verbosity))
(type (cl-getf result :type))
(enclosings (cl-getf result :enclosings)))
(setq ocaml-eglot-type-enclosing-types enclosings)
(setq ocaml-eglot-type-enclosing-current-type type)
(ocaml-eglot-type-enclosing--display type t)
(set-transient-map ocaml-eglot-type-enclosing-map t
'ocaml-eglot-type-enclosing--reset)))

(provide 'ocaml-eglot-type-enclosing)
;;; ocaml-eglot-type-enclosing.el ends here
31 changes: 30 additions & 1 deletion ocaml-eglot-util.el
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
;;; ocaml-eglot-util.el --- Auxiliary tools -*- coding: utf-8; lexical-binding: t -*-

;; Copyright (C) 2024 Xavier Van de Woestyne
;; Copyright (C) 2024-2025 Xavier Van de Woestyne
;; Licensed under the MIT license.

;; Author: Xavier Van de Woestyne <[email protected]>
Expand All @@ -21,6 +21,17 @@

;; Generic util

(defun ocaml-eglot-util--text-less-than (text limit)
"Return non-nil if TEXT is less than LIMIT."
(let ((count 0)
(pos 0))
(save-match-data
(while (and (<= count limit)
(string-match "\n" text pos))
(setq pos (match-end 0))
(setq count (1+ count))))
(<= count limit)))

(defun ocaml-eglot-util--vec-first-or-nil (vec)
"Return the first element of VEC or nil."
(when (> (length vec) 0)
Expand Down Expand Up @@ -110,6 +121,14 @@
(list :start start
:end (ocaml-eglot-util--position-increase-char start "_")))))

(defun ocaml-eglot-util--current-position-or-range ()
"Return the current position or a range if the region is active."
(if (region-active-p)
(let ((beg (eglot--pos-to-lsp-position (region-beginning)))
(end (eglot--pos-to-lsp-position (region-end))))
`(:start ,beg :end ,end))
(eglot--pos-to-lsp-position)))

(defun ocaml-eglot-util--visit-file (strategy current-file new-file range)
"Visits a referenced document, NEW-FILE at position start of RANGE.
The STRATEGY can be `'new' `'current' or `'smart'. The later opens a
Expand All @@ -122,5 +141,15 @@ current window otherwise."
(t (find-file-other-window new-file)))
(ocaml-eglot-util--jump-to-range range))

(defun ocaml-eglot-util--highlight-range (range face)
"Highlight a given RANGE using a given FACE."
(remove-overlays nil nil 'ocaml-eglot-highlight 'highlight)
(let* ((beg (eglot--lsp-position-to-point (cl-getf range :start)))
(end (eglot--lsp-position-to-point (cl-getf range :end)))
(overlay (make-overlay beg end)))
(overlay-put overlay 'face face)
(overlay-put overlay 'ocaml-eglot-highlight 'highlight)
(unwind-protect (sit-for 60) (delete-overlay overlay))))

(provide 'ocaml-eglot-util)
;;; ocaml-eglot-util.el ends here
17 changes: 15 additions & 2 deletions ocaml-eglot.el
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
;;; ocaml-eglot.el --- An OCaml companion for Eglot -*- coding: utf-8; lexical-binding: t -*-

;; Copyright (C) 2024 The OCaml-eglot Project Contributors
;; Copyright (C) 2024-2025 The OCaml-eglot Project Contributors
;; Licensed under the MIT license.

;; Author: Xavier Van de Woestyne <[email protected]>
Expand Down Expand Up @@ -33,10 +33,10 @@
;;; Code:

(require 'flymake)
(require 'xref)
(require 'cl-lib)
(require 'ocaml-eglot-util)
(require 'ocaml-eglot-req)
(require 'ocaml-eglot-type-enclosing)
(require 'eglot)

(defgroup ocaml-eglot nil
Expand Down Expand Up @@ -93,6 +93,10 @@ Otherwise, `merlin-construct' only includes constructors."
"Face describing the doc of values (used for search for example)."
:group 'ocaml-eglot)

(defface ocaml-eglot-highlight-region-face
'((t (:inherit highlight)))
"Face used when highlighting a region.")

;;; Features

;; Jump to errors
Expand Down Expand Up @@ -446,6 +450,14 @@ It use the ARG to use local values or not."
(interactive "sIdentifier: ")
(ocaml-eglot--document-aux identifier))

;; Type Enclosings

(defun ocaml-eglot-type-enclosing ()
"Print the type of the expression under point (or of the region, if it exists).
If called repeatedly, increase the verbosity of the type shown."
(interactive)
(ocaml-eglot-type-enclosing--call))

;;; Mode

(defvar ocaml-eglot-map
Expand All @@ -456,6 +468,7 @@ It use the ARG to use local values or not."
(define-key ocaml-eglot-keymap (kbd "C-c C-i") #'ocaml-eglot-find-declaration)
(define-key ocaml-eglot-keymap (kbd "C-c C-a") #'ocaml-eglot-alternate-file)
(define-key ocaml-eglot-keymap (kbd "C-c C-d") #'ocaml-eglot-document)
(define-key ocaml-eglot-keymap (kbd "C-c C-t") #'ocaml-eglot-type-enclosing)
ocaml-eglot-keymap)
"Keymap for OCaml-eglot minor mode.")

Expand Down
Loading