Personal emacs config
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.

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