You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

273 lines
11 KiB

  1. ;;; xref-js2.el --- Jump to references/definitions using ag & js2-mode's AST -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2016 Nicolas Petton
  3. ;; Author: Nicolas Petton <nicolas@petton.fr>
  4. ;; URL: https://github.com/NicolasPetton/xref-js2
  5. ;; Keywords: javascript, convenience, tools
  6. ;; Version: 1.0
  7. ;; Package: xref-js2
  8. ;; Package-Requires: ((emacs "25") (js2-mode "20150909"))
  9. ;; This program is free software; you can redistribute it and/or modify
  10. ;; it under the terms of the GNU General Public License as published by
  11. ;; the Free Software Foundation, either version 3 of the License, or
  12. ;; (at your option) any later version.
  13. ;; This program is distributed in the hope that it will be useful,
  14. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;; GNU General Public License for more details.
  17. ;; You should have received a copy of the GNU General Public License
  18. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. ;;; Commentary:
  20. ;;
  21. ;; xref-js2 adds an xref backend for JavaScript files.
  22. ;;
  23. ;; Instead of using a tag system, it relies on `ag' to query the codebase of a
  24. ;; project. This might sound crazy at first, but it turns out that `ag' is so
  25. ;; fast that jumping using xref-js2 is most of the time instantaneous, even on
  26. ;; fairly large JavaScript codebase (it successfully works with 50k lines of JS
  27. ;; code).
  28. ;;
  29. ;; Because line by line regexp search has its downside, xref-js2 does a second
  30. ;; pass on result candidates and eliminates possible false positives using
  31. ;; `js2-mode''s AST, thus giving very accurate results.
  32. ;;; Code:
  33. (require 'subr-x)
  34. (require 'xref)
  35. (require 'seq)
  36. (require 'map)
  37. (require 'js2-mode)
  38. (require 'vc)
  39. (defcustom xref-js2-search-program 'ag
  40. "The backend program used for searching."
  41. :type 'symbol
  42. :group 'xref-js2
  43. :options '(ag rg))
  44. (defcustom xref-js2-ag-arguments '("--js" "--noheading" "--nocolor")
  45. "Default arguments passed to ag."
  46. :type 'list
  47. :group 'xref-js2)
  48. (defcustom xref-js2-js-extensions '("js" "mjs" "jsx" "ts" "tsx")
  49. "Extensions for file types xref-js2 is expected to search.
  50. warning, this is currently only supported by ripgrep, not ag.
  51. if an empty-list/nil no filtering based on file extension will
  52. take place."
  53. :type 'list
  54. :group 'xref-js2)
  55. (defcustom xref-js2-rg-arguments '("--no-heading"
  56. "--line-number" ; not activated by default on comint
  57. "--pcre2" ; provides regexp backtracking
  58. "--ignore-case" ; ag is case insensitive by default
  59. "--color" "never")
  60. "Default arguments passed to ripgrep."
  61. :type 'list
  62. :group 'xref-js2)
  63. (defcustom xref-js2-ignored-dirs '("bower_components"
  64. "node_modules"
  65. "build"
  66. "lib")
  67. "List of directories to be ignored when performing a search."
  68. :type 'list
  69. :group 'xref-js2)
  70. (defcustom xref-js2-ignored-files '("*.min.js")
  71. "List of files to be ignored when performing a search."
  72. :type 'list
  73. :group 'xref-js2)
  74. (defcustom xref-js2-definitions-regexps '("\\b%s\\b[\\s]*[:=][^=]"
  75. "function[\\s]+\\b%s\\b"
  76. "class[\\s]+\\b%s\\b"
  77. "(?<!new)[^.]%s[\\s]*\\(")
  78. "List of regular expressions that match definitions of a symbol.
  79. In each regexp string, '%s' is expanded with the searched symbol."
  80. :type 'list
  81. :group 'xref-js2)
  82. (defcustom xref-js2-references-regexps '("\\b%s\\b(?!\\s*[:=][^=])")
  83. "List of regular expressions that match references to a symbol.
  84. In each regexp string, '%s' is expanded with the searched symbol."
  85. :type 'list
  86. :group 'xref-js2)
  87. ;;;###autoload
  88. (defun xref-js2-xref-backend ()
  89. "Xref-Js2 backend for Xref."
  90. 'xref-js2)
  91. (cl-defmethod xref-backend-identifier-at-point ((_backend (eql xref-js2)))
  92. (symbol-name (symbol-at-point)))
  93. (cl-defmethod xref-backend-definitions ((_backend (eql xref-js2)) symbol)
  94. (xref-js2--xref-find-definitions symbol))
  95. (cl-defmethod xref-backend-references ((_backend (eql xref-js2)) symbol)
  96. (xref-js2--xref-find-references symbol))
  97. (defun xref-js2--xref-find-definitions (symbol)
  98. "Return a list of candidates matching SYMBOL."
  99. (seq-map (lambda (candidate)
  100. (xref-js2--make-xref candidate))
  101. (xref-js2--find-definitions symbol)))
  102. (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql xref-js2)))
  103. "Return a list of terms for completions taken from the symbols in the current buffer.
  104. The current implementation returns all the words in the buffer,
  105. which is really sub optimal."
  106. (let (words)
  107. (save-excursion
  108. (save-restriction
  109. (widen)
  110. (goto-char (point-min))
  111. (while (re-search-forward "\\w+" nil t)
  112. (add-to-list 'words (match-string-no-properties 0)))
  113. (seq-uniq words)))))
  114. (defun xref-js2--xref-find-references (symbol)
  115. "Return a list of reference candidates matching SYMBOL."
  116. (seq-map (lambda (candidate)
  117. (xref-js2--make-xref candidate))
  118. (xref-js2--find-references symbol)))
  119. (defun xref-js2--make-xref (candidate)
  120. "Return a new Xref object built from CANDIDATE."
  121. (xref-make (map-elt candidate 'match)
  122. (xref-make-file-location (map-elt candidate 'file)
  123. (map-elt candidate 'line)
  124. 0)))
  125. (defun xref-js2--find-definitions (symbol)
  126. "Return a list of definitions for SYMBOL from an ag search."
  127. (xref-js2--find-candidates
  128. symbol
  129. (xref-js2--make-regexp symbol xref-js2-definitions-regexps)))
  130. (defun xref-js2--find-references (symbol)
  131. "Return a list of references for SYMBOL from an ag search."
  132. (xref-js2--find-candidates
  133. symbol
  134. (xref-js2--make-regexp symbol xref-js2-references-regexps)))
  135. (defun xref-js2--make-regexp (symbol regexps)
  136. "Return a regular expression to search for SYMBOL using REGEXPS.
  137. REGEXPS must be a list of regular expressions, which are
  138. concatenated together into one regexp, expanding occurrences of
  139. '%s' with SYMBOL."
  140. (mapconcat #'identity
  141. (mapcar (lambda (str)
  142. (format str symbol))
  143. regexps) "|"))
  144. (defun xref-js2--find-candidates (symbol regexp)
  145. (let ((default-directory (xref-js2--root-dir))
  146. matches)
  147. (with-temp-buffer
  148. (let* ((search-tuple (cond ;; => (prog-name . function-to-get-args)
  149. ((eq xref-js2-search-program 'rg)
  150. '("rg" . xref-js2--search-rg-get-args))
  151. (t ;; (eq xref-js2-search-program 'ag)
  152. '("ag" . xref-js2--search-ag-get-args))))
  153. (search-program (car search-tuple))
  154. (search-args (remove nil ;; rm in case no search args given
  155. (funcall (cdr search-tuple) regexp))))
  156. (apply #'process-file (executable-find search-program) nil t nil search-args))
  157. (goto-char (point-max)) ;; NOTE maybe redundant
  158. (while (re-search-backward "^\\(.+\\)$" nil t)
  159. (push (match-string-no-properties 1) matches)))
  160. (seq-remove #'xref-js2--false-positive
  161. (seq-map (lambda (match)
  162. (xref-js2--candidate symbol match))
  163. matches))))
  164. (defun xref-js2--search-ag-get-args (regexp)
  165. "Aggregate command line arguments to search for REGEXP using ag."
  166. `(,@xref-js2-ag-arguments
  167. ,@(seq-mapcat (lambda (dir)
  168. (list "--ignore-dir" dir))
  169. xref-js2-ignored-dirs)
  170. ,@(seq-mapcat (lambda (file)
  171. (list "--ignore" file))
  172. xref-js2-ignored-files)
  173. ,regexp))
  174. (defun xref-js2--search-rg-get-args (regexp)
  175. "Aggregate command line arguments to search for REGEXP using ripgrep."
  176. `(,@xref-js2-rg-arguments
  177. ,@(if (not xref-js2-js-extensions)
  178. nil ;; no filtering based on extension
  179. (seq-mapcat (lambda (ext)
  180. (list "-g" (concat "*." ext)))
  181. xref-js2-js-extensions))
  182. ,@(seq-mapcat (lambda (dir)
  183. (list "-g" (concat "!" ; exclude not include
  184. dir ; directory string
  185. (unless (string-suffix-p "/" dir) ; pattern for a directory
  186. "/")))) ; must end with a slash
  187. xref-js2-ignored-dirs)
  188. ,@(seq-mapcat (lambda (pattern)
  189. (list "-g" (concat "!" pattern)))
  190. xref-js2-ignored-files)
  191. ,regexp))
  192. (defun xref-js2--false-positive (candidate)
  193. "Return non-nil if CANDIDATE is a false positive.
  194. Filtering is done using the AST from js2-mode."
  195. (let* ((file (map-elt candidate 'file))
  196. (buffer-open (get-file-buffer file)))
  197. (prog1
  198. (with-current-buffer (find-file-noselect file t)
  199. (save-excursion
  200. (save-restriction
  201. (widen)
  202. (unless (or (eq major-mode 'js2-mode)
  203. (seq-contains (map-keys minor-mode-alist) 'js2-minor-mode))
  204. (js2-minor-mode 1))
  205. (goto-char (point-min))
  206. (forward-line (1- (map-elt candidate 'line)))
  207. (search-forward (map-elt candidate 'symbol) nil t)
  208. ;; js2-mode fails to parse the AST for some minified files
  209. (ignore-errors
  210. (let ((node (js2-node-at-point)))
  211. (or (js2-string-node-p node)
  212. (js2-comment-node-p node))))))))))
  213. (defun xref-js2--root-dir ()
  214. "Return the root directory of the project."
  215. (or (ignore-errors
  216. (projectile-project-root))
  217. (ignore-errors
  218. (vc-root-dir))
  219. (user-error "You are not in a project")))
  220. (defun xref-js2--candidate (symbol match)
  221. "Return a candidate alist built from SYMBOL and a raw MATCH result.
  222. The MATCH is one output result from the ag search."
  223. (let* ((attrs (split-string match ":" t))
  224. (match (string-trim (mapconcat #'identity (cddr attrs) ":"))))
  225. ;; Some minified JS files might match a search. To avoid cluttering the
  226. ;; search result, we trim the output.
  227. (when (> (seq-length match) 100)
  228. (setq match (concat (seq-take match 100) "...")))
  229. (list (cons 'file (expand-file-name (car attrs) (xref-js2--root-dir)))
  230. (cons 'line (string-to-number (cadr attrs)))
  231. (cons 'symbol symbol)
  232. (cons 'match match))))
  233. (provide 'xref-js2)
  234. ;;; xref-js2.el ends here