;;; xref-js2.el --- Jump to references/definitions using ag & js2-mode's AST -*- lexical-binding: t; -*- ;; Copyright (C) 2016 Nicolas Petton ;; Author: Nicolas Petton ;; 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 . ;;; 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" "(? (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