diff --git a/init.example.el b/init.example.el
index 3a7a691a7..b683638f8 100644
--- a/init.example.el
+++ b/init.example.el
@@ -34,7 +34,9 @@
eval ; run code, run (also, repls)
evil ; come to the dark side, we have cookies
file-templates ; auto-snippets for empty files
- jump ; helping you get around
+ (lookup ; helps you navigate your code and documentation
+ +devdocs ; ...on devdocs.io online
+ +docsets) ; ...or in Dash docsets locally
services ; TODO managing external services & code builders
snippets ; my elves. They type so I don't have to
spellcheck ; tasing you for misspelling mispelling
diff --git a/modules/feature/jump/autoload/evil.el b/modules/feature/jump/autoload/evil.el
deleted file mode 100644
index 51142c206..000000000
--- a/modules/feature/jump/autoload/evil.el
+++ /dev/null
@@ -1,11 +0,0 @@
-;;; feature/jump/autoload/evil.el -*- lexical-binding: t; -*-
-;;;###if (featurep! :feature evil)
-
-;;;###autoload (autoload '+jump:online "feature/jump/autoload/evil" nil t)
-(evil-define-command +jump:online (query &optional bang)
- "Look up QUERY online. Will prompt for search engine the first time, then
-reuse it on consecutive uses of this command. If BANG, always prompt for search
-engine."
- (interactive "")
- (+jump/online (or query (thing-at-point 'symbol t))
- (+jump--online-get-provider bang)))
diff --git a/modules/feature/jump/autoload/jump.el b/modules/feature/jump/autoload/jump.el
deleted file mode 100644
index 36ad3c49c..000000000
--- a/modules/feature/jump/autoload/jump.el
+++ /dev/null
@@ -1,146 +0,0 @@
-;;; feature/jump/autoload.el -*- lexical-binding: t; -*-
-
-(defvar +jump--rg-installed-p (executable-find "rg"))
-(defvar +jump--ag-installed-p (executable-find "ag"))
-
-(defun +jump-to (prop identifier &optional other-window)
- (with-selected-window
- (if other-window
- (save-excursion (other-window 1) (selected-window))
- (selected-window))
- (let ((fn (plist-get +jump-current-functions prop)))
- (if (commandp fn)
- (call-interactively fn)
- (funcall fn identifier)))))
-
-;;;###autoload
-(defun +jump/definition (identifier &optional other-window)
- "Jump to the definition of the symbol at point.
-
-Tries xref and falls back to `dumb-jump', then rg/ag, then
-`evil-goto-definition' (if evil is active)."
- (interactive
- (list (thing-at-point 'symbol t)
- current-prefix-arg))
- (cond ((null identifier)
- (user-error "Nothing under point"))
-
- ((plist-member +jump-current-functions :definition)
- (+jump-to :definition identifier))
-
- ((ignore-errors (if other-window
- (xref-find-definitions-other-window identifier)
- (xref-find-definitions identifier))
- t))
-
- ((and (require 'dumb-jump nil t)
- ;; dumb-jump doesn't tell us if it succeeded or not
- (let ((old-fn (symbol-function 'dumb-jump-get-results))
- successful)
- (cl-letf (((symbol-function 'dumb-jump-get-results)
- (lambda (&optional prompt)
- (let* ((plist (funcall old-fn prompt))
- (results (plist-get plist :results)))
- (when (and results (> (length results) 0))
- (setq successful t))
- plist))))
- (if other-window
- (dumb-jump-go-other-window)
- (dumb-jump-go))
- successful))))
-
- ((and identifier
- (featurep 'counsel)
- (let ((regex (rxt-quote-pcre identifier)))
- (or (and +jump--rg-installed-p
- (counsel-rg regex (doom-project-root)))
- (and +jump--ag-installed-p
- (counsel-ag regex (doom-project-root)))))))
-
- ((and (featurep 'evil)
- evil-mode
- (cl-destructuring-bind (beg . end)
- (bounds-of-thing-at-point 'symbol)
- (evil-goto-definition)
- (let ((pt (point)))
- (not (and (>= pt beg)
- (< pt end)))))))
-
- (t (user-error "Couldn't find '%s'" identifier))))
-
-;;;###autoload
-(defun +jump/references (identifier)
- "Show a list of references to the symbol at point.
-
-Tries `xref-find-references' and falls back to rg/ag."
- (interactive (list (thing-at-point 'symbol t)))
- (cond ((plist-member +jump-current-functions :references)
- (+jump-to :references identifier))
-
- ((ignore-errors (xref-find-references identifier)
- t))
-
- ((and identifier
- (featurep 'counsel)
- (let ((regex (rxt-quote-pcre identifier)))
- (or (and (executable-find "rg")
- (counsel-rg regex (doom-project-root)))
- (and (executable-find "ag")
- (counsel-ag regex (doom-project-root)))))))
-
- (t (error "Couldn't find '%s'" identifier))))
-
-;;;###autoload
-(defun +jump/documentation (identifier)
- "Show documentation for the symbol at point, if available."
- (interactive (list (thing-at-point 'symbol t)))
- (cond ((plist-member +jump-current-functions :documentation)
- (+jump-to :documentation identifier))
- (t
- (+jump/online
- identifier
- (+jump--online-get-provider (not current-prefix-arg))))))
-
-(defun +jump--online-get-provider (&optional force-p)
- (or (and (not force-p)
- +jump--online-last)
- (completing-read "Search on: "
- (mapcar #'car +jump-search-provider-alist)
- nil t)))
-
-(defvar +jump--online-last nil)
-;;;###autoload
-(defun +jump/online (search &optional provider)
- "Looks up SEARCH (a string) in you browser using PROVIDER.
-
-PROVIDER should be a key of `+jump-search-provider-alist'.
-
-When used interactively, it will prompt for a query and, for the first time, the
-provider from `+jump-search-provider-alist'. On consecutive uses, the last
-provider will be reused. If the universal argument is supplied, always prompt
-for the provider."
- (interactive
- (list (or (and (region-active-p)
- (buffer-substring-no-properties (region-beginning)
- (region-end)))
- (read-string "Search for: " (thing-at-point 'symbol t)))
- (+jump--online-get-provider current-prefix-arg)))
- (condition-case _ex
- (let ((url (cdr (assoc provider +jump-search-provider-alist))))
- (unless url
- (error "'%s' is an invalid search engine" provider))
- (when (or (functionp url) (symbolp url))
- (setq url (funcall url)))
- (cl-assert (and (stringp url) (not (string-empty-p url))))
- (when (string-empty-p search)
- (user-error "The search query is empty"))
- (setq +jump--online-last provider)
- (funcall +jump-search-browser-fn (format url (url-encode-url search))))
- ('error (setq +jump--online-last nil))))
-
-;;;###autoload
-(defun +jump/online-select ()
- "Runs `+jump/online', but always prompts for the provider to use."
- (interactive)
- (let ((current-prefix-arg t))
- (call-interactively #'+jump/online)))
diff --git a/modules/feature/jump/config.el b/modules/feature/jump/config.el
deleted file mode 100644
index f4e005446..000000000
--- a/modules/feature/jump/config.el
+++ /dev/null
@@ -1,106 +0,0 @@
-;;; feature/jump/config.el -*- lexical-binding: t; -*-
-
-;; "What am I looking at?"
-;;
-;; This module helps you answer that question. It helps you look up whatever
-;; you're looking at.
-;;
-;; + `+jump/definition': a jump-to-definition that should 'just work'
-;; + `+jump/references': find a symbol's references in the current project
-;; + `+jump/online'; look up a symbol on online resources, like stackoverflow,
-;; devdocs.io or google.
-;;
-;; This module uses `xref', an experimental new library in Emacs. It may change
-;; in the future. When xref can't be depended on it will fall back to
-;; `dumb-jump' to find what you want.
-
-(defvar +jump-search-provider-alist
- '(("Google" . "https://google.com/search?q=%s")
- ("Google images" . "https://google.com/images?q=%s")
- ("Google maps" . "https://maps.google.com/maps?q=%s")
- ("Project Gutenberg" . "http://www.gutenberg.org/ebooks/search/?query=%s")
- ("DuckDuckGo" . "https://duckduckgo.com/?q=%s")
- ("DevDocs.io" . "http://devdocs.io/#q=%s")
- ("StackOverflow" . "https://stackoverflow.com/search?q=%s")
- ("Github" . "https://github.com/search?ref=simplesearch&q=%s")
- ("Youtube" . "https://youtube.com/results?aq=f&oq=&search_query=%s")
- ("Wolfram alpha" . "https://wolframalpha.com/input/?i=%s")
- ("Wikipedia" . "https://wikipedia.org/search-redirect.php?language=en&go=Go&search=%s"))
- "An alist that maps online resources to their search url or a function that
-produces an url. Used by `+jump/online'.")
-
-(defvar +jump-search-browser-fn #'browse-url
- "Function to use to open search urls.")
-
-(defvar +jump-function-alist nil
- "An alist mapping major modes to jump function plists, describing what to do
-with `+jump/definition', `+jump/references' and `+jump/documentation' are
-called.")
-
-(defvar-local +jump-current-functions nil
- "The enabled jump functions for the current buffer.")
-
-(def-setting! :jump (modes &rest plist)
- "Definies a jump target for major MODES. PLIST accepts the following
-properties:
-
- :definition FN
- Run when jumping to a symbol's definition.
- Used by `+jump/definition'.
- :references FN
- Run when looking for usage references of a symbol in the current project.
- Used by `+jump/references'.
- :documentation FN
- Run when looking up documentation for a symbol.
- Used by `+jump/documentation'."
- `(dolist (mode (doom-enlist ,modes))
- (push (cons mode (list ,@plist)) +jump-function-alist)))
-
-;; Let me control what backends to fall back on
-(setq-default xref-backend-functions '(t))
-
-(set! :popup "*xref*" :noselect t :autokill t :autoclose t)
-
-;; Recenter after certain jumps
-(add-hook!
- '(imenu-after-jump-hook evil-jumps-post-jump-hook
- counsel-grep-post-action-hook dumb-jump-after-jump-hook)
- #'recenter)
-
-(defun +jump|init ()
- "Initialize `+jump-current-functions', used by `+jump/definition',
-`+jump/references' and `+jump/documentation'."
- (when-let* ((plist (cdr (assq major-mode +jump-function-alist))))
- (when-let* ((backend (plist-get plist :xref-backend)))
- (make-variable-buffer-local 'xref-backend-functions)
- (cl-pushnew backend xref-backend-functions :test #'eq))
- (setq-local +jump-current-functions plist)))
-(add-hook 'after-change-major-mode-hook #'+jump|init)
-
-
-;;
-;; Packages
-;;
-
-(def-package! ivy-xref
- :when (featurep! :completion ivy)
- :after xref
- :config (setq xref-show-xrefs-function #'ivy-xref-show-xrefs))
-
-
-(def-package! helm-xref
- :when (featurep! :completion helm)
- :after xref
- :config (setq xref-show-xrefs-function #'helm-xref-show-xrefs))
-
-
-(def-package! dumb-jump
- :commands (dumb-jump-go dumb-jump-quick-look
- dumb-jump-back dumb-jump-result-follow)
- :config
- (setq dumb-jump-default-project doom-emacs-dir
- dumb-jump-aggressive nil
- dumb-jump-selector (cond ((featurep! :completion ivy) 'ivy)
- ((featurep! :completion helm) 'helm)
- (t 'popup))))
-
diff --git a/modules/feature/jump/packages.el b/modules/feature/jump/packages.el
deleted file mode 100644
index 712158bb2..000000000
--- a/modules/feature/jump/packages.el
+++ /dev/null
@@ -1,8 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; feature/jump/packages.el
-
-(package! dumb-jump)
-(when (featurep! :completion ivy)
- (package! ivy-xref))
-(when (featurep! :completion helm)
- (package! helm-xref))
diff --git a/modules/feature/lookup/README.org b/modules/feature/lookup/README.org
new file mode 100644
index 000000000..8ca362370
--- /dev/null
+++ b/modules/feature/lookup/README.org
@@ -0,0 +1,182 @@
+#+TITLE: :feature lookup
+
+Integrates with code navigation and documentation tools to help you quickly look
+up definitions, references and documentation.
+
++ Integration with devdocs.io
++ Integration with Dash.app docsets.
++ Jump-to-definition and find-references implementations that just work.
++ Powerful xref integration for languages that support it.
+
+* Table of Contents :TOC:
+- [[#install][Install]]
+ - [[#module-flags][Module flags]]
+ - [[#dependencies][Dependencies]]
+- [[#usage][Usage]]
+ - [[#jump-to-definition][Jump to definition]]
+ - [[#find-references][Find references]]
+ - [[#look-up-documentation][Look up documentation]]
+ - [[#search-a-specific-documentation-backend][Search a specific documentation backend]]
+- [[#configuration][Configuration]]
+ - [[#settings][Settings]]
+ - [[#open-in-eww-instead-of-browser][Open in eww instead of browser]]
+- [[#appendix][Appendix]]
+ - [[#commands][Commands]]
+
+* Install
+To enable the module add =:feature lookup= to your ~doom!~ block in
+=~/.emacs.d/init.el=.
+
+** Module flags
+This module provides two flags:
+
++ ~+docsets~ Enables integration with Dash docsets.
++ ~+devdocs~ Enables integration with devdocs.io search.
+
+** Dependencies
+This module has several soft dependencies:
+
++ ~the_silver_searcher~ and/or ~ripgrep~ as a last-resort fallback for
+ jump-to-definition/find-references.
++ ~sqlite3~ for Dash docset support.
+
+*** MacOS
+#+BEGIN_SRC sh :tangle (if (doom-system-os 'macos) "yes")
+brew install the_silver_searcher ripgrep
+
+# An older version of sqlite is included in MacOS. If it causes you problems (and
+# it has been reported that it will), install it through homebrew:
+brew install sqlite
+# Note that it's keg-only, meaning it isn't symlinked to /usr/local/bin. You'll
+# have to add it to PATH yourself (or symlink it into your PATH somewhere). e.g.
+export PATH="/usr/local/opt/sqlite/bin:$PATH"
+#+END_SRC
+
+*** Arch Linux
+#+BEGIN_SRC sh :dir /sudo:: :tangle (if (doom-system-os 'arch) "yes")
+sudo pacman --needed --noconfirm -S sqlite the_silver_searcher ripgrep
+#+END_SRC
+
+* Usage
+** Jump to definition
+Use ~+lookup/definition~ (bound to =gd= in normal mode) to jump to the
+definition of the symbol at point
+
+This module provides a goto-definition implementation that will try the
+following sources before giving up:
+
+1. Whatever ~:definition~ function is registered for the current buffer with the
+ ~:lookup~ setting (see "Configuration" section).
+2. Any available xref backends.
+3. ~dumb-jump~ (a text search with aides to reduce false positives).
+3. An ordinary project-wide text search with ripgrep or the_silver_searcher.
+5. If ~evil-mode~ is active, use ~evil-goto-definition~, which preforms a simple
+ text search within the current buffer.
+
+If there are multiple results, you will be prompted to select one.
+
+** Find references
+Use ~+lookup/references~ (bound to =gD= in normal mode) to see a list of
+references for the symbol at point from throughout your project.
+
+Like ~+lookup/definition~, this tries a number of sources before giving up. It
+will try:
+
+1. Whatever ~:references~ function is registered for the current buffer with the
+ ~:lookup~ setting (see "Configuration" section).
+2. Any available xref backends.
+3. An ordinary project-wide text search with ripgrep or the_silver_searcher.
+
+If there are multiple results, you will be prompted to select one.
+
+** Look up documentation
+~+lookup/documentation~ (bound to =gh= in normal mode) will open documentation
+for the symbol at point.
+
+Depending on your configuration, this will try a list of sources:
+
+1. Whatever ~:documentation~ function is registered for the current buffer with
+ the ~:lookup~ setting (see "Configuration" section).
+2. Any Dash.app docsets, if any are installed for the current major mode.
+3. devdocs.io, if it has a docset for the current mode.
+4. An online search; using the last engine used (it will prompt you the first
+ time, or if ~current-prefix-arg~ is non-nil).
+
+** Search a specific documentation backend
+You can perform a documentation lookup on any backends directly:
+
++ Dash Docsets: ~+lookup/in-docsets~, or ~:dash QUERY~ for evil users.
++ devdocs.io: ~+lookup/in-devdocs~, or ~:dd QUERY~ for evil users.
++ Online (generic): ~+lookup/online~ or ~+lookup/online-select~ (bound to =SPC /
+ o=), or ~:lo[okup] QUERY~ for evil users.
+
+* Configuration
+** Settings
+This module provides three settings: ~:lookup~, ~:docset~ and ~:devdocs~.
+
+*** ~:lookup MODES &rest PLIST~
+Defines a lookup target for major MODES (one major-mode symbol or a list
+thereof). PLIST accepts the following optional properties:
+
++ ~:definition FN~ :: Run when jumping to a symbol's definition. Used by
+ ~+lookup/definition~.
++ ~:references FN~ :: Run when looking for usage references of a symbol in the
+ current project. Used by ~+lookup/references~.
++ ~:documentation FN~ :: Run when looking up documentation for a symbol. Used by
+ ~+lookup/documentation~.
++ ~:xref-backend FN~ :: Defines an xref backend for a major-mode. With this,
+ :definition and :references are unnecessary.
+
+**** Example
+#+BEGIN_SRC emacs-lisp
+;; For python-mode, anaconda-mode offers a backend for all three lookup
+;; functions. We can register them like so:
+(set! :lookup 'python-mode
+ :definition #'anaconda-mode-find-definitions
+ :references #'anaconda-mode-find-references
+ :documentation #'anaconda-mode-show-doc)
+
+;; If a language or plugin provides a custom xref backend available for it, use
+;; that instead. It will provide the best jump-to-definition and find-references
+;; experience. You can specify custom xref backends with:
+(set! :lookup 'js2-mode :xref-backend #'xref-js2-xref-backend)
+;; NOTE: xref doesn't provide a :documentation backend.
+#+END_SRC
+
+*** ~:docset MODES &rest DOCSETS~
+Registers a list of DOCSETS (strings) for MODES (either one major mode symbol or
+a list of them). Used by ~+lookup/in-docsets~ and ~+lookup/documentation~.
+
+#+BEGIN_SRC emacs-lisp
+(set! :docset 'js2-mode "JavaScript" "JQuery")
+;; Add docsets to minor modes by starting DOCSETS with :add
+(set! :docset 'rjsx-mode :add "React")
+;; Or remove docsets from minor modes
+(set! :docset 'nodejs-mode :remove "JQuery")
+#+END_SRC
+
+*** ~:devdocs MODES DOCSET~
+Registers a devdocs DOCset (one string) to search when in MODES (either one
+major mode symbol or a list). Used by ~+lookup/in-devdocs~ and
+~+lookup/documentation~. With devdocs you can only search one docset at a time.
+
+#+BEGIN_SRC emacs-lisp
+(set! :devdocs 'js2-mode "javascript")
+;; works on minor modes too
+(set! :devdocs 'rjsx-mode "react")
+#+END_SRC
+
+** Open in eww instead of browser
+#+BEGIN_SRC emacs-lisp
+(setq +lookup-open-url-fn 'eww)
+#+END_SRC
+
+* Appendix
+** Commands
++ ~+lookup/definition~
++ ~+lookup/references~
++ ~+lookup/documentation~
++ ~+lookup/online~
++ ~+lookup/online-select~
++ ~+lookup/in-devdocs~
++ ~+lookup/in-docsets~
diff --git a/modules/feature/lookup/autoload/evil.el b/modules/feature/lookup/autoload/evil.el
new file mode 100644
index 000000000..72c193fcc
--- /dev/null
+++ b/modules/feature/lookup/autoload/evil.el
@@ -0,0 +1,22 @@
+;;; feature/lookup/autoload/evil.el -*- lexical-binding: t; -*-
+;;;###if (featurep! :feature evil)
+
+;;;###autoload (autoload '+lookup:online "feature/lookup/autoload/evil" nil t)
+(evil-define-command +lookup:online (query &optional bang)
+ "Look up QUERY online. Will prompt for search engine the first time, then
+reuse it on consecutive uses of this command. If BANG, always prompt for search
+engine."
+ (interactive "")
+ (+lookup/online query (+lookup--online-provider bang 'evil-ex)))
+
+;;;###autoload (autoload '+lookup:dash "feature/lookup/autoload/evil" nil t)
+(evil-define-command +lookup:dash (query &optional bang)
+ "TODO"
+ (interactive "")
+ (+lookup/in-docsets query (if bang 'blank)))
+
+;;;###autoload (autoload '+lookup:devdocs "feature/lookup/autoload/evil" nil t)
+(evil-define-command +lookup:devdocs (query &optional bang)
+ "TODO"
+ (interactive "")
+ (+lookup/in-devdocs query (if bang 'blank)))
diff --git a/modules/feature/lookup/autoload/lookup.el b/modules/feature/lookup/autoload/lookup.el
new file mode 100644
index 000000000..03b19fe12
--- /dev/null
+++ b/modules/feature/lookup/autoload/lookup.el
@@ -0,0 +1,237 @@
+;;; feature/lookup/autoload/lookup.el -*- lexical-binding: t; -*-
+
+(defvar +lookup--rg-installed-p (executable-find "rg"))
+(defvar +lookup--ag-installed-p (executable-find "ag"))
+(defvar +lookup--last-provider nil)
+
+;; Helpers
+(defun +lookup--online-provider (&optional force-p namespace)
+ (let ((key (or namespace major-mode)))
+ (or (and (not force-p)
+ (cdr (assq key +lookup--last-provider)))
+ (when-let* ((provider
+ (completing-read
+ "Search on: "
+ (mapcar #'car +lookup-provider-url-alist)
+ nil t)))
+ (push (cons key provider)
+ (assq-delete-all key +lookup--last-provider))))))
+
+(defun +lookup--symbol-or-region (&optional initial)
+ (cond (initial)
+ ((use-region-p)
+ (buffer-substring-no-properties (region-beginning)
+ (region-end)))
+ ((thing-at-point 'symbol t))))
+
+(defun +lookup--jump-to (prop identifier &optional other-window)
+ (with-selected-window
+ (if other-window
+ (save-excursion (other-window 1) (selected-window))
+ (selected-window))
+ (let ((fn (plist-get +lookup-current-functions prop))
+ (origin (point-marker)))
+ (condition-case e
+ (or (if (commandp fn)
+ (call-interactively fn)
+ (funcall fn identifier))
+ (/= (point-marker) origin))
+ ('error
+ (message "%s" e)
+ nil)))))
+
+
+;;
+;; Main commands
+;;
+
+;;;###autoload
+(defun +lookup/definition (identifier &optional other-window)
+ "Jump to the definition of the symbol at point. It will try several things
+to find it:
+
+1. It will try whatever function that has been set for the current buffer, in
+ `+lookup-current-functions'.
+2. Then try any available xref backends,
+3. Then `dumb-jump',
+4. Then a plain project-wide text search, using ripgrep or the_silver_searcher.
+5. Then, if `evil-mode' is active, use `evil-goto-definition',
+
+Failing all that, it will give up with an error."
+ (interactive
+ (list (thing-at-point 'symbol t)
+ current-prefix-arg))
+ (cond ((null identifier)
+ (user-error "Nothing under point"))
+
+ ((and (plist-member +lookup-current-functions :definition)
+ (+lookup--jump-to :definition identifier)))
+
+ ((ignore-errors (if other-window
+ (xref-find-definitions-other-window identifier)
+ (xref-find-definitions identifier))
+ t))
+
+ ((and (require 'dumb-jump nil t)
+ ;; dumb-jump doesn't tell us if it succeeded or not
+ (let ((old-fn (symbol-function 'dumb-jump-get-results))
+ successful)
+ (cl-letf (((symbol-function 'dumb-jump-get-results)
+ (lambda (&optional prompt)
+ (let* ((plist (funcall old-fn prompt))
+ (results (plist-get plist :results)))
+ (when (and results (> (length results) 0))
+ (setq successful t))
+ plist))))
+ (if other-window
+ (dumb-jump-go-other-window)
+ (dumb-jump-go))
+ successful))))
+
+ ((and identifier
+ (featurep 'counsel)
+ (let ((regex (rxt-quote-pcre identifier)))
+ (or (and +lookup--rg-installed-p
+ (counsel-rg regex (doom-project-root)))
+ (and +lookup--ag-installed-p
+ (counsel-ag regex (doom-project-root)))))))
+
+ ((and (featurep 'evil)
+ evil-mode
+ (cl-destructuring-bind (beg . end)
+ (bounds-of-thing-at-point 'symbol)
+ (evil-goto-definition)
+ (let ((pt (point)))
+ (not (and (>= pt beg)
+ (< pt end)))))))
+
+ (t (user-error "Couldn't find '%s'" identifier))))
+
+;;;###autoload
+(defun +lookup/references (identifier)
+ "Show a list of references to the symbol at point.
+
+Tries `xref-find-references' and falls back to rg/ag."
+ (interactive (list (thing-at-point 'symbol t)))
+ (cond ((and (plist-member +lookup-current-functions :references)
+ (+lookup--jump-to :references identifier)))
+
+ ((ignore-errors (xref-find-references identifier)
+ t))
+
+ ((and identifier
+ (featurep 'counsel)
+ (let ((regex (rxt-quote-pcre identifier)))
+ (or (and (executable-find "rg")
+ (counsel-rg regex (doom-project-root)))
+ (and (executable-find "ag")
+ (counsel-ag regex (doom-project-root)))))))
+
+ (t (error "Couldn't find '%s'" identifier))))
+
+;;;###autoload
+(defun +lookup/documentation (identifier)
+ "Show documentation for IDENTIFIER (defaults to symbol at point or selection.
+
+Goes down a list of possible backends:
+
+1. The :documentation spec defined with by `doom--set:lookup'
+2. If the +docsets flag is active for :feature lookup, use `+lookup/in-docsets'
+3. If the +devdocs flag is active for :feature lookup, run `+lookup/in-devdocs'
+4. Fall back to an online search, with `+lookup/online'"
+ (interactive
+ (list (+lookup--symbol-or-region)))
+ (cond ((and (plist-member +lookup-current-functions :documentation)
+ (+lookup--jump-to :documentation identifier)))
+
+ ((and (featurep! :feature lookup +docsets)
+ (cl-find-if #'helm-dash-docset-installed-p
+ (or (bound-and-true-p counsel-dash-docsets)
+ (bound-and-true-p helm-dash-docsets))))
+ (+lookup/in-docsets identifier))
+
+ ((featurep! :feature lookup +devdocs)
+ (+lookup/in-devdocs identifier))
+
+ ((+lookup/online
+ identifier
+ (+lookup--online-provider (not current-prefix-arg))))))
+
+
+;;
+;; Source-specific commands
+;;
+
+;;;###autoload
+(defun +lookup/in-devdocs (&optional query docs)
+ "TODO"
+ (interactive)
+ (require 'devdocs)
+ (let* ((docs
+ (unless (eq docs 'blank)
+ (or docs (cdr (assq major-mode devdocs-alist)) "")))
+ (query (or query (+lookup--symbol-or-region) ""))
+ (pattern (string-trim-left (format "%s %s" docs query))))
+ (unless (and current-prefix-arg docs)
+ (setq pattern (read-string "Lookup on devdocs.io: " pattern)))
+ (funcall +lookup-open-url-fn
+ (format "%s/#q=%s" devdocs-url
+ (url-hexify-string pattern)))
+ (unless (string-empty-p pattern)
+ (cl-pushnew pattern devdocs-search-history))))
+
+;;;###autoload
+(defun +lookup/in-docsets (&optional query docsets)
+ "TODO"
+ (interactive)
+ (let* ((counsel-dash-docsets
+ (unless (eq docsets 'blank)
+ (or docsets
+ (or (bound-and-true-p counsel-dash-docsets)
+ (bound-and-true-p helm-dash-docsets)))))
+ (helm-dash-docsets counsel-dash-docsets)
+ (query (or query (+lookup--symbol-or-region) "")))
+ (cond ((featurep! :completion helm)
+ (helm-dash query))
+ ((featurep! :completion ivy)
+ (counsel-dash query))
+ (t
+ (user-error "No dash backend is installed, enable ivy or helm.")))))
+
+;;;###autoload
+(defun +lookup/online (search &optional provider)
+ "Looks up SEARCH (a string) in you browser using PROVIDER.
+
+PROVIDER should be a key of `+lookup-provider-url-alist'.
+
+When used interactively, it will prompt for a query and, for the first time, the
+provider from `+lookup-provider-url-alist'. On consecutive uses, the last
+provider will be reused. If the universal argument is supplied, always prompt
+for the provider."
+ (interactive
+ (list (or (and (use-region-p)
+ (buffer-substring-no-properties (region-beginning)
+ (region-end)))
+ (read-string "Search for: " (thing-at-point 'symbol t)))
+ (+lookup--online-provider current-prefix-arg)))
+ (condition-case _ex
+ (let ((url (cdr (assoc provider +lookup-provider-url-alist))))
+ (unless url
+ (error "'%s' is an invalid search engine" provider))
+ (when (or (functionp url) (symbolp url))
+ (setq url (funcall url)))
+ (cl-assert (and (stringp url) (not (string-empty-p url))))
+ (when (string-empty-p search)
+ (user-error "The search query is empty"))
+ (funcall +lookup-open-url-fn (format url (url-encode-url search))))
+ ('error
+ (setq +lookup--last-provider
+ (assq-delete-all major-mode +lookup--last-provider)))))
+
+;;;###autoload
+(defun +lookup/online-select ()
+ "Runs `+lookup/online', but always prompts for the provider to use."
+ (interactive)
+ (let ((current-prefix-arg t))
+ (call-interactively #'+lookup/online)))
+
diff --git a/modules/feature/lookup/config.el b/modules/feature/lookup/config.el
new file mode 100644
index 000000000..b51032a28
--- /dev/null
+++ b/modules/feature/lookup/config.el
@@ -0,0 +1,189 @@
+;;; feature/lookup/config.el -*- lexical-binding: t; -*-
+
+;; "What am I looking at?" This module helps you answer this question.
+;;
+;; + `+lookup/definition': a jump-to-definition that should 'just work'
+;; + `+lookup/references': find a symbol's references in the current project
+;; + `+lookup/online'; look up a symbol on online resources
+;; + `+lookup/docs-at-point'
+;; + `+lookup/docs-dash'
+;; + `+lookup/docs-dash-at-point'
+;; + `+lookup/devdocs'
+;; + `+lookup/devdocs-at-point'
+;;
+;; This module uses `xref', an experimental new library in Emacs. It may change
+;; in the future. When xref can't be depended on it will fall back to
+;; `dumb-jump' to find what you want.
+
+(defvar +lookup-provider-url-alist
+ '(("Google" . "https://google.com/search?q=%s")
+ ("Google images" . "https://google.com/images?q=%s")
+ ("Google maps" . "https://maps.google.com/maps?q=%s")
+ ("Project Gutenberg" . "http://www.gutenberg.org/ebooks/search/?query=%s")
+ ("DuckDuckGo" . "https://duckduckgo.com/?q=%s")
+ ("DevDocs.io" . "https://devdocs.io/#q=%s")
+ ("StackOverflow" . "https://stackoverflow.com/search?q=%s")
+ ("Github" . "https://github.com/search?ref=simplesearch&q=%s")
+ ("Youtube" . "https://youtube.com/results?aq=f&oq=&search_query=%s")
+ ("Wolfram alpha" . "https://wolframalpha.com/input/?i=%s")
+ ("Wikipedia" . "https://wikipedia.org/search-redirect.php?language=en&go=Go&search=%s"))
+ "An alist that maps online resources to their search url or a function that
+produces an url. Used by `+lookup/online'.")
+
+(defvar +lookup-open-url-fn #'browse-url
+ "Function to use to open search urls.")
+
+(defvar +lookup-function-alist nil
+ "An alist mapping major modes to jump function plists, describing what to do
+with `+lookup/definition', `+lookup/references' and `+lookup/documentation' are
+called.")
+
+(defvar-local +lookup-current-functions nil
+ "The enabled jump functions for the current buffer.")
+
+(def-setting! :lookup (modes &rest plist)
+ "Defines a jump target for major MODES. PLIST accepts the following
+properties:
+
+ :definition FN
+ Run when jumping to a symbol's definition.
+ Used by `+lookup/definition'.
+ :references FN
+ Run when looking for usage references of a symbol in the current project.
+ Used by `+lookup/references'.
+ :documentation FN
+ Run when looking up documentation for a symbol.
+ Used by `+lookup/documentation'.
+ :xref-backend FN
+ Defines an xref backend for a major-mode. With this, :definition and
+ :references are unnecessary."
+ `(dolist (mode (doom-enlist ,modes))
+ (push (cons mode (list ,@plist))
+ +lookup-function-alist)))
+
+;; Recenter buffer after certain jumps
+(add-hook!
+ '(imenu-after-jump-hook evil-jumps-post-jump-hook
+ counsel-grep-post-action-hook dumb-jump-after-jump-hook)
+ #'recenter)
+
+
+;;
+;; dumb-jump
+;;
+
+(def-package! dumb-jump
+ :commands (dumb-jump-go dumb-jump-quick-look
+ dumb-jump-back dumb-jump-result-follow)
+ :config
+ (setq dumb-jump-default-project doom-emacs-dir
+ dumb-jump-aggressive nil
+ dumb-jump-selector
+ (cond ((featurep! :completion ivy) 'ivy)
+ ((featurep! :completion helm) 'helm)
+ (t 'popup))))
+
+
+;;
+;; xref
+;;
+
+(after! xref
+ ;; By default, `etags--xref-backend' is the default xref backend. No need.
+ ;; We'll set these up ourselves in other modules.
+ (setq-default xref-backend-functions '(t))
+ (set! :popup "*xref*" :noselect t :autokill t :autoclose t))
+
+(defun +lookup|init-xref-backends ()
+ "Set `+lookup-current-functions' for the current buffer.
+
+This variable is used by `+lookup/definition',`+lookup/references' and
+`+lookup/documentation'."
+ (when-let* ((plist (cdr (assq major-mode +lookup-function-alist))))
+ (when-let* ((backend (plist-get plist :xref-backend)))
+ (make-variable-buffer-local 'xref-backend-functions)
+ (cl-pushnew backend xref-backend-functions :test #'eq))
+ (setq-local +lookup-current-functions plist)))
+(add-hook 'after-change-major-mode-hook #'+lookup|init-xref-backends)
+
+
+(def-package! ivy-xref
+ :when (featurep! :completion ivy)
+ :after xref
+ :config (setq xref-show-xrefs-function #'ivy-xref-show-xrefs))
+
+
+(def-package! helm-xref
+ :when (featurep! :completion helm)
+ :after xref
+ :config (setq xref-show-xrefs-function #'helm-xref-show-xrefs))
+
+
+;;
+;; Dash docset integration
+;;
+
+(when (featurep! +docsets)
+ (def-setting! :docset (modes &rest docsets)
+ "Registers a list of DOCSETS (strings) for MODES (either one major mode
+symbol or a list of them).
+
+If MODES is a minor mode, you can use :add or :remove as the first element of
+DOCSETS, to instruct it to append (or remove) those from the docsets already set
+by a major-mode, if any.
+
+Used by `+lookup/in-docsets' and `+lookup/documentation'."
+ (cl-loop with ivy-p = (featurep! :completion ivy)
+ for mode in (doom-enlist (doom-unquote modes))
+ for hook-sym = (intern (format "+docs|init-for-%s" mode))
+ for var-sym = (if ivy-p 'counsel-dash-docsets 'helm-dash-docsets)
+ collect hook-sym into hooks
+ collect
+ `(defun ,hook-sym ()
+ (make-variable-buffer-local ',var-sym)
+ (cond ((eq (car docsets) :add)
+ `(setq ,var-sym (append ,var-sym (list ,@(cdr docsets)))))
+ ((eq (car docsets) :remove)
+ `(setq ,var-sym
+ (cl-loop with to-delete = (list ,@(cdr docsets))
+ for docset in ,var-sym
+ unless (member docset to-delete)
+ collect docset)))
+ (`(setq ,var-sym (list ,@docsets)))))
+ into forms
+ finally return `(progn ,@forms (add-hook! ,modes ',hooks))))
+
+ ;; Both packages depend on helm-dash
+ (def-package! helm-dash
+ :commands (helm-dash helm-dash-install-docset helm-dash-at-point
+ helm-dash-docset-installed-p)
+ :config
+ ;; Obey XDG conventions
+ (when-let* ((xdg-data-home (getenv "XDG_DATA_HOME")))
+ (setq helm-dash-docsets-path (expand-file-name "docsets" xdg-data-home)))
+ (unless (file-directory-p helm-dash-docsets-path)
+ (make-directory helm-dash-docsets-path t))
+ (setq helm-dash-enable-debugging doom-debug-mode))
+
+ (def-package! counsel-dash
+ :when (featurep! :completion ivy)
+ :commands (counsel-dash counsel-dash-install-docset)
+ :after helm-dash
+ :config (setq counsel-dash-min-length 2)))
+
+
+;;
+;; devdocs.io integration
+;;
+
+(when (featurep! +devdocs)
+ (def-setting! :devdocs (modes docset)
+ "Map major MODES (one major-mode symbol or a list of them) to a devdocs
+DOCSET (a string).
+
+See `devdocs-alist' for the defaults. "
+ `(dolist (mode ',modes)
+ (push (cons mode ,docset) devdocs-alist)))
+
+ (def-package! devdocs :defer t))
+
diff --git a/modules/feature/lookup/packages.el b/modules/feature/lookup/packages.el
new file mode 100644
index 000000000..3d54bd1a4
--- /dev/null
+++ b/modules/feature/lookup/packages.el
@@ -0,0 +1,17 @@
+;; -*- no-byte-compile: t; -*-
+;;; feature/lookup/packages.el
+
+(package! dumb-jump)
+(when (featurep! :completion ivy)
+ (package! ivy-xref))
+(when (featurep! :completion helm)
+ (package! helm-xref))
+
+(when (featurep! +docsets)
+ (when (featurep! :completion helm)
+ (package! helm-dash))
+ (when (featurep! :completion ivy)
+ (package! counsel-dash)))
+
+(when (featurep! +devdocs)
+ (package! devdocs))
diff --git a/modules/lang/javascript/config.el b/modules/lang/javascript/config.el
index 772976ae9..c775bd034 100644
--- a/modules/lang/javascript/config.el
+++ b/modules/lang/javascript/config.el
@@ -45,7 +45,9 @@
;; A find-{definition,references} backend for js2-mode. NOTE The xref API is
;; unstable and may break with an Emacs update.
-(def-package! xref-js2 :commands xref-js2-xref-backend)
+(def-package! xref-js2
+ :when (featurep! :feature lookup)
+ :commands xref-js2-xref-backend)
(def-package! nodejs-repl :commands nodejs-repl)
diff --git a/modules/lang/javascript/packages.el b/modules/lang/javascript/packages.el
index c89f3bd09..22358eb9a 100644
--- a/modules/lang/javascript/packages.el
+++ b/modules/lang/javascript/packages.el
@@ -16,6 +16,6 @@
(when (featurep! :completion company)
(package! company-tern))
-(when (featurep! :feature jump)
+(when (featurep! :feature lookup)
(package! xref-js2))
diff --git a/modules/private/default/+bindings.el b/modules/private/default/+bindings.el
index 75c59770a..759d1e967 100644
--- a/modules/private/default/+bindings.el
+++ b/modules/private/default/+bindings.el
@@ -113,7 +113,7 @@
:desc "Swiper" :nv "/" #'swiper
:desc "Imenu" :nv "i" #'imenu
:desc "Imenu across buffers" :nv "I" #'imenu-anywhere
- :desc "Online providers" :nv "o" #'+jump/online-select)
+ :desc "Online providers" :nv "o" #'+lookup/online-select)
(:desc "workspace" :prefix "TAB"
:desc "Display tab bar" :n "TAB" #'+workspace/display
@@ -159,8 +159,8 @@
:v "e" #'+eval/region
:desc "Evaluate & replace region" :nv "E" #'+eval:replace-region
:desc "Build tasks" :nv "b" #'+eval/build
- :desc "Jump to definition" :n "d" #'+jump/definition
- :desc "Jump to references" :n "D" #'+jump/references
+ :desc "Jump to definition" :n "d" #'+lookup/definition
+ :desc "Jump to references" :n "D" #'+lookup/references
:desc "Open REPL" :n "r" #'+eval/open-repl
:v "r" #'+eval:repl)
@@ -205,9 +205,9 @@
:desc "Describe face" :n "F" #'describe-face
:desc "Describe DOOM setting" :n "s" #'doom/describe-setting
:desc "Describe DOOM module" :n "d" #'doom/describe-module
- :desc "Find definition" :n "." #'+jump/definition
- :desc "Find references" :n "/" #'+jump/references
- :desc "Find documentation" :n "h" #'+jump/documentation
+ :desc "Find definition" :n "." #'+lookup/definition
+ :desc "Find references" :n "/" #'+lookup/references
+ :desc "Find documentation" :n "h" #'+lookup/documentation
:desc "What face" :n "'" #'doom/what-face
:desc "What minor modes" :n ";" #'doom/what-minor-mode
:desc "Info" :n "i" #'info
@@ -297,9 +297,9 @@
:n "[w" #'+workspace/switch-left
:m "gt" #'+workspace/switch-right
:m "gT" #'+workspace/switch-left
- :m "gd" #'+jump/definition
- :m "gD" #'+jump/references
- :m "gh" #'+jump/documentation
+ :m "gd" #'+lookup/definition
+ :m "gD" #'+lookup/references
+ :m "gh" #'+lookup/documentation
:n "gp" #'+evil/reselect-paste
:n "gr" #'+eval:region
:n "gR" #'+eval/buffer
diff --git a/modules/private/default/+evil-commands.el b/modules/private/default/+evil-commands.el
index 2ee09ea6c..9dfda8bc8 100644
--- a/modules/private/default/+evil-commands.el
+++ b/modules/private/default/+evil-commands.el
@@ -29,7 +29,9 @@
;; TODO (ex! "db" #'doom:db)
;; TODO (ex! "dbu[se]" #'doom:db-select)
;; TODO (ex! "go[ogle]" #'doom:google-search)
-(ex! "lo[okup]" #'+jump:online)
+(ex! "lo[okup]" #'+lookup:online)
+(ex! "dash" #'+lookup:dash)
+(ex! "dd" #'+lookup:devdocs)
(ex! "http" #'httpd-start) ; start http server
(ex! "repl" #'+eval:repl) ; invoke or send to repl
;; TODO (ex! "rx" 'doom:regex) ; open re-builder