;;; lsp-ui-peek.el --- Lsp-Ui-Peek -*- lexical-binding: t -*- ;; Copyright (C) 2017 Sebastien Chapuis ;; Author: Sebastien Chapuis ;; URL: https://github.com/emacs-lsp/lsp-ui ;; Keywords: languagues, tools ;; Version: 0.0.1 ;;; License ;; ;; 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, 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; see the file COPYING. If not, write to ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth ;; Floor, Boston, MA 02110-1301, USA. ;;; Commentary: ;; ;; Load this file and execute `lsp-ui-peek-find-references' ;; on a symbol to find its references ;; or `lsp-ui-peek-find-definitions'. ;; Type 'q' to close the window. ;; ;;; Code: (require 'lsp-protocol) (require 'lsp-mode) (require 'xref) (require 'dash) (defgroup lsp-ui-peek nil "Improve version of xref with peek feature." :group 'tools :group 'convenience :group 'lsp-ui :link '(custom-manual "(lsp-ui-peek) Top") :link '(info-link "(lsp-ui-peek) Customizing")) (defcustom lsp-ui-peek-enable t "Whether or not to enable ‘lsp-ui-peek’." :type 'boolean :group 'lsp-ui) (defcustom lsp-ui-peek-show-directory t "Whether or not to show the directory of files." :type 'boolean :safe t :group 'lsp-ui-peek) (defcustom lsp-ui-peek-peek-height 20 "Height of the peek code." :type 'integer :group 'lsp-ui-peek) (defcustom lsp-ui-peek-list-width 50 "Width of the right panel." :type 'integer :group 'lsp-ui-peek) (defcustom lsp-ui-peek-fontify 'on-demand "Whether to fontify chunks of code (use semantics colors). WARNING: 'always can heavily slow the processing when `lsp-ui-peek-expand-function' expands more than 1 file. It is recommended to keep the default value of `lsp-ui-peek-expand-function' when this variable is set to 'always." :type '(choice (const :tag "Never" never) (const :tag "On demand" on-demand) (const :tag "Always" always)) :group 'lsp-ui-peek) (defcustom lsp-ui-peek-always-show nil "Show the peek view even if there is only 1 cross reference. By default, the peek view isn't shown if there is 1 xref." :type 'boolean :group 'lsp-ui-peek) (defface lsp-ui-peek-peek '((((background light)) :background "light gray") (t :background "#031A25")) "Face used for the peek." :group 'lsp-ui-peek) (defface lsp-ui-peek-list '((((background light)) :background "light gray") (t :background "#181818")) "Face used to list references." :group 'lsp-ui-peek) (defface lsp-ui-peek-filename '((((background light)) :foreground "red") (t :foreground "dark orange")) "Face used for the filename's reference in the list." :group 'lsp-ui-peek) (defface lsp-ui-peek-line-number '((t :foreground "grey25")) "Line number face." :group 'lsp-ui-peek) (defface lsp-ui-peek-highlight '((((background light)) :background "yellow" :box (:line-width -1 :color "red")) (t :background "white" :foreground "black" :distant-foreground "white" :box (:line-width -1 :color "red"))) "Face used to highlight the reference/definition. Do not use box, underline or overline prop. If you want to use box, use a negative value for its width. Those properties deform the whole overlay." :group 'lsp-ui-peek) (defface lsp-ui-peek-header '((((background light)) :background "grey30" :foreground "white") (t :background "white" :foreground "black")) "Face used for the headers." :group 'lsp-ui-peek) (defface lsp-ui-peek-footer '((t :inherit lsp-ui-peek-header)) "Face used for the footers. Only the background of this face is used." :group 'lsp-ui-peek) (defface lsp-ui-peek-selection '((((background light)) :background "grey30" :foreground "white") (t :background "white" :foreground "black")) "Face used for the current selection. Do not use box, underline or overline prop. If you want to use box, use a negative value for its width. Those properties deform the whole overlay." :group 'lsp-ui-peek) (defvar lsp-ui-peek-expand-function 'lsp-ui-peek--expand-buffer "A function used to determinate which file(s) to expand in the list of xrefs. The function takes one parameter: a list of cons where the car is the filename and the cdr is the number of references in that file. It should returns a list of filenames to expand. WARNING: If you change this variable and expand more than 1 file, it is recommended to set `lsp-ui-peek-fontify' to 'never or 'on-demand, otherwise it will cause performances issues.") (defvar-local lsp-ui-peek--overlay nil) (defvar-local lsp-ui-peek--list nil) (defvar-local lsp-ui-peek--last-xref nil) (defvar-local lsp-ui-peek--selection 0) (defvar-local lsp-ui-peek--offset 0) (defvar-local lsp-ui-peek--size-list 0) (defvar-local lsp-ui-peek--win-start nil) (defvar-local lsp-ui-peek--method nil) (defvar-local lsp-ui-peek--deactivate-keymap-fn nil) (defvar lsp--peek-save-major-mode nil "Stores the major mode for lsp ui peek.") (defvar lsp-ui-peek--jumps (make-hash-table) "Hashtable which stores all jumps on a per window basis.") (defvar evil--jumps-window-jumps) ; defined in evil-jumps.el (defmacro lsp-ui-peek--with-evil-jumps (&rest body) "Make `evil-jumps.el' commands work on `lsp-ui-peek--jumps'." (declare (indent 1)) `(let ((evil--jumps-window-jumps lsp-ui-peek--jumps)) ,@body)) (with-eval-after-load 'evil-jumps ;; We need to jump through some hoops to prevent the byte-compiler from ;; compiling this code. We can’t compile the code without requiring ;; ‘evil-macros’. (eval '(progn (evil-define-motion lsp-ui-peek-jump-backward (count) (lsp-ui-peek--with-evil-jumps (evil--jump-backward count) (run-hooks 'xref-after-return-hook))) (evil-define-motion lsp-ui-peek-jump-forward (count) (lsp-ui-peek--with-evil-jumps (evil--jump-forward count) (run-hooks 'xref-after-return-hook)))) t)) (defmacro lsp-ui-peek--prop (prop &optional string) `(get-text-property 0 ,prop (or ,string (lsp-ui-peek--get-text-selection) ""))) (defmacro lsp-ui-peek--add-prop (prop &optional string) `(let ((obj (or ,string (lsp-ui-peek--get-text-selection)))) (add-text-properties 0 (length obj) ,prop obj) obj)) (defun lsp-ui-peek--truncate (len s) (if (> (string-width s) len) (concat (truncate-string-to-width s (max (- len 2) 0)) "..") s)) (defun lsp-ui-peek--get-text-selection (&optional n) (nth (or n lsp-ui-peek--selection) (--remove (get-text-property 0 'lsp-ui-peek-hidden it) lsp-ui-peek--list))) (defun lsp-ui-peek--get-selection () (get-text-property 0 'lsp-ui-peek (or (lsp-ui-peek--get-text-selection) ""))) (defun lsp-ui-peek--visual-index () (- lsp-ui-peek--selection lsp-ui-peek--offset)) (defun lsp-ui-peek--make-line (index src) (-let* (((s1 . s2) src) (len-s1 (length s1)) (len-s2 (length s2)) (on-selection (= (1+ (lsp-ui-peek--visual-index)) index)) (face-left (if (= index 0) 'lsp-ui-peek-header 'lsp-ui-peek-peek)) (face-right (cond (on-selection 'lsp-ui-peek-selection) ((= index 0) 'lsp-ui-peek-header) (t 'lsp-ui-peek-list)))) (when on-selection (setq s2 (copy-sequence s2)) (add-face-text-property 0 len-s2 face-right nil s2)) (unless (get-text-property 0 'lsp-ui-peek-faced s2) (add-face-text-property 0 len-s2 face-right t s2) (add-text-properties 0 len-s2 '(lsp-ui-peek-faced t) s2) (add-face-text-property 0 len-s2 'default t s2)) (add-face-text-property 0 len-s1 face-left t s1) (add-face-text-property 0 len-s1 'default t s1) (concat s1 (propertize "_" 'face face-left 'display `(space :align-to (- right-fringe ,(1+ lsp-ui-peek-list-width)))) " " s2 (propertize "_" 'face face-right 'display `(space :align-to (- right-fringe 1))) (propertize "\n" 'face face-right)))) (defun lsp-ui-peek--adjust (width strings) (-let* (((s1 . s2) strings)) (cons (lsp-ui-peek--truncate (- width (1+ lsp-ui-peek-list-width)) s1) (lsp-ui-peek--truncate (- lsp-ui-peek-list-width 2) s2)))) (defun lsp-ui-peek--make-footer () ;; Character-only terminals don't support characters of different height (when (display-graphic-p) (list (concat (propertize " " 'face `(:background ,(face-background 'lsp-ui-peek-footer nil t) :height 1) 'display `(space :align-to (- right-fringe ,(1+ lsp-ui-peek-list-width)))) (propertize " " 'face '(:height 1) 'display `(space :align-to (- right-fringe ,lsp-ui-peek-list-width))) (propertize " " 'face `(:background ,(face-background 'lsp-ui-peek-footer nil t) :height 1) 'display `(space :align-to (- right-fringe 0))) (propertize "\n" 'face '(:height 1)) (propertize "\n" 'face '(:height 0.5)))))) (defun lsp-ui-peek--peek-new (src1 src2) (-let* ((win-width (- (window-text-width) (if (bound-and-true-p display-line-numbers-mode) (+ 2 (line-number-display-width)) 0))) (string (-some--> (-zip-fill "" src1 src2) (--map (lsp-ui-peek--adjust win-width it) it) (-map-indexed 'lsp-ui-peek--make-line it) (-concat it (lsp-ui-peek--make-footer)))) (next-line (line-beginning-position 2)) (ov (or (when (overlayp lsp-ui-peek--overlay) lsp-ui-peek--overlay) (make-overlay next-line next-line)))) (setq lsp-ui-peek--overlay ov) (overlay-put ov 'after-string (mapconcat 'identity string "")) (overlay-put ov 'display-line-numbers-disable t) (overlay-put ov 'window (get-buffer-window)))) (defun lsp-ui-peek--expand-buffer (files) (if (--any? (equal (car it) buffer-file-name) files) (list buffer-file-name) (list (caar files)))) (defun lsp-ui-peek--expand (xrefs) (let* ((to-expand (->> (--map (cons (plist-get it :file) (plist-get it :count)) xrefs) (funcall lsp-ui-peek-expand-function))) first) (while (nth lsp-ui-peek--selection lsp-ui-peek--list) (when (and (lsp-ui-peek--prop 'xrefs) (member (lsp-ui-peek--prop 'file) to-expand)) (unless first (setq first (1+ lsp-ui-peek--selection))) (lsp-ui-peek--toggle-file t)) (setq lsp-ui-peek--selection (1+ lsp-ui-peek--selection))) (setq lsp-ui-peek--selection (or first 0)) (lsp-ui-peek--recenter))) (defun lsp-ui-peek--show (xrefs) "Create a window to list references/defintions. XREFS is a list of references/definitions." (setq lsp-ui-peek--win-start (window-start) lsp-ui-peek--selection 0 lsp-ui-peek--offset 0 lsp-ui-peek--size-list 0 lsp-ui-peek--list nil) (when (eq (logand lsp-ui-peek-peek-height 1) 1) (setq lsp-ui-peek-peek-height (1+ lsp-ui-peek-peek-height))) (when (< (- (line-number-at-pos (window-end)) (line-number-at-pos)) (+ lsp-ui-peek-peek-height 3)) (recenter 15)) (setq xrefs (--sort (string< (plist-get it :file) (plist-get other :file)) xrefs)) (--each xrefs (-let* (((&plist :file filename :xrefs xrefs :count count) it) (len-str (number-to-string count))) (setq lsp-ui-peek--size-list (+ lsp-ui-peek--size-list count)) (push (concat (propertize (if lsp-ui-peek-show-directory (lsp-ui--workspace-path filename) (file-name-nondirectory filename)) 'face 'lsp-ui-peek-filename 'file filename 'xrefs xrefs) (propertize " " 'display `(space :align-to (- right-fringe ;; Account for Emacs TTY's window divider ;; Without this leeway, the reference count ;; string goes to next line - impairs readability ,(if (display-graphic-p) 0 1) ,(1+ (length len-str))))) (propertize len-str 'face 'lsp-ui-peek-filename)) lsp-ui-peek--list))) (setq lsp-ui-peek--list (nreverse lsp-ui-peek--list)) (lsp-ui-peek--expand xrefs) (lsp-ui-peek--peek)) (defun lsp-ui-peek--recenter () (let ((half-height (/ lsp-ui-peek-peek-height 2))) (when (> lsp-ui-peek--selection half-height) (setq lsp-ui-peek--offset (- lsp-ui-peek--selection (1- half-height)))))) (defun lsp-ui-peek--fill (min-len list) (let ((len (length list))) (if (< len min-len) (append list (-repeat (- min-len len) "")) list))) (defun lsp-ui-peek--render (major string) (with-temp-buffer (insert string) (delay-mode-hooks (let ((inhibit-message t)) (funcall major)) (ignore-errors (font-lock-ensure))) (buffer-string))) (defun lsp-ui-peek--peek () "Show reference's chunk of code." (-let* ((xref (lsp-ui-peek--get-selection)) ((&plist :file file :chunk chunk) (or xref lsp-ui-peek--last-xref)) (header (concat " " (lsp-ui--workspace-path file) "\n")) (header2 (format " %s %s" lsp-ui-peek--size-list (string-remove-prefix "workspace/" (string-remove-prefix "textDocument/" lsp-ui-peek--method)))) (ref-view (--> chunk (subst-char-in-string ?\t ?\s it) (concat header it) (split-string it "\n"))) (list-refs (->> lsp-ui-peek--list (--remove (lsp-ui-peek--prop 'lsp-ui-peek-hidden it)) (-drop lsp-ui-peek--offset) (-take (1- lsp-ui-peek-peek-height)) (lsp-ui-peek--fill (1- lsp-ui-peek-peek-height)) (-concat (list header2))))) (setq lsp-ui-peek--last-xref (or xref lsp-ui-peek--last-xref)) (lsp-ui-peek--peek-new ref-view list-refs) (and (fboundp 'lsp-ui-doc--hide-frame) (lsp-ui-doc--hide-frame)))) (defun lsp-ui-peek--toggle-text-prop (s) (let ((state (lsp-ui-peek--prop 'lsp-ui-peek-hidden s))) (lsp-ui-peek--add-prop `(lsp-ui-peek-hidden ,(not state)) s))) (defun lsp-ui-peek--toggle-hidden (file) (setq lsp-ui-peek--list (--map-when (string= (plist-get (lsp-ui-peek--prop 'lsp-ui-peek it) :file) file) (prog1 it (lsp-ui-peek--toggle-text-prop it)) lsp-ui-peek--list))) (defun lsp-ui-peek--remove-hidden (file) (setq lsp-ui-peek--list (--map-when (string= (plist-get (lsp-ui-peek--prop 'lsp-ui-peek it) :file) file) (prog1 it (lsp-ui-peek--add-prop '(lsp-ui-peek-hidden nil) it)) lsp-ui-peek--list))) (defun lsp-ui-peek--make-ref-line (xref) (-let* (((&plist :summary summary :line line :file file) xref) (string (format "%-3s %s" (propertize (number-to-string (1+ line)) 'face 'lsp-ui-peek-line-number) (string-trim summary)))) (lsp-ui-peek--add-prop `(lsp-ui-peek ,xref file ,file) string))) (defun lsp-ui-peek--insert-xrefs (xrefs filename index) (setq lsp-ui-peek--list (--> (lsp-ui-peek--get-xrefs-in-file (cons filename xrefs)) (-map 'lsp-ui-peek--make-ref-line it) (-insert-at (1+ index) it lsp-ui-peek--list) (-flatten it))) (lsp-ui-peek--add-prop '(xrefs nil))) (defun lsp-ui-peek--toggle-file (&optional no-update) (interactive) (-if-let* ((xrefs (lsp-ui-peek--prop 'xrefs)) (filename (lsp-ui-peek--prop 'file)) (index (--find-index (equal (lsp-ui-peek--prop 'file it) filename) lsp-ui-peek--list))) (lsp-ui-peek--insert-xrefs xrefs filename index) (let ((file (lsp-ui-peek--prop 'file))) (lsp-ui-peek--toggle-hidden file) (while (not (equal file (lsp-ui-peek--prop 'file))) (lsp-ui-peek--select-prev t)))) (unless no-update (lsp-ui-peek--peek))) (defun lsp-ui-peek--select (index) (setq lsp-ui-peek--selection (+ lsp-ui-peek--selection index))) (defun lsp-ui-peek--select-next (&optional no-update) (interactive) (when (lsp-ui-peek--get-text-selection (1+ lsp-ui-peek--selection)) (lsp-ui-peek--select 1) (while (> (lsp-ui-peek--visual-index) (- lsp-ui-peek-peek-height 2)) (setq lsp-ui-peek--offset (1+ lsp-ui-peek--offset))) (unless no-update (lsp-ui-peek--peek)))) (defun lsp-ui-peek--select-prev (&optional no-update) (interactive) (when (> lsp-ui-peek--selection 0) (lsp-ui-peek--select -1) (while (< (lsp-ui-peek--visual-index) 0) (setq lsp-ui-peek--offset (1- lsp-ui-peek--offset)))) (unless no-update (lsp-ui-peek--peek))) (defun lsp-ui-peek--skip-refs (fn) (let ((last-file (lsp-ui-peek--prop 'file)) last-selection) (when (lsp-ui-peek--get-selection) (while (and (equal (lsp-ui-peek--prop 'file) last-file) (not (equal last-selection lsp-ui-peek--selection))) (setq last-selection lsp-ui-peek--selection) (funcall fn t))))) (defun lsp-ui-peek--select-prev-file () (interactive) (if (not (lsp-ui-peek--get-selection)) (lsp-ui-peek--select-prev) (lsp-ui-peek--skip-refs 'lsp-ui-peek--select-prev) (when (lsp-ui-peek--get-selection) (lsp-ui-peek--skip-refs 'lsp-ui-peek--select-prev) (unless (= lsp-ui-peek--selection 0) (lsp-ui-peek--select-next t)))) (if (lsp-ui-peek--prop 'xrefs) (lsp-ui-peek--toggle-file) (lsp-ui-peek--remove-hidden (lsp-ui-peek--prop 'file))) (lsp-ui-peek--select-next t) (lsp-ui-peek--recenter) (lsp-ui-peek--peek)) (defun lsp-ui-peek--select-next-file () (interactive) (lsp-ui-peek--skip-refs 'lsp-ui-peek--select-next) (if (lsp-ui-peek--prop 'xrefs) (lsp-ui-peek--toggle-file) (lsp-ui-peek--remove-hidden (lsp-ui-peek--prop 'file))) (lsp-ui-peek--select-next t) (lsp-ui-peek--recenter) (lsp-ui-peek--peek)) (defun lsp-ui-peek--peek-hide () "Hide the chunk of code and restore previous state." (when (overlayp lsp-ui-peek--overlay) (delete-overlay lsp-ui-peek--overlay)) (setq lsp-ui-peek--overlay nil lsp-ui-peek--last-xref nil) (when lsp-ui-peek--win-start (set-window-start (get-buffer-window) lsp-ui-peek--win-start))) (defun lsp-ui-peek--deactivate-keymap () "Deactivate keymap." (-when-let (fn lsp-ui-peek--deactivate-keymap-fn) (setq lsp-ui-peek--deactivate-keymap-fn nil) (funcall fn))) (defun lsp-ui-peek--goto-xref (&optional x other-window) "Go to a reference/definition." (interactive) (-if-let (xref (or x (lsp-ui-peek--get-selection))) (-let (((&plist :file file :line line :column column) xref) (buffer (current-buffer))) (if (not (file-readable-p file)) (user-error "File not readable: %s" file) (setq lsp-ui-peek--win-start nil) (lsp-ui-peek--abort) (let ((marker (with-current-buffer (or (get-file-buffer file) (find-file-noselect file)) (save-restriction (widen) (save-excursion ;; When we jump to a file with line/column unspecified, ;; we do not want to move the point if the buffer exists. ;; We interpret line=column=0 differently here. (when (> (+ line column) 0) (goto-char 1) (forward-line line) (forward-char column)) (point-marker))))) (cur-buffer-workspaces (and (boundp 'lsp--buffer-workspaces) lsp--buffer-workspaces))) (if other-window (pop-to-buffer (marker-buffer marker) t) (switch-to-buffer (marker-buffer marker))) (with-current-buffer buffer (lsp-ui-peek-mode -1)) (unless lsp--buffer-workspaces (setq lsp--buffer-workspaces cur-buffer-workspaces) (lsp-mode 1) (dolist (workspace cur-buffer-workspaces) (lsp--open-in-workspace workspace))) (goto-char marker) (run-hooks 'xref-after-jump-hook)))) (lsp-ui-peek--toggle-file))) (defun lsp-ui-peek--goto-xref-other-window () (interactive) (lsp-ui-peek--goto-xref nil t)) (defvar lsp-ui-peek-mode-map (let ((map (make-sparse-keymap))) (suppress-keymap map t) (define-key map "\e\e\e" 'lsp-ui-peek--abort) (define-key map "\C-g" 'lsp-ui-peek--abort) (define-key map (kbd "M-n") 'lsp-ui-peek--select-next-file) (define-key map (kbd "") 'lsp-ui-peek--select-next-file) (define-key map (kbd "M-p") 'lsp-ui-peek--select-prev-file) (define-key map (kbd "") 'lsp-ui-peek--select-prev-file) (define-key map (kbd "C-n") 'lsp-ui-peek--select-next) (define-key map (kbd "n") 'lsp-ui-peek--select-next) (define-key map (kbd "") 'lsp-ui-peek--select-next) (define-key map (kbd "C-p") 'lsp-ui-peek--select-prev) (define-key map (kbd "p") 'lsp-ui-peek--select-prev) (define-key map (kbd "") 'lsp-ui-peek--select-prev) (define-key map (kbd "TAB") 'lsp-ui-peek--toggle-file) (define-key map (kbd "") 'lsp-ui-peek--toggle-file) (define-key map (kbd "q") 'lsp-ui-peek--abort) (define-key map (kbd "RET") 'lsp-ui-peek--goto-xref) (define-key map (kbd "M-RET") 'lsp-ui-peek--goto-xref-other-window) map) "Keymap for ‘lsp-ui-peek-mode’.") (defun lsp-ui-peek--disable () "Do not call this function, call `lsp-ui-peek--abort' instead." (when (bound-and-true-p lsp-ui-peek-mode) (lsp-ui-peek-mode -1) (lsp-ui-peek--peek-hide))) (defun lsp-ui-peek--abort () "Abort peek." (interactive) ;; The timer fixes https://github.com/emacs-lsp/lsp-ui/issues/33 (run-with-idle-timer 0 nil 'lsp-ui-peek--disable)) (define-minor-mode lsp-ui-peek-mode "Mode for lsp-ui-peek." :init-value nil (if lsp-ui-peek-mode (setq lsp-ui-peek--deactivate-keymap-fn (set-transient-map lsp-ui-peek-mode-map t 'lsp-ui-peek--abort)) (lsp-ui-peek--deactivate-keymap) (lsp-ui-peek--peek-hide))) (defun lsp-ui-peek--find-xrefs (input method param) "Find INPUT references. METHOD is ‘references’, ‘definitions’, `implementation` or a custom kind. PARAM is the request params." (setq lsp-ui-peek--method method) (let ((xrefs (lsp-ui-peek--get-references method param))) (unless xrefs (user-error "Not found for: %s" input)) (xref-push-marker-stack) (when (featurep 'evil-jumps) (lsp-ui-peek--with-evil-jumps (evil-set-jump))) (if (and (not lsp-ui-peek-always-show) (not (cdr xrefs)) (= (length (plist-get (car xrefs) :xrefs)) 1)) (let ((x (car (plist-get (car xrefs) :xrefs)))) (-if-let (uri (lsp:location-uri x)) (-let (((&Range :start (&Position :line :character)) (lsp:location-range x))) (lsp-ui-peek--goto-xref `(:file ,(lsp--uri-to-path uri) :line ,line :column ,character))) (-let (((&Range :start (&Position :line :character)) (or (lsp:location-link-target-selection-range x) (lsp:location-link-target-range x)))) (lsp-ui-peek--goto-xref `(:file ,(lsp--uri-to-path (lsp:location-link-target-uri x)) :line ,line :column ,character))))) (lsp-ui-peek-mode) (lsp-ui-peek--show xrefs)))) (defun lsp-ui-peek-find-references (&optional include-declaration extra) "Find references to the IDENTIFIER at point." (interactive) (lsp-ui-peek--find-xrefs (symbol-at-point) "textDocument/references" (append extra (lsp--make-reference-params nil include-declaration)))) (defun lsp-ui-peek-find-definitions (&optional extra) "Find definitions to the IDENTIFIER at point." (interactive) (lsp-ui-peek--find-xrefs (symbol-at-point) "textDocument/definition" (append extra (lsp--text-document-position-params)))) (defun lsp-ui-peek-find-implementation (&optional extra) "Find implementation locations of the symbol at point." (interactive) (lsp-ui-peek--find-xrefs (symbol-at-point) "textDocument/implementation" (append extra (lsp--text-document-position-params)))) (defun lsp-ui-peek-find-workspace-symbol (pattern &optional extra) "Find symbols in the worskpace. The symbols are found matching PATTERN." (interactive (list (read-string "workspace/symbol: " nil 'xref--read-pattern-history))) (lsp-ui-peek--find-xrefs pattern "workspace/symbol" (append extra (lsp-make-workspace-symbol-params :query pattern)))) (defun lsp-ui-peek-find-custom (method &optional extra) "Find custom references. KIND is a symbol to name the references (definition, reference, ..). REQUEST is the method string to send the the language server. EXTRA is a plist of extra parameters." (lsp-ui-peek--find-xrefs (symbol-at-point) method (append extra (lsp--text-document-position-params)))) (defun lsp-ui-peek--extract-chunk-from-buffer (pos start end) "Return the chunk of code pointed to by POS (a Position object) in the current buffer. START and END are delimiters." (let* ((point (lsp--position-to-point pos)) (inhibit-field-text-motion t) (line-start (1+ (- 1 (/ lsp-ui-peek-peek-height 2)))) (line-end (/ lsp-ui-peek-peek-height 2))) (save-excursion (goto-char point) (let* ((before (buffer-substring (line-beginning-position line-start) (line-beginning-position))) (line (buffer-substring (line-beginning-position) (line-end-position))) (after (buffer-substring (line-end-position) (line-end-position line-end))) (len (length line)) (chunk (concat before line after)) (start-in-chunk (length before))) (when (eq lsp-ui-peek-fontify 'on-demand) (setq chunk (lsp-ui-peek--render lsp--peek-save-major-mode chunk))) (remove-text-properties (+ (min start len) start-in-chunk) (+ (if (null end) len (min end len)) start-in-chunk) '(face nil) chunk) (add-face-text-property (+ (min start len) start-in-chunk) (+ (if (null end) len (min end len)) start-in-chunk) 'lsp-ui-peek-highlight t chunk) `(,(substring chunk start-in-chunk (+ start-in-chunk len)) . ,chunk))))) (defun lsp-ui-peek--xref-make-item (filename loc) "Return an item from FILENAME given a LOC. LOCATION can be either a LSP Location or SymbolInformation." ;; TODO: Read more informations from SymbolInformation. ;; For now, only the location is used. (-let* ((loc (or (lsp:symbol-information-location loc) loc)) (range (or (lsp:location-range loc) (lsp:location-link-target-selection-range loc) (lsp:location-link-target-range loc))) ((&Range :start pos-start :end pos-end) range) ((&Position :line start-line :character start-col) pos-start) ((&Position :line end-line :character end-col) pos-end) ((line . chunk) (lsp-ui-peek--extract-chunk-from-buffer pos-start start-col (when (= start-line end-line) end-col)))) (list :summary (or line filename) :chunk (or chunk filename) :file filename :line start-line :column start-col))) (defun lsp-ui-peek--fontify-buffer (filename) (when (eq lsp-ui-peek-fontify 'always) (unless buffer-file-name (make-local-variable 'delay-mode-hooks) (let ((buffer-file-name filename) (enable-local-variables nil) (inhibit-message t) (delay-mode-hooks t)) (set-auto-mode))) (font-lock-ensure))) (defun lsp-ui-peek--get-xrefs-in-file (file) "Return all references that contain a file. FILE is a cons where its car is the filename and the cdr is a list of Locations within the file. We open and/or create the file/buffer only once for all references. The function returns a list of `ls-xref-item'." (let* ((filename (car file)) (visiting (find-buffer-visiting filename)) (fn (lambda (loc) (lsp-ui-peek--xref-make-item filename loc)))) (setq lsp--peek-save-major-mode major-mode) (cond (visiting (with-temp-buffer (insert-buffer-substring-no-properties visiting) (lsp-ui-peek--fontify-buffer filename) (mapcar fn (cdr file)))) ((file-readable-p filename) (with-temp-buffer (insert-file-contents-literally filename) (lsp-ui-peek--fontify-buffer filename) (mapcar fn (cdr file)))) (t (user-error "Cannot read %s" filename))))) (defun lsp-ui-peek--get-xrefs-list (file) "Return a list of xrefs in FILE." (-let* (((filename . xrefs) file)) `(:file ,filename :xrefs ,xrefs :count ,(length xrefs)))) (defun lsp-ui-peek--get-references (method params) "Get all references/definitions for the symbol under point. Returns item(s)." (-when-let* ((locs (lsp-request method params)) (locs (if (listp locs) (if (symbolp (car locs)) ;; A single plist was returned (list locs) locs) (if (vectorp locs) (append locs nil) (list locs))))) (-filter (-lambda ((&plist :file)) (or (f-file? file) (ignore (lsp-log "The following file %s is missing, ignoring from the results." file)))) (mapcar #'lsp-ui-peek--get-xrefs-list (if (lsp:location-uri (car locs)) ;; Location[] (--group-by (lsp--uri-to-path (lsp:location-uri it)) locs) ;; LocationLink[] (--group-by (lsp--uri-to-path (lsp:location-link-target-uri it)) locs)))))) (defvar lsp-ui-mode-map) (defun lsp-ui-peek-enable (_enable) (interactive) (unless (bound-and-true-p lsp-ui-mode-map) (user-error "Please load lsp-ui before trying to enable lsp-ui-peek"))) ;; lsp-ui.el loads lsp-ui-peek.el, so we can’t ‘require’ lsp-ui. ;; FIXME: Remove this cyclic dependency. (declare-function lsp-ui--workspace-path "lsp-ui" (path)) (declare-function evil-set-jump "ext:evil-jumps.el" (&optional pos)) (provide 'lsp-ui-peek) ;;; lsp-ui-peek.el ends here