diff --git a/README.md b/README.md
index aa17fd2..5945815 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,30 @@ navigating through errors:

+### 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` (C-c C-t)
+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` (C-c
+ C-t or C-→): to increase the verbosity of the
+ type observed
+- `ocaml-eglot-type-enclosing-decrease-verbosity` (C-←): to
+ decrease verbosity of the type observed
+- `ocaml-eglot-type-enclosing-grow` (C-↑): to grow the
+ expression
+- `ocaml-eglot-type-enclosing-shrink` (C-↓): to shrink the
+ expression
+- `ocaml-eglot-type-enclosing-copy` (C-w): to copy the
+ type expression to the _kill-ring_ (clipboard)
+
+
+
### Jump to definition/declaration
OCaml-eglot provides a shortcut to quickly jump to the definition or
diff --git a/media/type-enclosing.gif b/media/type-enclosing.gif
new file mode 100644
index 0000000..a65adfa
Binary files /dev/null and b/media/type-enclosing.gif differ
diff --git a/ocaml-eglot-req.el b/ocaml-eglot-req.el
index a586654..e2e56ad 100644
--- a/ocaml-eglot-req.el
+++ b/ocaml-eglot-req.el
@@ -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
@@ -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 ()
@@ -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
diff --git a/ocaml-eglot-type-enclosing.el b/ocaml-eglot-type-enclosing.el
new file mode 100644
index 0000000..2b3a579
--- /dev/null
+++ b/ocaml-eglot-type-enclosing.el
@@ -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
+;; 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-") #'ocaml-eglot-type-enclosing-grow)
+ (define-key keymap (kbd "C-") #'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-") #'ocaml-eglot-type-enclosing-increase-verbosity)
+ (define-key keymap (kbd "C-") #'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))
+ (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
diff --git a/ocaml-eglot-util.el b/ocaml-eglot-util.el
index 1073b0f..702b8d5 100644
--- a/ocaml-eglot-util.el
+++ b/ocaml-eglot-util.el
@@ -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
@@ -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)
@@ -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
@@ -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
diff --git a/ocaml-eglot.el b/ocaml-eglot.el
index 2cea2cc..6febcc3 100644
--- a/ocaml-eglot.el
+++ b/ocaml-eglot.el
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.")