summaryrefslogtreecommitdiff
path: root/lisp/progmodes/xref.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/progmodes/xref.el')
-rw-r--r--lisp/progmodes/xref.el525
1 files changed, 525 insertions, 0 deletions
diff --git a/lisp/progmodes/xref.el b/lisp/progmodes/xref.el
new file mode 100644
index 00000000000..f3dc4bd4cfd
--- /dev/null
+++ b/lisp/progmodes/xref.el
@@ -0,0 +1,525 @@
+;; xref.el --- Cross-referencing commands -*-lexical-binding:t-*-
+
+;; Copyright (C) 2014 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file provides a somewhat generic infrastructure for cross
+;; referencing commands, in particular "find-definition".
+;;
+;; Some part of the functionality must be implemented in a language
+;; dependent way and that's done by defining `xref-find-function',
+;; `xref-identifier-at-point-function' and
+;; `xref-identifier-completion-table-function', which see.
+;;
+;; A major mode should make these variables buffer-local first.
+;;
+;; `xref-find-function' can be called in several ways, see its
+;; description. It has to operate with "xref" and "location" values.
+;;
+;; One would usually call `make-xref' and `xref-make-file-location',
+;; `xref-make-buffer-location' or `xref-make-bogus-location' to create
+;; them.
+;;
+;; Each identifier must be represented as a string. Implementers can
+;; use string properties to store additional information about the
+;; identifier, but they should keep in mind that values returned from
+;; `xref-identifier-completion-table-function' should still be
+;; distinct, because the user can't see the properties when making the
+;; choice.
+;;
+;; See the functions `etags-xref-find' and `elisp-xref-find' for full
+;; examples.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'eieio)
+(require 'ring)
+
+(defgroup xref nil "Cross-referencing commands"
+ :group 'tools)
+
+
+;;; Locations
+
+(defclass xref-location () ()
+ :documentation "A location represents a position in a file or buffer.")
+
+;; If a backend decides to subclass xref-location it can provide
+;; methods for some of the following functions:
+(defgeneric xref-location-marker (location)
+ "Return the marker for LOCATION.")
+
+(defgeneric xref-location-group (location)
+ "Return a string used to group a set of locations.
+This is typically the filename.")
+
+;;;; Commonly needed location classes are defined here:
+
+;; FIXME: might be useful to have an optional "hint" i.e. a string to
+;; search for in case the line number is sightly out of date.
+(defclass xref-file-location (xref-location)
+ ((file :type string :initarg :file)
+ (line :type fixnum :initarg :line)
+ (column :type fixnum :initarg :column))
+ :documentation "A file location is a file/line/column triple.
+Line numbers start from 1 and columns from 0.")
+
+(defun xref-make-file-location (file line column)
+ "Create and return a new xref-file-location."
+ (make-instance 'xref-file-location :file file :line line :column column))
+
+(defmethod xref-location-marker ((l xref-file-location))
+ (with-slots (file line column) l
+ (with-current-buffer
+ (or (get-file-buffer file)
+ (let ((find-file-suppress-same-file-warnings t))
+ (find-file-noselect file)))
+ (save-restriction
+ (widen)
+ (save-excursion
+ (goto-char (point-min))
+ (beginning-of-line line)
+ (move-to-column column)
+ (point-marker))))))
+
+(defmethod xref-location-group ((l xref-file-location))
+ (oref l :file))
+
+(defclass xref-buffer-location (xref-location)
+ ((buffer :type buffer :initarg :buffer)
+ (position :type fixnum :initarg :position)))
+
+(defun xref-make-buffer-location (buffer position)
+ "Create and return a new xref-buffer-location."
+ (make-instance 'xref-buffer-location :buffer buffer :position position))
+
+(defmethod xref-location-marker ((l xref-buffer-location))
+ (with-slots (buffer position) l
+ (let ((m (make-marker)))
+ (move-marker m position buffer))))
+
+(defmethod xref-location-group ((l xref-buffer-location))
+ (with-slots (buffer) l
+ (or (buffer-file-name buffer)
+ (format "(buffer %s)" (buffer-name buffer)))))
+
+(defclass xref-bogus-location (xref-location)
+ ((message :type string :initarg :message
+ :reader xref-bogus-location-message))
+ :documentation "Bogus locations are sometimes useful to
+indicate errors, e.g. when we know that a function exists but the
+actual location is not known.")
+
+(defun xref-make-bogus-location (message)
+ "Create and return a new xref-bogus-location."
+ (make-instance 'xref-bogus-location :message message))
+
+(defmethod xref-location-marker ((l xref-bogus-location))
+ (user-error "%s" (oref l :message)))
+
+(defmethod xref-location-group ((_ xref-bogus-location)) "(No location)")
+
+;; This should be in elisp-mode.el, but it's preloaded, and we can't
+;; preload defclass and defmethod (at least, not yet).
+(defclass xref-elisp-location (xref-location)
+ ((symbol :type symbol :initarg :symbol)
+ (type :type symbol :initarg :type)
+ (file :type string :initarg :file
+ :reader xref-location-group))
+ :documentation "Location of an Emacs Lisp symbol definition.")
+
+(defun xref-make-elisp-location (symbol type file)
+ (make-instance 'xref-elisp-location :symbol symbol :type type :file file))
+
+(defmethod xref-location-marker ((l xref-elisp-location))
+ (with-slots (symbol type file) l
+ (let ((buffer-point
+ (pcase type
+ (`defun (find-function-search-for-symbol symbol nil file))
+ ((or `defvar `defface)
+ (find-function-search-for-symbol symbol type file))
+ (`feature
+ (cons (find-file-noselect file) 1)))))
+ (with-current-buffer (car buffer-point)
+ (goto-char (or (cdr buffer-point) (point-min)))
+ (point-marker)))))
+
+
+;;; Cross-reference
+
+(defclass xref--xref ()
+ ((description :type string :initarg :description
+ :reader xref--xref-description)
+ (location :type xref-location :initarg :location
+ :reader xref--xref-location))
+ :comment "An xref is used to display and locate constructs like
+variables or functions.")
+
+(defun xref-make (description location)
+ "Create and return a new xref.
+DESCRIPTION is a short string to describe the xref.
+LOCATION is an `xref-location'."
+ (make-instance 'xref--xref :description description :location location))
+
+
+;;; API
+
+(declare-function etags-xref-find "etags" (action id))
+(declare-function tags-lazy-completion-table "etags" ())
+
+;; For now, make the etags backend the default.
+(defvar xref-find-function #'etags-xref-find
+ "Function to look for cross-references.
+It can be called in several ways:
+
+ (definitions IDENTIFIER): Find definitions of IDENTIFIER. The
+result must be a list of xref objects. If no definitions can be
+found, return nil.
+
+ (references IDENTIFIER): Find references of IDENTIFIER. The
+result must be a list of xref objects. If no references can be
+found, return nil.
+
+ (apropos PATTERN): Find all symbols that match PATTERN. PATTERN
+is a regexp.
+
+IDENTIFIER can be any string returned by
+`xref-identifier-at-point-function', or from the table returned
+by `xref-identifier-completion-table-function'.
+
+To create an xref object, call `xref-make'.")
+
+(defvar xref-identifier-at-point-function #'xref-default-identifier-at-point
+ "Function to get the relevant identifier at point.
+
+The return value must be a string or nil. nil means no
+identifier at point found.
+
+If it's hard to determine the identifier precisely (e.g., because
+it's a method call on unknown type), the implementation can
+return a simple string (such as symbol at point) marked with a
+special text property which `xref-find-function' would recognize
+and then delegate the work to an external process.")
+
+(defvar xref-identifier-completion-table-function #'tags-lazy-completion-table
+ "Function that returns the completion table for identifiers.")
+
+(defun xref-default-identifier-at-point ()
+ (let ((thing (thing-at-point 'symbol)))
+ (and thing (substring-no-properties thing))))
+
+
+;;; misc utilities
+(defun xref--alistify (list key test)
+ "Partition the elements of LIST into an alist.
+KEY extracts the key from an element and TEST is used to compare
+keys."
+ (let ((alist '()))
+ (dolist (e list)
+ (let* ((k (funcall key e))
+ (probe (cl-assoc k alist :test test)))
+ (if probe
+ (setcdr probe (cons e (cdr probe)))
+ (push (cons k (list e)) alist))))
+ ;; Put them back in order.
+ (cl-loop for (key . value) in (reverse alist)
+ collect (cons key (reverse value)))))
+
+(defun xref--insert-propertized (props &rest strings)
+ "Insert STRINGS with text properties PROPS."
+ (let ((start (point)))
+ (apply #'insert strings)
+ (add-text-properties start (point) props)))
+
+(defun xref--search-property (property &optional backward)
+ "Search the next text range where text property PROPERTY is non-nil.
+Return the value of PROPERTY. If BACKWARD is non-nil, search
+backward."
+ (let ((next (if backward
+ #'previous-single-char-property-change
+ #'next-single-char-property-change))
+ (start (point))
+ (value nil))
+ (while (progn
+ (goto-char (funcall next (point) property))
+ (not (or (setq value (get-text-property (point) property))
+ (eobp)
+ (bobp)))))
+ (cond (value)
+ (t (goto-char start) nil))))
+
+
+;;; Marker stack (M-. pushes, M-, pops)
+
+(defcustom xref-marker-ring-length 16
+ "Length of the xref marker ring."
+ :type 'integer
+ :version "25.1")
+
+(defvar xref--marker-ring (make-ring xref-marker-ring-length)
+ "Ring of markers to implement the marker stack.")
+
+(defun xref-push-marker-stack ()
+ "Add point to the marker stack."
+ (ring-insert xref--marker-ring (point-marker)))
+
+;;;###autoload
+(defun xref-pop-marker-stack ()
+ "Pop back to where \\[xref-find-definitions] was last invoked."
+ (interactive)
+ (let ((ring xref--marker-ring))
+ (when (ring-empty-p ring)
+ (error "Marker stack is empty"))
+ (let ((marker (ring-remove ring 0)))
+ (switch-to-buffer (or (marker-buffer marker)
+ (error "The marked buffer has been deleted")))
+ (goto-char (marker-position marker))
+ (set-marker marker nil nil))))
+
+;; etags.el needs this
+(defun xref-clear-marker-stack ()
+ "Discard all markers from the marker stack."
+ (let ((ring xref--marker-ring))
+ (while (not (ring-empty-p ring))
+ (let ((marker (ring-remove ring)))
+ (set-marker marker nil nil)))))
+
+
+(defun xref--goto-location (location)
+ "Set buffer and point according to xref-location LOCATION."
+ (let ((marker (xref-location-marker location)))
+ (set-buffer (marker-buffer marker))
+ (cond ((and (<= (point-min) marker) (<= marker (point-max))))
+ (widen-automatically (widen))
+ (t (error "Location is outside accessible part of buffer")))
+ (goto-char marker)))
+
+(defun xref--pop-to-location (location &optional window)
+ "Goto xref-location LOCATION and display the buffer.
+WINDOW controls how the buffer is displayed:
+ nil -- switch-to-buffer
+ 'window -- pop-to-buffer (other window)
+ 'frame -- pop-to-buffer (other frame)"
+ (xref--goto-location location)
+ (cl-ecase window
+ ((nil) (switch-to-buffer (current-buffer)))
+ (window (pop-to-buffer (current-buffer) t))
+ (frame (let ((pop-up-frames t)) (pop-to-buffer (current-buffer) t)))))
+
+
+;;; XREF buffer (part of the UI)
+
+;; The xref buffer is used to display a set of xrefs.
+
+(defun xref--display-position (pos other-window recenter-arg)
+ ;; show the location, but don't hijack focus.
+ (with-selected-window (display-buffer (current-buffer) other-window)
+ (goto-char pos)
+ (recenter recenter-arg)))
+
+(defun xref--show-location (location)
+ (condition-case err
+ (progn
+ (xref--goto-location location)
+ (xref--display-position (point) t 1))
+ (user-error (message (error-message-string err)))))
+
+(defun xref--next-line (backward)
+ (let ((loc (xref--search-property 'xref-location backward)))
+ (when loc
+ (save-window-excursion
+ (xref--show-location loc)
+ (sit-for most-positive-fixnum)))))
+
+(defun xref-next-line ()
+ "Move to the next xref and display its source in the other window."
+ (interactive)
+ (xref--next-line nil))
+
+(defun xref-prev-line ()
+ "Move to the previous xref and display its source in the other window."
+ (interactive)
+ (xref--next-line t))
+
+(defun xref--location-at-point ()
+ (or (get-text-property (point) 'xref-location)
+ (error "No reference at point")))
+
+(defvar-local xref--window nil)
+
+(defun xref-goto-xref ()
+ "Jump to the xref at point and bury the xref buffer."
+ (interactive)
+ (let ((loc (xref--location-at-point))
+ (window xref--window))
+ (quit-window)
+ (xref--pop-to-location loc window)))
+
+(define-derived-mode xref--xref-buffer-mode fundamental-mode "XREF"
+ "Mode for displaying cross-references."
+ (setq buffer-read-only t))
+
+(let ((map xref--xref-buffer-mode-map))
+ (define-key map (kbd "q") #'quit-window)
+ (define-key map [remap next-line] #'xref-next-line)
+ (define-key map [remap previous-line] #'xref-prev-line)
+ (define-key map (kbd "RET") #'xref-goto-xref)
+
+ ;; suggested by Johan Claesson "to further reduce finger movement":
+ (define-key map (kbd ".") #'xref-next-line)
+ (define-key map (kbd ",") #'xref-prev-line))
+
+(defconst xref-buffer-name "*xref*"
+ "The name of the buffer to show xrefs.")
+
+(defun xref--insert-xrefs (xref-alist)
+ "Insert XREF-ALIST in the current-buffer.
+XREF-ALIST is of the form ((GROUP . (XREF ...)) ...). Where
+GROUP is a string for decoration purposes and XREF is an
+`xref--xref' object."
+ (cl-loop for ((group . xrefs) . more1) on xref-alist do
+ (xref--insert-propertized '(face bold) group "\n")
+ (cl-loop for (xref . more2) on xrefs do
+ (insert " ")
+ (with-slots (description location) xref
+ (xref--insert-propertized
+ (list 'xref-location location
+ 'face 'font-lock-keyword-face)
+ description))
+ (when (or more1 more2)
+ (insert "\n")))))
+
+(defun xref--analyze (xrefs)
+ "Find common filenames in XREFS.
+Return an alist of the form ((FILENAME . (XREF ...)) ...)."
+ (xref--alistify xrefs
+ (lambda (x)
+ (xref-location-group (xref--xref-location x)))
+ #'equal))
+
+(defun xref--show-xref-buffer (xrefs window)
+ (let ((xref-alist (xref--analyze xrefs)))
+ (with-current-buffer (get-buffer-create xref-buffer-name)
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (xref--insert-xrefs xref-alist)
+ (xref--xref-buffer-mode)
+ (pop-to-buffer (current-buffer))
+ (goto-char (point-min))
+ (setq xref--window window)
+ (current-buffer)))))
+
+
+;; This part of the UI seems fairly uncontroversial: it reads the
+;; identifier and deals with the single definition case.
+;;
+;; The controversial multiple definitions case is handed off to
+;; xref-show-xrefs-function.
+
+(defvar xref-show-xrefs-function 'xref--show-xref-buffer
+ "Function to display a list of xrefs.")
+
+(defun xref--show-xrefs (id kind xrefs window)
+ (cond
+ ((null xrefs)
+ (user-error "No known %s for: %s" kind id))
+ ((not (cdr xrefs))
+ (xref-push-marker-stack)
+ (xref--pop-to-location (xref--xref-location (car xrefs)) window))
+ (t
+ (xref-push-marker-stack)
+ (funcall xref-show-xrefs-function xrefs window))))
+
+(defun xref--read-identifier (prompt)
+ "Return the identifier at point or read it from the minibuffer."
+ (let ((id (funcall xref-identifier-at-point-function)))
+ (cond ((or current-prefix-arg (not id))
+ (completing-read prompt
+ (funcall xref-identifier-completion-table-function)
+ nil t id))
+ (t id))))
+
+
+;;; Commands
+
+(defun xref--find-definitions (id window)
+ (xref--show-xrefs id "definitions"
+ (funcall xref-find-function 'definitions id)
+ window))
+
+;;;###autoload
+(defun xref-find-definitions (identifier)
+ "Find the definition of the identifier at point.
+With prefix argument or when there's no identifier at point,
+prompt for it."
+ (interactive (list (xref--read-identifier "Find definitions of: ")))
+ (xref--find-definitions identifier nil))
+
+;;;###autoload
+(defun xref-find-definitions-other-window (identifier)
+ "Like `xref-find-definitions' but switch to the other window."
+ (interactive (list (xref--read-identifier "Find definitions of: ")))
+ (xref--find-definitions identifier 'window))
+
+;;;###autoload
+(defun xref-find-definitions-other-frame (identifier)
+ "Like `xref-find-definitions' but switch to the other frame."
+ (interactive (list (xref--read-identifier "Find definitions of: ")))
+ (xref--find-definitions identifier 'frame))
+
+;;;###autoload
+(defun xref-find-references (identifier)
+ "Find references to the identifier at point.
+With prefix argument, prompt for the identifier."
+ (interactive (list (xref--read-identifier "Find references of: ")))
+ (xref--show-xrefs identifier "references"
+ (funcall xref-find-function 'references identifier)
+ nil))
+
+;;;###autoload
+(defun xref-find-apropos (pattern)
+ "Find all meaningful symbols that match PATTERN.
+The argument has the same meaning as in `apropos'."
+ (interactive (list (read-from-minibuffer
+ "Search for pattern (word list or regexp): ")))
+ (require 'apropos)
+ (xref--show-xrefs pattern "apropos"
+ (funcall xref-find-function 'apropos
+ (apropos-parse-pattern
+ (if (string-equal (regexp-quote pattern) pattern)
+ ;; Split into words
+ (or (split-string pattern "[ \t]+" t)
+ (user-error "No word list given"))
+ pattern)))
+ nil))
+
+
+;;; Key bindings
+
+;;;###autoload (define-key esc-map "." #'xref-find-definitions)
+;;;###autoload (define-key esc-map "," #'xref-pop-marker-stack)
+;;;###autoload (define-key esc-map [?\C-.] #'xref-find-apropos)
+;;;###autoload (define-key ctl-x-4-map "." #'xref-find-definitions-other-window)
+;;;###autoload (define-key ctl-x-5-map "." #'xref-find-definitions-other-frame)
+
+
+(provide 'xref)
+
+;;; xref.el ends here