273 lines
11 KiB
EmacsLisp
273 lines
11 KiB
EmacsLisp
;;; xref-js2.el --- Jump to references/definitions using ag & js2-mode's AST -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 2016 Nicolas Petton
|
|
|
|
;; Author: Nicolas Petton <nicolas@petton.fr>
|
|
;; URL: https://github.com/NicolasPetton/xref-js2
|
|
;; Package-Version: 20190915.2032
|
|
;; Keywords: javascript, convenience, tools
|
|
;; Version: 1.0
|
|
;; Package: xref-js2
|
|
;; Package-Requires: ((emacs "25") (js2-mode "20150909"))
|
|
|
|
;; 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 of the License, 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. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
;;
|
|
;; xref-js2 adds an xref backend for JavaScript files.
|
|
;;
|
|
;; Instead of using a tag system, it relies on `ag' to query the codebase of a
|
|
;; project. This might sound crazy at first, but it turns out that `ag' is so
|
|
;; fast that jumping using xref-js2 is most of the time instantaneous, even on
|
|
;; fairly large JavaScript codebase (it successfully works with 50k lines of JS
|
|
;; code).
|
|
;;
|
|
;; Because line by line regexp search has its downside, xref-js2 does a second
|
|
;; pass on result candidates and eliminates possible false positives using
|
|
;; `js2-mode''s AST, thus giving very accurate results.
|
|
|
|
;;; Code:
|
|
|
|
(require 'subr-x)
|
|
(require 'xref)
|
|
(require 'seq)
|
|
(require 'map)
|
|
(require 'js2-mode)
|
|
(require 'vc)
|
|
|
|
(defcustom xref-js2-search-program 'ag
|
|
"The backend program used for searching."
|
|
:type 'symbol
|
|
:group 'xref-js2
|
|
:options '(ag rg))
|
|
|
|
(defcustom xref-js2-ag-arguments '("--js" "--noheading" "--nocolor")
|
|
"Default arguments passed to ag."
|
|
:type 'list
|
|
:group 'xref-js2)
|
|
|
|
(defcustom xref-js2-js-extensions '("js" "mjs" "jsx" "ts" "tsx")
|
|
"Extensions for file types xref-js2 is expected to search.
|
|
warning, this is currently only supported by ripgrep, not ag.
|
|
|
|
if an empty-list/nil no filtering based on file extension will
|
|
take place."
|
|
:type 'list
|
|
:group 'xref-js2)
|
|
|
|
(defcustom xref-js2-rg-arguments '("--no-heading"
|
|
"--line-number" ; not activated by default on comint
|
|
"--pcre2" ; provides regexp backtracking
|
|
"--ignore-case" ; ag is case insensitive by default
|
|
"--color" "never")
|
|
"Default arguments passed to ripgrep."
|
|
:type 'list
|
|
:group 'xref-js2)
|
|
|
|
(defcustom xref-js2-ignored-dirs '("bower_components"
|
|
"node_modules"
|
|
"build"
|
|
"lib")
|
|
"List of directories to be ignored when performing a search."
|
|
:type 'list
|
|
:group 'xref-js2)
|
|
|
|
(defcustom xref-js2-ignored-files '("*.min.js")
|
|
"List of files to be ignored when performing a search."
|
|
:type 'list
|
|
:group 'xref-js2)
|
|
|
|
(defcustom xref-js2-definitions-regexps '("\\b%s\\b[\\s]*[:=][^=]"
|
|
"function[\\s]+\\b%s\\b"
|
|
"class[\\s]+\\b%s\\b"
|
|
"(?<!new)[^.]%s[\\s]*\\(")
|
|
"List of regular expressions that match definitions of a symbol.
|
|
In each regexp string, '%s' is expanded with the searched symbol."
|
|
:type 'list
|
|
:group 'xref-js2)
|
|
|
|
(defcustom xref-js2-references-regexps '("\\b%s\\b(?!\\s*[:=][^=])")
|
|
"List of regular expressions that match references to a symbol.
|
|
In each regexp string, '%s' is expanded with the searched symbol."
|
|
:type 'list
|
|
:group 'xref-js2)
|
|
|
|
;;;###autoload
|
|
(defun xref-js2-xref-backend ()
|
|
"Xref-Js2 backend for Xref."
|
|
'xref-js2)
|
|
|
|
(cl-defmethod xref-backend-identifier-at-point ((_backend (eql xref-js2)))
|
|
(symbol-name (symbol-at-point)))
|
|
|
|
(cl-defmethod xref-backend-definitions ((_backend (eql xref-js2)) symbol)
|
|
(xref-js2--xref-find-definitions symbol))
|
|
|
|
(cl-defmethod xref-backend-references ((_backend (eql xref-js2)) symbol)
|
|
(xref-js2--xref-find-references symbol))
|
|
|
|
(defun xref-js2--xref-find-definitions (symbol)
|
|
"Return a list of candidates matching SYMBOL."
|
|
(seq-map (lambda (candidate)
|
|
(xref-js2--make-xref candidate))
|
|
(xref-js2--find-definitions symbol)))
|
|
|
|
(cl-defmethod xref-backend-identifier-completion-table ((_backend (eql xref-js2)))
|
|
"Return a list of terms for completions taken from the symbols in the current buffer.
|
|
|
|
The current implementation returns all the words in the buffer,
|
|
which is really sub optimal."
|
|
(let (words)
|
|
(save-excursion
|
|
(save-restriction
|
|
(widen)
|
|
(goto-char (point-min))
|
|
(while (re-search-forward "\\w+" nil t)
|
|
(add-to-list 'words (match-string-no-properties 0)))
|
|
(seq-uniq words)))))
|
|
|
|
(defun xref-js2--xref-find-references (symbol)
|
|
"Return a list of reference candidates matching SYMBOL."
|
|
(seq-map (lambda (candidate)
|
|
(xref-js2--make-xref candidate))
|
|
(xref-js2--find-references symbol)))
|
|
|
|
(defun xref-js2--make-xref (candidate)
|
|
"Return a new Xref object built from CANDIDATE."
|
|
(xref-make (map-elt candidate 'match)
|
|
(xref-make-file-location (map-elt candidate 'file)
|
|
(map-elt candidate 'line)
|
|
0)))
|
|
|
|
(defun xref-js2--find-definitions (symbol)
|
|
"Return a list of definitions for SYMBOL from an ag search."
|
|
(xref-js2--find-candidates
|
|
symbol
|
|
(xref-js2--make-regexp symbol xref-js2-definitions-regexps)))
|
|
|
|
(defun xref-js2--find-references (symbol)
|
|
"Return a list of references for SYMBOL from an ag search."
|
|
(xref-js2--find-candidates
|
|
symbol
|
|
(xref-js2--make-regexp symbol xref-js2-references-regexps)))
|
|
|
|
(defun xref-js2--make-regexp (symbol regexps)
|
|
"Return a regular expression to search for SYMBOL using REGEXPS.
|
|
|
|
REGEXPS must be a list of regular expressions, which are
|
|
concatenated together into one regexp, expanding occurrences of
|
|
'%s' with SYMBOL."
|
|
(mapconcat #'identity
|
|
(mapcar (lambda (str)
|
|
(format str symbol))
|
|
regexps) "|"))
|
|
|
|
(defun xref-js2--find-candidates (symbol regexp)
|
|
(let ((default-directory (xref-js2--root-dir))
|
|
matches)
|
|
(with-temp-buffer
|
|
(let* ((search-tuple (cond ;; => (prog-name . function-to-get-args)
|
|
((eq xref-js2-search-program 'rg)
|
|
'("rg" . xref-js2--search-rg-get-args))
|
|
(t ;; (eq xref-js2-search-program 'ag)
|
|
'("ag" . xref-js2--search-ag-get-args))))
|
|
(search-program (car search-tuple))
|
|
(search-args (remove nil ;; rm in case no search args given
|
|
(funcall (cdr search-tuple) regexp))))
|
|
(apply #'process-file (executable-find search-program) nil t nil search-args))
|
|
|
|
(goto-char (point-max)) ;; NOTE maybe redundant
|
|
(while (re-search-backward "^\\(.+\\)$" nil t)
|
|
(push (match-string-no-properties 1) matches)))
|
|
(seq-remove #'xref-js2--false-positive
|
|
(seq-map (lambda (match)
|
|
(xref-js2--candidate symbol match))
|
|
matches))))
|
|
|
|
(defun xref-js2--search-ag-get-args (regexp)
|
|
"Aggregate command line arguments to search for REGEXP using ag."
|
|
`(,@xref-js2-ag-arguments
|
|
,@(seq-mapcat (lambda (dir)
|
|
(list "--ignore-dir" dir))
|
|
xref-js2-ignored-dirs)
|
|
,@(seq-mapcat (lambda (file)
|
|
(list "--ignore" file))
|
|
xref-js2-ignored-files)
|
|
,regexp))
|
|
|
|
(defun xref-js2--search-rg-get-args (regexp)
|
|
"Aggregate command line arguments to search for REGEXP using ripgrep."
|
|
`(,@xref-js2-rg-arguments
|
|
,@(if (not xref-js2-js-extensions)
|
|
nil ;; no filtering based on extension
|
|
(seq-mapcat (lambda (ext)
|
|
(list "-g" (concat "*." ext)))
|
|
xref-js2-js-extensions))
|
|
,@(seq-mapcat (lambda (dir)
|
|
(list "-g" (concat "!" ; exclude not include
|
|
dir ; directory string
|
|
(unless (string-suffix-p "/" dir) ; pattern for a directory
|
|
"/")))) ; must end with a slash
|
|
xref-js2-ignored-dirs)
|
|
,@(seq-mapcat (lambda (pattern)
|
|
(list "-g" (concat "!" pattern)))
|
|
xref-js2-ignored-files)
|
|
,regexp))
|
|
|
|
(defun xref-js2--false-positive (candidate)
|
|
"Return non-nil if CANDIDATE is a false positive.
|
|
Filtering is done using the AST from js2-mode."
|
|
(let* ((file (map-elt candidate 'file))
|
|
(buffer-open (get-file-buffer file)))
|
|
(prog1
|
|
(with-current-buffer (find-file-noselect file t)
|
|
(save-excursion
|
|
(save-restriction
|
|
(widen)
|
|
(unless (or (eq major-mode 'js2-mode)
|
|
(seq-contains (map-keys minor-mode-alist) 'js2-minor-mode))
|
|
(js2-minor-mode 1))
|
|
(goto-char (point-min))
|
|
(forward-line (1- (map-elt candidate 'line)))
|
|
(search-forward (map-elt candidate 'symbol) nil t)
|
|
;; js2-mode fails to parse the AST for some minified files
|
|
(ignore-errors
|
|
(let ((node (js2-node-at-point)))
|
|
(or (js2-string-node-p node)
|
|
(js2-comment-node-p node))))))))))
|
|
|
|
(defun xref-js2--root-dir ()
|
|
"Return the root directory of the project."
|
|
(or (ignore-errors
|
|
(projectile-project-root))
|
|
(ignore-errors
|
|
(vc-root-dir))
|
|
(user-error "You are not in a project")))
|
|
|
|
(defun xref-js2--candidate (symbol match)
|
|
"Return a candidate alist built from SYMBOL and a raw MATCH result.
|
|
The MATCH is one output result from the ag search."
|
|
(let* ((attrs (split-string match ":" t))
|
|
(match (string-trim (mapconcat #'identity (cddr attrs) ":"))))
|
|
;; Some minified JS files might match a search. To avoid cluttering the
|
|
;; search result, we trim the output.
|
|
(when (> (seq-length match) 100)
|
|
(setq match (concat (seq-take match 100) "...")))
|
|
(list (cons 'file (expand-file-name (car attrs) (xref-js2--root-dir)))
|
|
(cons 'line (string-to-number (cadr attrs)))
|
|
(cons 'symbol symbol)
|
|
(cons 'match match))))
|
|
|
|
(provide 'xref-js2)
|
|
;;; xref-js2.el ends here
|