]> git.eshelyaron.com Git - emacs.git/commitdiff
Eglot: add support for call and type hierarchies
authorJoão Távora <joaotavora@gmail.com>
Fri, 7 Feb 2025 11:08:29 +0000 (11:08 +0000)
committerEshel Yaron <me@eshelyaron.com>
Sun, 9 Feb 2025 08:43:32 +0000 (09:43 +0100)
* lisp/progmodes/eglot.el (eglot--lsp-interface-alist): Add new
interfaces.
(eglot-client-capabilities): Advertise support for callHierarchy
and typeHierarchy.
(eglot-ignored-server-capabilities): Add new providers.
(eglot--goto): New helper.
(eglot-menu): Add new menu items.
(eglot-handle-request window/showDocument): Use eglot--goto.
(button, tree-widget): Require them.
(eglot--hierarchy-item): New button type.
(eglot--hierarchy-interactive, eglot--hierarchy-children)
(eglot--hierarchy-label, eglot--hierarchy-1, eglot--hierarchy-2):
New internal functions.
(eglot--define-hierarchy-command): New macro.
(eglot-show-type-hierarchy, eglot-show-call-hierarchy)
(eglot-hierarchy-center-on-node): New commands.
(eglot--hierarchy-roots, eglot--hierarchy-specs): New local variables.
(eglot-hierarchy-label-map): New keymap.
(eglot-hierarchy-mode): New major mode.

* doc/misc/eglot.texi (Eglot Commands, Eglot Features): Describe
new feature.

* etc/EGLOT-NEWS (Changes in upcoming Eglot): Mention new feature.

(cherry picked from commit 1ef9de69b3c3d8254ab58bf455137a4439dce516)

doc/misc/eglot.texi
etc/EGLOT-NEWS
lisp/progmodes/eglot.el

index 722766843ec1bbf7882cde47f231c6744460982b..333e369e44094f1766ec832063ecc2d5dde25c51 100644 (file)
@@ -452,6 +452,11 @@ be it the type of a variable, or the name of a formal parameter in a
 function call.  @xref{Eglot Commands} and the
 @code{eglot-inlay-hints-mode} minor mode.
 
+@item
+Display of function call and type hierarchies via the
+@code{eglot-show-call-hierarchy} and @code{eglot-show-type-hierarchy}
+commands (@pxref{Eglot Commands}).
+
 @item
 Code reformatting via the @code{eglot-format} and related commands
 (@pxref{Eglot Commands}).  Automatic reformatting of source code is also
@@ -738,6 +743,16 @@ instead of indicating problems.  For example, a C++ language server can
 serve hints about positional parameter names in function calls and a
 variable's automatically deduced type.  Inlay hints help the user not
 have to remember these things by heart.
+
+@cindex type hierarchy
+@item M-x eglot-show-type-hierarchy
+Pop up a special buffer showing a interactive tree which represents a
+hierarchy of subtypes and supertypes for the symbol at point.
+
+@cindex call hierarchy
+@item M-x eglot-call-type-hierarchy
+Pop up a special buffer showing a interactive tree which represents a
+hierarchy of callers and callee for the symbol at point.
 @end ftable
 
 The following Eglot commands are used less commonly, mostly for
index 02355e25f934be99e304efcf17869b086127f436..20a2e6944263549e02e085c5d4f90e2c3fd93e28 100644 (file)
@@ -20,6 +20,13 @@ https://github.com/joaotavora/eglot/issues/1234.
 \f
 * Changes in upcoming Eglot
 
+** Support for call and type hierarchies
+
+The new commands 'eglot-show-type-hierarchy' and
+'eglot-show-call-hierarchy', when invoked on a symbol, pop up a special
+buffer showing an interactive tree which represents a hierarchy of sub-
+and super-types or callers and callees for that symbol.
+
 ** New 'eglot-advertise-cancellation' variable
 
 Tweaking this variable may help some LSP servers avoid doing costly but
index 1329903d64746100396376dfde09c3b96564d154..2a854af18ac801c411c52aed44af9f83f876af71 100644 (file)
@@ -564,7 +564,9 @@ under cursor."
           (const :tag "Decorate color references" :colorProvider)
           (const :tag "Fold regions of buffer" :foldingRangeProvider)
           (const :tag "Execute custom commands" :executeCommandProvider)
-          (const :tag "Inlay hints" :inlayHintProvider)))
+          (const :tag "Inlay hints" :inlayHintProvider)
+          (const :tag "Type hierarchies" :typeHierarchyProvider)
+          (const :tag "Call hierarchies" :callHierarchyProvider)))
 
 (defcustom eglot-advertise-cancellation nil
   "If non-nil, Eglot attemps to inform server of cancelled requests.
@@ -715,7 +717,13 @@ This can be useful when using docker to run a language server.")
       (WorkspaceSymbol (:name :kind) (:containerName :location :data))
       (InlayHint (:position :label) (:kind :textEdits :tooltip :paddingLeft
                                            :paddingRight :data))
-      (InlayHintLabelPart (:value) (:tooltip :location :command)))
+      (InlayHintLabelPart (:value) (:tooltip :location :command))
+      ;; HACK! 'HierarchyItem' doesn't exist, only `CallHierarchyItem'
+      ;; and `TypeHierarchyItem'.  But they're the same, so no bother.
+      (HierarchyItem (:name :kind)
+                     (:tags :detail :uri :range :selectionRange :data))
+      (CallHierarchyIncomingCall (:from :fromRanges) ())
+      (CallHierarchyOutgoingCall (:to :fromRanges) ()))
     "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces.
 
 INTERFACE-NAME is a symbol designated by the spec as
@@ -1064,6 +1072,8 @@ object."
              :rangeFormatting    `(:dynamicRegistration :json-false)
              :rename             `(:dynamicRegistration :json-false)
              :inlayHint          `(:dynamicRegistration :json-false)
+             :callHierarchy      `(:dynamicRegistration :json-false)
+             :typeHierarchy      `(:dynamicRegistration :json-false)
              :publishDiagnostics (list :relatedInformation :json-false
                                        ;; TODO: We can support :codeDescription after
                                        ;; adding an appropriate UI to
@@ -1781,6 +1791,15 @@ in project `%s'."
   (let ((warning-minimum-level :error))
     (display-warning 'eglot (apply #'eglot--format format args) :warning)))
 
+(defun eglot--goto (range)
+  "Goto and momentarily highlight RANGE in current buffer."
+  (pcase-let ((`(,beg . ,end) (eglot-range-region range)))
+    ;; FIXME: it is very naughty to use someone else's `--'
+    ;; function, but `xref--goto-char' happens to have
+    ;; exactly the semantics we want vis-a-vis widening.
+    (xref--goto-char beg)
+    (pulse-momentary-highlight-region beg end 'highlight)))
+
 (defalias 'eglot--bol
   (if (fboundp 'pos-bol) #'pos-bol
     (lambda (&optional n) (let ((inhibit-field-text-motion t))
@@ -2275,6 +2294,9 @@ If it is activated, also signal textDocument/didOpen."
      :visible (eglot-server-capable :codeActionProvider)]
     ["Quickfix" eglot-code-action-quickfix
      :visible (eglot-server-capable :codeActionProvider)]
+    "--"
+    ["Show type hierarchy" eglot-show-type-hierarchy]
+    ["Show call hierarchy" eglot-show-call-hierarchy]
     "--"))
 
 (easy-menu-define eglot-server-menu nil "Manage server communication"
@@ -2738,12 +2760,7 @@ THINGS are either registrations or unregisterations (sic)."
                   (select-frame-set-input-focus (selected-frame)))
                  ((display-buffer (current-buffer))))
            (when selection
-             (pcase-let ((`(,beg . ,end) (eglot-range-region selection)))
-               ;; FIXME: it is very naughty to use someone else's `--'
-               ;; function, but `xref--goto-char' happens to have
-               ;; exactly the semantics we want vis-a-vis widening.
-               (xref--goto-char beg)
-               (pulse-momentary-highlight-region beg end 'highlight)))))))
+             (eglot--goto selection))))))
      (t (setq success :json-false)))
     `(:success ,success)))
 
@@ -4442,6 +4459,194 @@ If NOERROR, return predicate, else erroring function."
          (jit-lock-unregister #'eglot--update-hints)
          (remove-overlays nil nil 'eglot--inlay-hint t))))
 
+\f
+;;; Call and type hierarchies
+(require 'button)
+(require 'tree-widget)
+
+(define-button-type 'eglot--hierarchy-item
+  'follow-link t
+  'face 'font-lock-function-name-face)
+
+(defun eglot--hierarchy-interactive (specs)
+  (let ((ans
+         (completing-read "[eglot] Direction (default both)?"
+                   (cons "both" (mapcar #'cl-fourth specs))
+                   nil t nil nil "both")))
+    (list
+     (cond ((equal ans "both") t)
+           (t (cl-third (cl-find ans specs :key #'cl-fourth :test #'equal)))))))
+
+(defmacro eglot--define-hierarchy-command
+    (name kind feature preparer specs)
+  `(defun ,name (direction)
+     ,(concat
+       "Show " kind " hierarchy for symbol at point.\n"
+       "DIRECTION can be:\n"
+       (cl-loop for (_ _ d e) in specs
+                concat (format "  - `%s' for %s;\n" d e))
+       "or t, the default, to consider both.\n"
+       "Interactively with a prefix argument, prompt for DIRECTION.")
+     (interactive (if current-prefix-arg
+                      (eglot--hierarchy-interactive ',specs)
+                    (list t)))
+     (let* ((specs ',specs)
+            (specs (if (eq t direction) specs
+                     (list
+                      (cl-find direction specs :key #'cl-third)))))
+       (eglot--hierarchy-1
+        (format "*EGLOT %s hierarchy for %s*"
+                ,kind
+                (eglot-project-nickname (eglot--current-server-or-lose)))
+        ,feature ,preparer specs))))
+
+(eglot--define-hierarchy-command
+ eglot-show-type-hierarchy
+ "type"
+ :typeHierarchyProvider
+ :textDocument/prepareTypeHierarchy
+ ((:typeHierarchy/supertypes " ↑ " derived "supertypes" "derives from")
+  (:typeHierarchy/subtypes " ↓ " base "subtypes" "base of")))
+
+(eglot--define-hierarchy-command
+ eglot-show-call-hierarchy
+ "call"
+ :callHierarchyProvider
+ :textDocument/prepareCallHierarchy
+ ((:callHierarchy/incomingCalls " ← " incoming "incoming calls" "called by"
+                                :from :fromRanges)
+  (:callHierarchy/outgoingCalls " → " base "outgoing calls" "calls"
+                                :to :fromRanges)))
+
+(defvar-local eglot--hierarchy-roots nil)
+(defvar-local eglot--hierarchy-specs nil)
+
+(defun eglot--hierarchy-children (node)
+  (cl-flet ((get-them (method node)
+              (eglot--dbind ((HierarchyItem) name) node
+                (let* ((sym (intern (format "eglot--%s" method)))
+                       (plist (text-properties-at 0 name))
+                       (probe (cl-getf plist sym :none)))
+                  (cond ((eq probe :none)
+                         (let ((v (ignore-errors (jsonrpc-request
+                                                  (eglot--current-server-or-lose) method
+                                                  `(:item ,node)))))
+                           (put-text-property 0 1 sym v name)
+                           v))
+                        (t probe))))))
+    (cl-loop
+     with specs = eglot--hierarchy-specs
+     for (method bullet _ _ hint key ranges) in specs
+     for resp = (get-them method node)
+     for items =
+     (cl-loop for r across resp
+              for item = (if key (plist-get r key) r)
+              collect item
+              do (eglot--dbind ((HierarchyItem) name) item
+                   (put-text-property 0 1 'eglot--hierarchy-method
+                                      method name)
+                   (put-text-property 0 1 'eglot--hierarchy-bullet
+                                      (propertize bullet
+                                                  'help-echo hint)
+                                      name)
+                   (when ranges
+                     (put-text-property 0 1 'eglot--hierarchy-call-sites
+                                        (plist-get r ranges)
+                                        name))))
+     append items)))
+
+(defvar eglot-hierarchy-label-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map button-map)
+    (define-key map [mouse-3] (eglot--mouse-call
+                               #'eglot-hierarchy-center-on-node))
+    map)
+  "Keymap active in labels Eglot hierarchy buffers.")
+
+(defun eglot--hierarchy-label (node)
+  (eglot--dbind ((HierarchyItem) name uri _detail ((:range item-range))) node
+    (with-temp-buffer
+      (insert (propertize
+               (or (get-text-property
+                    0 'eglot--hierarchy-bullet name)
+                   " ∘ ")
+               'face 'shadow))
+      (insert-text-button
+       name
+       :type 'eglot--hierarchy-item
+       'eglot--hierarchy-node node
+       'help-echo "mouse-1, RET: goto definition, mouse-3: center on node"
+       'keymap eglot-hierarchy-label-map
+       'action
+       (lambda (_btn)
+         (pop-to-buffer (find-file-noselect (eglot-uri-to-path uri)))
+         (eglot--goto
+          (or
+           (elt
+            (get-text-property 0 'eglot--hierarchy-call-sites name)
+            0)
+           item-range))))
+      (buffer-string))))
+
+(defun eglot--hierarchy-1 (name provider preparer specs)
+  (eglot-server-capable-or-lose provider)
+  (let* ((server (eglot-current-server))
+         (roots (jsonrpc-request
+                 server
+                 preparer
+                 (eglot--TextDocumentPositionParams))))
+    (with-current-buffer (get-buffer-create name)
+      (eglot-hierarchy-mode)
+      (setq-local
+       eglot--hierarchy-roots roots
+       eglot--hierarchy-specs specs
+       eglot--cached-server server
+       buffer-read-only t
+       revert-buffer-function
+       (lambda (&rest _ignore)
+         ;; flush cache, would defeat purpose of a revert
+         (mapc (lambda (r)
+                 (eglot--dbind ((HierarchyItem) name) r
+                   (set-text-properties 0 1 nil name)))
+               eglot--hierarchy-roots)
+         (eglot--hierarchy-2)))
+      (eglot--hierarchy-2))))
+
+(defun eglot--hierarchy-2 ()
+  (cl-labels ((expander-for (node)
+                (lambda (_widget)
+                  (mapcar
+                   #'convert
+                   (eglot--hierarchy-children node))))
+              (convert (node)
+                (let ((w (widget-convert
+                          'tree-widget
+                          :tag (eglot--hierarchy-label node)
+                          :expander (expander-for node))))
+                  (widget-put w :empty-icon
+                              (widget-get w :leaf-icon))
+                  w)))
+    (let ((inhibit-read-only t))
+      (erase-buffer)
+      (mapc (lambda (r)
+              (widget-create (convert r)))
+            eglot--hierarchy-roots)
+      (goto-char (point-min))))
+    (pop-to-buffer (current-buffer)))
+
+(define-derived-mode eglot-hierarchy-mode special-mode
+  "Eglot special" "Eglot mode for viewing hierarchies.
+\\{eglot-hierarchy-mode-map}"
+  :interactive nil)
+
+(defun eglot-hierarchy-center-on-node ()
+  "Refresh hierarchy, centering on node at point."
+  (interactive)
+  (setq-local eglot--hierarchy-roots
+              (list (get-text-property (point)
+                                 'eglot--hierarchy-node)))
+  (eglot--hierarchy-2))
+
 \f
 ;;; Hacks
 ;;;