/init.el= is now loaded before
- modules (after core).
- + =:private = is now automatically loaded by ~doom!~.
- + New help command: ~doom/describe-module~ -- for DOOM modules.
- + New help command: ~doom/describe-setting~ -- for possible ~set!~ targets.
- + Add =make doctor= to diagnose common issues with your setup & environment.
- + Removed ~def-bootstrap~ & ~doom-bootstrap~. It was a clumsy system. I'll
- replace it with README.org files in each module, with working, tangle-able
- source blocks.
- + =core-os=
- + Don't use GTK tooltips on Linux (ugly!).
- + =core-ui=
- + New plugin: [[https://github.com/syl20bnr/vi-tilde-fringe][vi-tilde-fringe]] -- subtle, vim-ish empty-line indicator.
- + New variable: ~doom-ui-mode-names~ (alist) -- for changing ~mode-name~ of
- major-modes.
- + Fix left-over hl-line overlays when hl-line-mode is uncleanly killed (e.g.
- when the major-mode is changed).
- + Fix disappearing line numbers in nlinum (thanks to [[https://github.com/gilbertw1][gilbertw1]]).
- + Move theme/font bootstrap to core-ui.
- + New hook: ~doom-init-ui-hook~
- + New global minor-mode ~doom-big-font-mode~ and variable ~doom-big-font~.
- + =core-keybinds=
- + New property for ~map!~: ~:textobj~ -- for binding to evil text objects
- keymaps.
- + Fix ~:after~ & ~:map*~ properties in ~map!~ macro (wasn't working at all).
- + Change keybinding scheme; the leader key is now =SPC= and localleader =SPC
- m=, inspired by spacemacs.
- + Enable which-key pops up for all keys.
- + =core-popups=
- + Properly persist ~popup~ window parameter between sessions.
- + Improve magit+shackle integration; ensures that links will be followed
- within the popup they were opened.
- + Add ~doom-popup-no-fringe~ option (default = t). When non-nil, fringes
- will be disabled in popup windows (in ~doom-popup-mode~).
- + =core-packages=
- + Fix failure to detect out-of-date QUELPA packages.
- + Fix ~custom-file~ (and custom settings) not being loaded.
- + Fix crash in ~doom-update-package~ caused by unreachable, new
- dependencies.
- + Make ~doom-update-package~ atomic in case of failure.
- + Make ~doom-refresh-packages~ async.
- + Improve the security of package management (via ELPA) by a) forcing Emacs
- to verify TLS connections and b) use HTTPS sources for MELPA and ELPA.
- + Make ~doom-get-outdated-packages~ asynchronous, producing a substantial
- speed-up when updating packages from Quelpa sources.
-+ =feature=
- + =feature/evil=
- + Add ~+evil:mc~ command [[https://github.com/gabesoft/evil-mc][evil-mc]].
- + Add ~+evil/mc-make-cursor-here~, with visual-block support for [[https://github.com/gabesoft/evil-mc][evil-mc]].
- + =d= (operator) now invokes ~wgrep-mark-deletion~ in wgrep buffers.
- + New code folding system that combines hideshow (built-in; for
- indent/marker-based folds) and [[https://github.com/alexmurray/evil-vimish-fold][evil-vimish-fold]] (for arbitrary folds).
- + Fix [[https://github.com/redguardtoo/evil-matchit][evil-matchit]] in visual mode.
- + Fix [[https://github.com/hlissner/evil-multiedit][evil-multiedit]] M-d bindings.
- + Fix stringp error caused by unintialized state in hideshow.
- + Fix evil normal-mode keybindings in help-mode popups.
- + Change how ~+evil-esc-hook~ hooks are handled: they now short-circuit on
- the first hook to return non-nil.
- + Remove ~+evil/matchit~ (thin wrapper around ~evilmi-jump-items~).
- + Remove [[https://github.com/jaypei/emacs-neotree/][neotree]] plugin (moved to =tools/neotree=).
- + =feature/jump=
- + Call ~recenter~ after using [[https://github.com/jacktasia/dumb-jump][dumb-jump]].
- + =feature/workspaces=
- + No longer saves session on quit if session was blank.
- + Fix persp-mode switching to main workspace if auto-resume is on.
- + Fix ~+workspace-get~ returning a non-nil "null perspective" on some
- occasions where NAME doesn't exist. This is because ~persp-get-by-name~
- returns the value of ~persp-not-persp~ to signify null instead of actual
- nil.
- + Decouple workspace buffer-list functions from doom buffer library. Now,
- the workspaces module will explicitly advise ~doom-buffer-list~.
- + ~+workspace-list~ now returns a list of perspective structs, rather than a
- list of strings. ~+workspace-list-names~ was introduced for the latter.
-+ =completion=
- + =completion/company=
- + Change ~:company-backends~ to accept a variadic list of backends to
- prepend to ~company-backends~. Its signature is now ~(set!
- :company-backends MODES &rest BACKENDS)~ ([[https://github.com/hlissner/.emacs.d/pull/125][#125]]).
- + =completion/ivy=
- + Flexible column width for ~+ivy/tasks~.
-+ =ui=
- + =ui/doom=
- + New plugin: [[https://github.com/hlissner/emacs-solaire-mode][solaire-mode]] -- replaces ~doom-buffer-mode~; brightens source
- windows and dims transient, temporary, or popup windows.
- + BREAKING CHANGE: Decoupled theme and font loading from ui/doom. This has
- been moved to core-ui. The following variables have been renamed:
- + ~+doom-theme~ => ~doom-theme~
- + ~+doom-font~ => ~doom-font~
- + ~+doom-variable-pitch-font~ => ~doom-variable-pitch-font~
- + ~+doom-unicode-font~ => ~doom-unicode-font~
- + =ui/doom-modeline=
- + Reduce excess whitespace on right of flycheck segment.
- + Buffer-path and file-name segments now use different faces.
- + The vcs segment now uses a slightly darker color (in clean branches).
- + Fix blank mode-line when buffer-file-name is nil ([[https://github.com/hlissner/.emacs.d/pull/130][#130]])
- + =ui/nav-flash=
- + Fix over-aggressive nav-flash'ing on evil-multiedit or in eshell/term
- buffers.
-+ =tools=
- + =tools/gist=
- + Changed new gists to be private by default.
-+ =lang=
- + =lang/haskell=
- + New plugin: [[https://github.com/iquiw/company-ghc][company-ghc]] -- code-completion support for haskell (requires
- ~ghc-mod~).
- + =lang/php=
- + New plugin: [[https://github.com/xcwen/ac-php][company-php]] -- code-completion support for php (requires a
- TAGs file created with [[https://github.com/xcwen/phpctags][phpctags]]).
- + =lang/emacs-lisp=
- + Omit defuns inside macros from the imenu index.
- + Don't enable ~flycheck-mode~ in emacs.d files.
- + =lang/org=
- + Replace org-bullets source with more up-to-date fork.
- + =lang/scala=
- + Fix ~void-variable imenu-auto-rescan~ error caused by
- ~ensime--setup-imenu~ trying to use imenu variables before loading imenu.
-+ =private/hlissner=
- + Add =gzz= binding (~+evil/mc-make-cursor-here~)
- + Add =:mc= ex command (~+evil:mc~)
- + Add =:lookup= ex command (~+jump:online~).
- + Add =:gblame= ex command (~magit-blame~).
- + Add =:grevert= ex command (~git-gutter:revert-hook~).
-
-* 2.0.2 (May 13, 2017)
-+ *New modules*
- + =tools/gist= -- allows you to manage and create gists, using [[https://github.com/defunkt/gist.el][gist.el]].
- + =tools/term= -- quickly spawn a terminal (in a popup or buffer) using
- [[https://github.com/emacsorphanage/multi-term][multi-term]]
- + =app/twitter= -- Emacs as a twitter client, using [[https://github.com/hayamiz/twittering-mode][twittering-mode]]
-+ =core=
- + Stop "buffer is read-only" messages while in minibuffer, when I accidentally
- try to edit the prompt. It's correct behavior, but it consumes the
- minibuffers, hiding what I'm typing.
- + Fix Emacs daemon compatibility with DOOM, which assumed a frame will always
- be visible on startup, causing errors when Emacs is launched as a daemon.
- + Code-style change: use sharp-quote for functions. This makes the
- byte-compiler output missing-function warnings when they can't be found,
- which is helpful.
- + Stop projectile & git-gutter checks when in a TRAMP buffer; it causes
- tremendous slowdowns, to the point of being unusable.
- + Add ~message!~ & ~format!~ macros for printing colored output either a) in a
- popup buffer when in an interactive session or b) with ansi codes when in an
- noninteractive session.
- + Changed ~doom/recompile~ to aggresively recompile =core/core.el= to fix
- load-path inconsistencies when you've byte-compiled your config and run a
- package management command.
- + =core-lib=
- + Add ~:append~ support to ~add-transient-hook!~ macro.
- + =core-popups=
- + Fix over-eager ESC binding killing all popups indiscriminantly
- + =core-ui=
- + Remove references to ~ace-maximize-window~ (obsolete)
- + Fix whitespace adjustment in ~highlight-indentation-current-column-mode~
- + =core-packages=
- + Package management now produces colored and detailed feedback.
-+ =ui=
- + =ui/doom=
- + Git-gutter fringe bitmaps no longer appear truncated.
- + Fix lack of syntax highlighting in scratch buffer
- + Use comment face as default color for ~+doom-folded-face~
- + =ui/doom-modeline=
- + Fix modeline disappearing due to loss of state. ~doom--modeline-format~
- was being killed when switching major modes.
-+ =feature=
- + =feature/eval=
- + Fix ~:repl~ & ~+eval/repl-send-region~.
- + Fix ~+eval/region~ failing only on first invocation because
- ~+eval-runners~ wasn't populated until quickrun is loaded.
- + Add TAB auto-completion in comint-mode and REPL buffers
- + =feature/evil=
- + Fix ~:mv~ & ~:rm~.
- + Fix Neotree forgetting that it's a neotree window when switching
- perspectives.
- + New plugin: [[https://github.com/gabesoft/evil-mc][evil-mc]] -- multiple cursors for evil-mode (thanks to
- [[https://github.com/gilbertw1][gilbertw1]])
- + Achieve vim parity w/ file modifiers
- (~+evil*ex-replace-special-filenames~)
- + =feature/version-control=
- + New plugin: [[https://github.com/pidu/git-timemachine][git-timemachine]] -- stepping through a file's git history.
- + New plugin: [[https://github.com/sshaw/git-link][git-link]] -- generates and opens links to "this file"'s remote
- repo with your default browser.
- + Add ~:gbrowse~: find this file on github/gitlab/bitbucket in your browser.
- + Add ~:gissues~: open this project's issues page in your browser.
- + Fix ~+vcs/git-browse~ and ~+vcs/git-browse-issues~.
- + =feature/workspaces=
- + Add BANG modifier to ~:cleanup~ to span all workspaces.
- + Since persp-mode handles its "nil" perspective differently from others,
- pretend that it doesn't exist and spawn a new "main" perspective.
-+ =completion=
- + =completion/ivy=
- + Add ~+ivy-do-action!~ factory macro. Use it for in-ivy keybindings.
- + Add ripgrep file search support. Ripgrep doesn't support multiline
- searches, but is faster. Use =ag= for multiline (or more PCRE-compliant)
- searches.
- + Reverse ivy's built-in behavior of reversing escaping of parentheses when
- using the_silver_searcher or ripgrep. If you want literal parentheses,
- escape them explicitly.
- + Removed ~def-counsel-action!~
- + When a selection is used for ~:ag~, the selected text is now
- regexp-escaped.
-+ =tools=
- + =tools/tmux=
- + Fix and refactor library (general update).
-+ =lang=
- + =lang/cc=
- + Integrate counsel-ivy into [[https://github.com/Sarcasm/irony-mode][irony-mode]]
- + =lang/javascript=
- + Improve electric indent support for ~js2-mode~ and ~rjsx-mode~
- + =lang/org=
- + Fix org-checkbox-statistics not respecting underlying faces
- + Disable ~show-paren-mode~ in org-mode due to conflicts with org-indent
- which cause indentation flickering.
- + Bind ~M-z~ (~undo~), ~C-u~ (~delete-line~) and ~C-w~ (~delete-word~) in
- ~org-store-link~ and ~org-insert-link~ prompts.
- + Apply org-headline-done face to checked-checkbox lines, to match how DONE
- headlines look. Also applies this to items whose subitems are all
- complete.
- + Changed default fold behavior when loading an org-file to unfold first
- level folds.
- + Add =bin/org-capture= shell script for invoking the org-capture frame from
- outside Emacs.
- + Add babel support for: rust, restclient, sql, google translate, haskell
- and go.
- + Add ~+org-pretty-mode~ for toggling "pretty" fontification. Prettified
- entities or hidden regions can make editing difficult.
- + =lang/python=
- + Add ipython detection and REPL support
- + Simplify matchit key (%) in python. The default is to prioritize if-else
- and other blocks over brackets. I found this frustrating.
- + =lang/web=
- + Fix ~+web-encode-entities~, ~+web-decode-entities~,
- ~+web/encode-entities-region~ and ~+web/decode-entities-region~.
-+ =app=
- + =app/email=
- + Replace mbsync with offlineimap.
- + Add support for marking multiple emails when in visual-mode (evil) in a
- ~mu4e-headers-mode~ buffer.
- + Fix trash mark causing duplicates upstream.
- + Make refiling more compatible with archiving in gmail.
-+ =private/hlissner=
- + Add keybinds for [[https://github.com/gabesoft/evil-mc][evil-mc]]: based around ~gz~ (like ~gzz~ to toggle cursor
- freeze, and ~gzc~ to create a cursor "here").
- + Add keybinds for [[https://github.com/hlissner/evil-multiedit][evil-multiedit]]: based around ~M-d~ and ~M-D~.
- + Replace ~:find~ with ~:ag~, ~:agc[wd]~, ~:rg~ and ~:rgc[wd]~.
- + Fix ~:x~ ex command (open scratch buffer)
-
-* 2.0.1 (Apr 8, 2017)
-+ *New modules*
- + =feature/jump= -- extra code navigation tools, a jump-to-definition
- implementation that just works ([[https://github.com/jacktasia/dumb-jump][dumb-jump]]), and tools for looking things up
- online.
- + =app/rss= -- Emacs as an RSS reader, using [[https://github.com/skeeto/elfeed][elfeed]]
-+ =core=
- + Fix ~doom-kill-process-buffers~ not killing process buffers.
- + Fix ~hippie-expand~ in ex mode/the minibuffer.
- + Remove unnecessary ~provide~'s in core autoloaded libraries.
- + Fix ~doom-buffers-in-mode~ not detecting buffers in major-modes derived from
- the target mode.
- + Fix out-of-bounds error in ~doom/backward-delete-whitespace-to-column~.
- + Remove ~doom/append-semicolon~; use evil append mode instead.
- + Add module bootstrapping mechanism (for installing external dependencies);
- see ~doom-bootstrap~, ~make bootstrap~ and ~def-bootstrap!~.
- + Use ~doom-local-dir~ for TRAMP's temp files.
- + New variable: ~doom-real-buffer-functions~ -- for customizing how
- ~doom-real-buffer-p~ determines what a "real" buffer is.
- + Add ~def-memoize!~ for defining memoized functions and ~doom-memoize~ for
- memoizing existing ones.
- + =core-lib=
- + Fix ~remove-hook!~ macro not expanding correctly.
- + New macro: ~add-transient-hook!~; attach a hook to a hook or function that
- will remove itself once it runs.
- + =core-packages=
- + Add ~doom/recompile~, for re-byte-compiling DOOM.
- + Add ~doom/compile-lite~ / ~make compile-lite~, which will only
- byte-compile DOOM's core files, which is a lighter alternative to
- ~doom/compile~.
- + Fix duplicates packages appearing in package-management retrieval library.
- + =core-os=
- + Reducing how aggressive ~exec-path~ caching is. A =persistent-soft= /and/
- byte-compilation cache is excessive. The latter is good (and flexible)
- enough.
- + =core-popups=
- + Set default ~:align~ and ~:select~ shackle properties (of =8= and =below=).
- + =core-editor=
- + Advise ~delete-trailing-whitespace~ to not affect current line. If evil is
- loaded, then it may affect the current line if we're *not* in insert mode.
- + =core-projects=
- + Recognize =package.json= as a project-root file (see
- ~projectile-project-root-files~).
- + Fix ~:files~ property in ~def-project-mode!~ not detecting project files.
- + =core-ui=
- + Replace [[https://github.com/DarthFennec/highlight-indent-guides][highlight-indent-guides-mode]] with [[https://github.com/antonj/Highlight-Indentation-for-Emacs/][highlight-indentation-mode]]; the
- former won't display indent guides on blank lines, even with my whitespace
- injection hook.
-+ =feature=
- + =feature/eval=
- + Fix build tasks system; now tested and works.
- + Complete rewrite of the module.
- + =feature/evil=
- + Fix error in ~+evil:file-move~ if ~save-place-mode~ is disabled.
- + =feature/snippets=
- + Don't hijack TAB in other modes.
- + Enable ~yas-triggers-in-field~, which adds support for nested snippets.
- + Fix snippet aliases (~%alias~).
- + =feature/version-control=
- + Remove ~evil-magit~ and evil-ified bindings for magit in general. Instead,
- just use emacs mode. If evil is needed, toggle it with =C-z=.
- + =feature/workspaces=
- + Fix ~+workspace/kill-session~ not actually killing the session.
- + Revert forcing persp-mode to stay quiet when it saves the session to file.
- It just isn't important enough.
- + Create a new perspective when switching projects (integrates projectile
- with persp-mode).
- + Create a new perspective for new frames.
-+ =ui=
- + =ui/doom=
- + Improve ~doom-buffer-mode~ heuristics with ~doom-real-buffer-p~, so that
- only truly real buffers are enlightened.
- + Replace plugin [[https://github.com/Malabarba/beacon][beacon]] with [[https://github.com/rolandwalker/nav-flash][nav-flash]] -- the former had a habit of causing
- pauses and pushing my cursor. It also didn't look as nice.
- + =ui/doom-modeline=
- + Complete rewrite of the module for code readability and performance.
- + =ui/doom-dashboard=
- + Fix a ~max-specpdl-size~ error caused on MacOS, having to do with a
- ~kill-buffer-query-function~ hook being attached way too soon in the
- startup process.
-+ =lang=
- + =lang/cc=
- + Fix empty ~buffer-file-name~ in ~magic-mode-alist~ rule for obj-c.
- + Fix irony-mode initialization in cc modes.
- + =lang/emacs-lisp=
- + Add xref support for elisp.
- + =lang/go=
- + Add code-completion support with [[https://github.com/nsf/gocode][gocode and company-go]].
- + Add code navigation support with [[https://github.com/dominikh/go-mode.el/blob/master/go-guru.el][go-guru]] (built-in to go-mode).
- + Add REPL support with [[https://github.com/manute/gorepl-mode][gore and gorepl-mode]].
- + =lang/haskell=
- + New plugin: [[https://github.com/jyp/dante][dante]] -- offers xref and flycheck integration, as well as
- code-navigation tools, like finding definitions, references, type info,
- etc.
- + Fix errors on haskell-mode (caused by missing dependencies).
- + =lang/javascript=
- + New plugin: [[https://github.com/skeeto/skewer-mode][skewer-mode]] -- provides live JS/CSS/HTML evaluation in a
- browser.
- + New plugin: [[https://github.com/yasuyk/web-beautify][web-beautify]] -- js reformatting.
- + New plugin: [[https://github.com/NicolasPetton/xref-js2][xref-js2]] -- xref integration for javascript.
- + New plugin: [[https://github.com/felipeochoa/rjsx-mode][rjsx-mode]] -- adds jsx support.
- + Fix ~doom/newline-and-indent~ in rjsx-mode
- + Remove electric =<= in rjsx-mode
- + Enable [[https://github.com/smihica/emmet-mode][emmet-mode]] in rjsx-mode
- + Fix empty ~buffer-file-name~ in ~magic-mode-alist~ rule for ~rjsx-mode~.
- + Force [[https://github.com/ternjs/tern][tern]] use projectile for project path detection and resolution.
- + Add gulpfile.js detection (~+javascript-gulp-mode~).
- + =lang/latex=
- + Improve integration between auctex, evil and ~reftex-toc-mode~: j/k motion
- keys have been bound in reftex-toc-mode, the modeline is hidden, and
- ~reftex-toc-rescan~ is run automatically.
- + =lang/org=
- + Add =+notes= submodule, which makes it easy to access org-mode based notes
- for the current major-mode or the current project. See
- ~+org/browse-notes-for-major-mode~ and ~+org/browse-notes-for-project~.
- + Ensure newer org-mode 9.0+ (downloaded from ELPA) is loaded instead of the
- older, built-in version of org-mode (8.3).
- + Update ~+org/dwim-at-point~, ~+org/insert-item~ & ~+org/toggle-checkbox~
- for org-mode 9.0
- + Fix shackle popup integration with org-export dispatch window.
- + =lang/ruby=
- + Add ={Pod,Puppet,Berks}file= detection for ~ruby-mode~.
- + =lang/web=
- + New plugin: [[https://github.com/yasuyk/web-beautify][web-beautify]] -- html/css reformatting
- + Remove ~+web-bower-mode~. I don't use it anymore; I prefer npm as my sole
- package manager.
- + Improve ~+web-angularjs-mode~ detection by searching for angular 1 and 2
- dependencies in package.json.
- + Add ~+web-react-mode~ and detect it by searching for reactjs dependencies
- in package.json.
-+ =app=
- + =app/rss=
- + Hide modeline in ~elfeed-search-mode~ buffer.
- + =app/present=
- + New plugin: [[https://github.com/yjwen/org-reveal/][ox-reveal]] -- export a presentation to html, js & css from
- org-mode using [[http://lab.hakim.se/reveal-js/][reveal.js]].
- + New plugin: [[https://github.com/takaxp/org-tree-slide][org-tree-slides]] -- use ~org-mode~ directly for presentations.
- + Add ~+present/big-mode~; which will toggle large fonts, controlled by
- ~+present-big-font~.
- + New plugin: [[https://github.com/skeeto/impatient-mode][impatient-mode]] -- show off current buffer(s) over HTTP.
- + =app/email=
- + Add support for writing emails in org-mode, which renders to HTML on send.
- + Add support for sending email through SMTP on a secure port.
- + Add basic mu4e support.
-+ =private/hlissner=
- + Fix TAB hijacking in magit.
-
-* 2.0.0 (Jan 17, 2017)
-+ *New modules*
- + =tools/upload= -- map local files to remotes, allowing you to
- upload/download files between them.
- + =feature/jump= -- offers a system for navigating codebases that "just
- works", powered by xref (new experimental Emacs reference API) & [[https://github.com/jacktasia/dumb-jump][dumb-jump]].
- + =feature/workspaces= -- offers grouped buffers/windows and persistent
- sessions. Recently replaced [[https://github.com/pashinin/workgroups2][workgroups2]] with the much-faster [[https://github.com/Bad-ptr/persp-mode.el][persp-mode]].
-+ =core=
- + Add .travis.yml and unit-tests.
- + Remove mplist library -- it was unused and poorly written.
- + =core-popups=
- + Replaced popwin with shackle; which is significantly lighter and more
- stable.
-+ =feature/spellcheck=
- + Add selection popup for correcting spelling mistakes highlighted by
- flyspell.
-+ =completion/ivy=
- + Add ~+ivy:todo~ for jumping to TODO/FIXME tags in your project.
-+ =lang/org=
- + Make TAB do-what-I-mean (~+org/dwim-at-point~), which will either: follow a
- link, realign/recalculate tables, toggle checkboxes, toggle TODO/DONE tags,
- cycle archived subtrees, toggle latex preview fragments, execute babel
- blocks, or refresh inline images -- depending on where the cursor is.
-+ =lang/web=
- + Add ~+css/toggle-inline-or-block~ command; it will expand/contract
- curly-braced blocks.
-+ =private/hlissner=
- + Add ~:todo~ ex command (~+ivy:todo~)
-
diff --git a/LICENSE b/LICENSE
index 5b1a7fdf5..02b93bee4 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2016-2017 Henrik Lissner.
+Copyright (c) 2016-2019 Henrik Lissner.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/Makefile b/Makefile
index 937fbd275..ca088d921 100644
--- a/Makefile
+++ b/Makefile
@@ -1,89 +1,77 @@
-# Ensure emacs always runs from this makefile's PWD
-EMACS_FLAGS=--eval '(setq user-emacs-directory default-directory)' -l core/core.el
-EMACS=emacs --quick --batch $(EMACS_FLAGS)
-EMACSI=emacs -q $(EMACS_FLAGS)
+DOOM = "bin/doom"
+MODULES = $(patsubst modules/%/, %, $(sort $(dir $(wildcard modules/*/ modules/*/*/))))
-MODULES=$(patsubst modules/%, %, $(shell find modules/ -maxdepth 2 -type d))
-
-all: autoloads autoremove install
+all:
+ @$(DOOM) refresh
## Shortcuts
a: autoloads
i: install
u: update
+U: upgrade
r: autoremove
c: compile
cc: compile-core
-ce: compile-elpa
+cp: compile-plugins
+re: recompile
+d: doctor
+
+quickstart:
+ @$(DOOM) quickstart
+
## Package management
-install: init.el .local/autoloads.el
- @$(EMACS) -f doom//packages-install
-
-update: init.el .local/autoloads.el
- @$(EMACS) -f doom//packages-update
-
-autoremove: init.el .local/autoloads.el
- @$(EMACS) -f doom//packages-autoremove
-
-autoloads: init.el
- @$(EMACS) -f doom//reload-autoloads
-
+install:
+ @$(DOOM) install
+update:
+ @$(DOOM) update
+autoremove:
+ @$(DOOM) autoremove
+autoloads:
+ @$(DOOM) autoloads
+upgrade:
+ @$(DOOM) upgrade
## Byte compilation
-# compile
-# compile-core
+compile:
+ @$(DOOM) compile
+compile-core:
+ @$(DOOM) compile :core
+compile-private:
+ @$(DOOM) compile :private
+compile-plugins:
+ @$(DOOM) compile :plugins
+recompile:
+ @$(DOOM) recompile
+clean:
+ @$(DOOM) clean
# compile-module
# compile-module/submodule
-compile: init.el clean
- @$(EMACS) -f doom//byte-compile
-
-compile-core: init.el clean
- @$(EMACS) -f doom//byte-compile-core
-
-compile-elpa: init.el
- @$(EMACS) -f doom//byte-recompile-plugins
-
-$(patsubst %, compile-%, $(MODULES)): init.el .local/autoloads.el
- @$(EMACS) -f doom//byte-compile -- $(patsubst compile-%, %, $@)
-
-recompile: init.el
- @$(EMACS) -f doom//byte-compile -- -r
-
-clean:
- @$(EMACS) -f doom//clean-byte-compiled-files
+$(patsubst %, compile-%, $(MODULES)): | .local/autoloads.el
+ @$(DOOM) $@ $(subst compile-, , $@)
## Unit tests
-# test
-# test-core
+test:
+ @$(DOOM) test
+test-core:
+ @$(DOOM) test :core
# test-module
# test-module/submodule
-test: init.el .local/autoloads.el
- @$(EMACS) -f doom//run-tests
-
-test-core $(patsubst %, test-%, $(MODULES)): init.el .local/autoloads.el
- @$(EMACS) -f doom//run-tests -- $(subst test-, , $@)
-
-# run tests interactively
-testi: init.el .local/autoloads.el
- @$(EMACSI) -f doom//run-tests
+$(patsubst %, test-%, $(MODULES)):
+ @$(DOOM) test $(subst test-, , $@)
## Utility tasks
-# Runs Emacs from a different folder than ~/.emacs.d
+# Runs Emacs from a different folder than ~/.emacs.d; only use this for testing!
run:
- @$(EMACSI) -l init.el
+ @$(DOOM) run $(ARGS)
+# Prints debug info about your current setup
+info:
+ @$(DOOM) info
# Diagnoses potential OS/environment issues
doctor:
- @bin/doom-doctor
-
-## Internal tasks
-init.el:
- @$(error No init.el file; create one or copy init.example.el)
-
-.local/autoloads.el:
- @$(EMACS) -f doom-initialize-autoloads
+ @$(DOOM) doctor
.PHONY: all compile test testi clean
diff --git a/README.md b/README.md
index 241b8cc52..90fec667e 100644
--- a/README.md
+++ b/README.md
@@ -1,137 +1,197 @@
-
-[](https://travis-ci.org/hlissner/doom-emacs)
-[](https://travis-ci.org/hlissner/doom-emacs)
-[](./LICENSE)
+
-[](/../../tree/screenshots)
-
-- - -
+
+
+
+
+
+
+
+
+
+
- Wiki |
- Screenshots |
- Troubleshooting |
- FAQ |
- Changelog
+ wiki |
+ screenshots |
+ faq |
+ troubleshooting
+
- - -
+Quick start
+-----------
+
+```bash
+git clone https://github.com/hlissner/doom-emacs ~/.emacs.d
+~/.emacs.d/bin/doom quickstart
+```
+
+> Doom supports Emacs 25.3 and newer, but **Emacs 26.1 is recommended.** Doom
+> works best on Linux & MacOS. Your mileage may vary on Windows.
+
+
+Table of Contents
+==================
+- [What is Doom Emacs](#what-is-doom-emacs)
+ - [Doom's mantras](#dooms-mantras)
+ - [Feature highlights](#feature-highlights)
+- [Getting Help](#getting-help)
+- [Contributing](#contributing)
+
+
+What is Doom Emacs
+==================
+
-
+
It is a story as old as time. A stubborn, shell-dwelling, and melodramatic
vimmer -- envious of the features of modern text editors -- spirals into despair
-before finally succumbing to the [dark side][evil-mode]. This is his config.
+before succumbing to the [dark side][url:evil-mode]. This is his config.
-Doom strives to be fast, fabulous and hacker friendly. It is tailored for
-neckbeards with blue belts or better in command-line-fu, Elisp and git.
+Doom is a configuration for [GNU Emacs](https://www.gnu.org/software/emacs/). It
+can be used as framework for your own configuration, or as a resource for fellow
+Emacs enthusiasts who want to learn more about our favorite OS.
-> Doom **only** supports Emacs >= 25.1, and is tested on Arch Linux 4.7+ and
-> MacOS 10.11. YMMV on other platforms.
+Doom's mantras
+--------------
+- **Gotta go fast.** Startup and runtime speed are high priorities; many
+ expensive, heavy-handed features and packages have been fine-tuned to that
+ end.
+- **Hacker-friendly.** Doom caters to the command line denizen unafraid of
+ writing a little (or a lot of) code to tailor their editor. It also inherits
+ your shell configuration, warts 'n all, and expects frequent trips into the
+ terminal to manage Doom with its `bin/doom` utility.
+- **Opinionated, but not stubborn.** Doom has _many_ opinions spread out across
+ its 120+ modules designed to iron out idiosynchrosies and provide a better and
+ more consistent baseline experience of Emacs and its plugins. However, they
+ mustn't ever compromise your ability to change, rewrite or disable any or all
+ of it, if you ask nicely.
+- **Written to be read.** Doom's source ought to be self documenting and easy to
+ grok. Modules should be syntactically sweet and concise, and backend logic
+ should be explicit and abstraction-light. Where complexity arises, comments
+ and documentation shouldn't be far away.
-- - -
+Feature Highlights
+------------------
+- A declarative [package management system][doom:packages] with a command line
+ interface that combines package.el, [use-package] and [quelpa], allowing you
+ to install packages from anywhere.
+- A [popup management system][doom:popups] with customizable rules to dictate
+ how temporary/disposable buffers are displayed.
+- A vim-centric (and optional) experience with [evil-mode][url:evil-mode],
+ including ports of several popular vim plugins, C-x omnicompletion
+ and a slew of [custom ex commands][doom:commands].
+- A Spacemacs-esque [keybinding scheme][doom:bindings], centered around leader
+ and localleader prefix keys (SPC and SPCm, by
+ default).
+- Indentation detection and optional integration with
+ [editorconfig][url:editorconfig]. Let someone else argue about tabs vs
+ ___***spaces***___.
+- Code completion for many languages, powered by
+ [company-mode][url:company-mode] (some may have external dependencies).
+- Project-awareness powered by [projectile][url:projectile], with tools and an
+ API to navigate and manage projects, as well as project/framework-specific
+ minor modes and snippets libraries (and the ability to define your own).
+- Project search (and replace) utilities, powered by
+ [the_silver_searcher][url:ag], [ripgrep][url:rg], git-grep and
+ [wgrep][url:wgrep], with integration for [ivy][url:ivy] (the default) and
+ [helm][url:helm].
+- Isolated and persistent workspaces powered by [persp-mode][url:persp-mode].
+ Also substitutes as vim tabs.
+- Inline/live code evaluation (using [quickrun][url:quickrun]), with REPL
+ support for a variety of languages.
+- A jump-to-definition/references implementation for all languages that tries to
+ "just work," resorting to mode-specific functionality, before falling back on
+ [dump-jump][url:dumb-jump].
-## Quick start
-```bash
-git clone https://github.com/hlissner/doom-emacs ~/.emacs.d
-cd ~/.emacs.d
-cp init.example.el init.el # maybe edit init.el
-make install
-```
+Troubleshooting
+===============
-Don't forget to run `make` every time you modify init.el!
+Encountered strange behavior or an error? Here are some things to try before you
+shoot off that bug report:
-Visit the wiki for [a more detailed guide on installing, customizing and
-grokking Doom][wiki].
+- Run `bin/doom refresh`. This ensures Doom is properly set up and its autoloads
+ files are up-to-date.
+- If you have byte-compiled your config (with `bin/doom compile`), see if
+ `bin/doom clean` makes your issue go away. Never debug issues with a
+ byte-compiled config, it will only make your job harder.
+- Run `bin/doom doctor` to detect common issues in your development environment.
+- Search Doom's issue tracker for mention of any error messages you've received.
+- [Visit our FAQ][docs:faq] to see if your issue is listed.
-## Feature highlights
+If all else fails, [file that bug report][github:new-issue]! Please include the
+behavior you've observed, the behavior you expected, and any error message in
+the \*Messages\* buffer (can be opened with SPC h m or `M-x
+view-echo-area-messages`). It'd be a great help if you included a backtrace with
+them as well.
-+ A fast, organized and opinionated Emacs configuration with a command line
- interface.
-+ A custom, declarative [package management system][doom-packages] that combines
- package.el, [use-package] and [quelpa], allowing you to manage packages from
- the command line and install packages from sources other than ELPA.
-+ A [popup management system][doom-popups] (powered by [shackle]) that minimizes
- the presence and footprint of temporary and/or disposable buffers.
-+ A vim-like experience with [evil-mode], including ports for several vim
- plugins, C-x omnicompletion and a slew of [custom ex
- commands][doom-my-commands].
-+ Integration with [editorconfig]. Let someone else argue about tabs and spaces.
- (spaces, duh).
-+ Code completion for many languages, powered by [company-mode] (some languages
- may have external dependencies).
-+ Project-awareness powered by [projectile], with tools and an API to navigate
- and manage projects and their files.
-+ Fast project search (and replace) utilities, powered by [the_silver_searcher],
- [ripgrep] and [wgrep], with integration for [ivy] (the default), [helm] and
- ido.
-+ Isolated and persistent workspaces powered by [persp-mode]. Also substitutes
- for vim tabs.
-+ Inline/live code evaluation (using [quickrun]), including REPLs for a variety
- of languages.
+We've also got [a Discord server][url:discord]. Hop on! We can help!
-## Troubleshooting
-Found a problem? Here are some things to try:
-
-+ Run `make install` to ensure all plugins are installed.
-+ `void-function` or `void-variable` errors could signal an out-of-date
- autoloads file. Run `make autoloads` or `M-x doom//reload-autoloads` to update
- it.
-+ Scan for common OS/environment issues with `make doctor`.
-+ **Never debug byte-compiled code. It will interfere in subtle ways.** Clean up
- \*.elc files with `make clean` or `M-x doom//clean-byte-compiled-files`.
-+ Check [the FAQ][wiki-troubleshooting] to see if your issue is mentioned.
-+ Check the relevant module's README.org, if one exists. There may be extra
- steps to getting certain features to work.
-
-If all else has failed, [file a bug report][doom-new-issue].
-
-## Contribute
+Contributing
+============
Doom (and my Emacs work in general) is a labor of love and incurable madness,
-done on my spare time. It wasn't intended for public use, but I enjoy making
-Doom a resource for others.
+done on my spare time. If you'd like to support my work, I welcome
+contributions:
-If you'd like to support my efforts, I welcome contributions of any kind:
-
-+ I love pull requests and bug reports. Elisp pointers are especially welcome.
- Seriously, don't hesitate to [tell me my Elisp-fu sucks][doom-new-issue]!
-+ Talk to me about Emacs workflow, ideas or tooling. Or talk to me about
- gamedev, or pixel art, or anime, or programming, or the weather, or band camp.
- Whatever. I don't mind. Holler at henrik@lissner.net.
+- I love pull requests and bug reports. Check out the [Contributing
+ Guidelines][docs:contributing] (WIP) to find out how you can help out.
+- I welcome Elisp pointers! Don't hesitate to [tell me my Elisp-fu
+ sucks][github:new-issue] (but please tell me why).
+- Hop on [our Discord server][url:discord] and say hi! Help others out, hang out
+ or talk to me about Emacs, or gamedev, or programming, machine learning,
+ physics, pixel art, anime, gaming -- anything you like. Nourish this lonely
+ soul!
+- If you'd like to support my work financially, consider buying me a drink
+ through [liberapay][url:liberapay] or [paypal][url:paypal]. Donations are a
+ great help. My work here contends with full-time studies, my ventures in indie
+ gamedev, and my freelance work.
-[wiki]: /../../wiki
-[wiki-conventions]: /../../wiki/Conventions
-[wiki-modules]: /../../wiki/Modules
-[wiki-customization]: /../../wiki/Customization
-[wiki-troubleshooting]: /../../wiki/FAQ#troubleshooting
+
+
+
+
+
+
+[docs:faq]: /../../wiki/FAQ
-[doom-my-bindings]: modules/private/hlissner/+bindings.el
-[doom-my-commands]: modules/private/hlissner/+commands.el
-[doom-new-issue]: https://github.com/hlissner/doom-emacs/issues/new
-[doom-packages]: core/autoload/packages.el
-[doom-popups]: core/core-popups.el
-[doom-theme]: https://github.com/hlissner/emacs-doom-theme
+[github:new-issue]: https://github.com/hlissner/doom-emacs/issues/new
+[doom:bindings]: modules/config/default/+bindings.el
+[doom:commands]: modules/config/default/+evil-commands.el
+[doom:packages]: core/autoload/packages.el
+[doom:popups]: modules/feature/popup/README.org
-[company-mode]: https://github.com/company-mode/company-mode
-[editorconfig]: http://editorconfig.org/
-[evil-mode]: https://github.com/emacs-evil/evil
-[git-gutter-fringe]: https://github.com/syohex/emacs-git-gutter-fringe
-[helm]: https://github.com/emacs-helm/helm
-[ivy]: https://github.com/abo-abo/swiper
-[persp-mode]: https://github.com/Bad-ptr/persp-mode.el
-[projectile]: https://github.com/bbatsov/projectile
-[quelpa]: https://github.com/quelpa/quelpa
-[quickrun]: https://github.com/syohex/emacs-quickrun
-[ripgrep]: https://github.com/BurntSushi/ripgrep
-[shackle]: https://github.com/wasamasa/shackle
-[the_silver_searcher]: https://github.com/ggreer/the_silver_searcher
-[use-package]: https://github.com/jwiegley/use-package
-[vim]: https://github.com/hlissner/.vim
-[wgrep]: https://github.com/mhayashi1120/Emacs-wgrep
+[url:discord]: https://discord.gg/bcZ6P3y
+[url:liberapay]: https://liberapay.com/hlissner/donate
+[url:paypal]: https://paypal.me/henriklissner/10
+[url:company-mode]: https://github.com/company-mode/company-mode
+[url:doom-themes]: https://github.com/hlissner/emacs-doom-themes
+[url:editorconfig]: http://editorconfig.org/
+[url:evil-mode]: https://github.com/emacs-evil/evil
+[url:helm]: https://github.com/emacs-helm/helm
+[url:ivy]: https://github.com/abo-abo/swiper
+[url:persp-mode]: https://github.com/Bad-ptr/persp-mode.el
+[url:projectile]: https://github.com/bbatsov/projectile
+[url:quelpa]: https://github.com/quelpa/quelpa
+[url:quickrun]: https://github.com/syohex/emacs-quickrun
+[url:ripgrep]: https://github.com/BurntSushi/ripgrep
+[url:the_silver_searcher]: https://github.com/ggreer/the_silver_searcher
+[url:use-package]: https://github.com/jwiegley/use-package
+[url:wgrep]: https://github.com/mhayashi1120/Emacs-wgrep
diff --git a/bin/doom b/bin/doom
new file mode 100755
index 000000000..9178f630e
--- /dev/null
+++ b/bin/doom
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+":"; [[ $EMACS = *"term"* ]] && EMACS=emacs || EMACS=${EMACS:-emacs} # -*-emacs-lisp-*-
+":"; command -v $EMACS >/dev/null || { >&2 echo "Emacs isn't installed"; exit 1; }
+":"; VERSION=$($EMACS --version | head -n1)
+":"; [[ $VERSION == *\ 2[0-2].[0-1].[0-9] ]] && { echo "You're running $VERSION"; echo "That version is too old to run Doom. Check your PATH"; echo; exit 2; }
+":"; DOOMBASE=$(dirname "${BASH_SOURCE:-${(%):-%x}}")/..
+":"; [[ $1 == doc || $1 == doctor ]] && { cd "$DOOMBASE"; exec $EMACS --script bin/doom-doctor; exit 0; }
+":"; [[ $1 == run ]] && { cd "$DOOMBASE"; shift; exec $EMACS -q --no-splash -l bin/doom "$@"; exit 0; }
+":"; exec $EMACS --script "$0" -- $@
+":"; exit 0
+
+(defun usage ()
+ (with-temp-buffer
+ (insert (format! "%s %s [COMMAND] [ARGS...]\n"
+ (bold "Usage:")
+ (file-name-nondirectory load-file-name))
+ "\n"
+ "A command line interface for managing Doom Emacs; including\n"
+ "package management, diagnostics, unit tests, and byte-compilation.\n"
+ "\n"
+ "This tool also makes it trivial to launch Emacs out of a different\n"
+ "folder or with a different private module.\n"
+ "\n"
+ (format! (bold "Example:\n"))
+ " doom install\n"
+ " doom help update\n"
+ " doom compile :core lang/php lang/python\n"
+ " doom run\n"
+ " doom run -nw file.txt file2.el\n"
+ " doom run -p ~/.other.doom.d -e ~/.other.emacs.d -nw file.txt\n"
+ "\n"
+ (format! (bold "Options:\n"))
+ " -h --help\t\tSame as help command\n"
+ " -d --debug\t\tTurns on doom-debug-mode (and debug-on-error)\n"
+ " -e --emacsd DIR\tUse the emacs config at DIR (e.g. ~/.emacs.d)\n"
+ " -i --insecure\t\tDisable TLS/SSL validation (not recommended)\n"
+ " -p --private DIR\tUse the private module at DIR (e.g. ~/.doom.d)\n"
+ " -y --yes\t\tAuto-accept all confirmation prompts\n\n")
+ (princ (buffer-string)))
+ (doom--dispatch-help))
+
+;;
+(let ((args (cdr (cdr (cdr command-line-args))))
+ (emacs-dir (or (getenv "EMACSDIR")
+ (expand-file-name "../" (file-name-directory (file-truename load-file-name))))))
+ ;; Parse options
+ (while (ignore-errors (string-prefix-p "-" (car args)))
+ (pcase (pop args)
+ ((or "-h" "--help")
+ (push "help" args))
+ ((or "-d" "--debug")
+ (setenv "DEBUG" "1")
+ (message "Debug mode on"))
+ ((or "-i" "--insecure")
+ (setenv "INSECURE" "1")
+ (message "Insecure mode on"))
+ ((or "-p" "--private")
+ (setq doom-private-dir (expand-file-name (concat (pop args) "/")))
+ (setenv "DOOMDIR" doom-private-dir)
+ (message "DOOMDIR changed to %s" doom-private-dir)
+ (or (file-directory-p doom-private-dir)
+ (message "Warning: %s does not exist"
+ (abbreviate-file-name doom-private-dir))))
+ ((or "-e" "--emacsd")
+ (setq emacs-dir (expand-file-name (concat (pop args) "/")))
+ (message "Emacs directory changed to %s" emacs-dir))
+ ((or "-y" "--yes")
+ (setenv "YES" "1")
+ (message "Auto-yes mode on"))))
+
+ (or (file-directory-p emacs-dir)
+ (error "%s does not exist" emacs-dir))
+
+ ;; Bootstrap Doom
+ (load (expand-file-name "init" emacs-dir)
+ nil 'nomessage)
+
+ (cond ((not noninteractive)
+ (doom|run-all-startup-hooks))
+ ((and (not (cdr args))
+ (member (car args) '("help" "h")))
+ (usage))
+ ((not args)
+ (message "No command detected, aborting!\n\nRun %s help for documentation."
+ (file-name-nondirectory load-file-name)))
+ ((let ((default-directory emacs-dir))
+ (setq argv nil
+ noninteractive 'doom)
+ (condition-case e
+ (doom-dispatch (car args) (cdr args))
+ (user-error
+ (signal (car e) (cdr e)))
+ ((debug error)
+ (message "--------------------------------------------------\n")
+ (message "There was an unexpected error:")
+ (message " %s (%s)" (get (car e) 'error-message) (car e))
+ (dolist (item (cdr e))
+ (message " %s" item))
+ (unless debug-on-error
+ (message
+ (concat "\nRun the command again with the -d (or --debug) option to enable debug\n"
+ "mode and, hopefully, generate a stack trace. If you decide to file a bug\n"
+ "report, please include it!\n\n"
+ "Emacs outputs to standard error, so you'll need to redirect stderr to\n"
+ "stdout to pipe this to a file or clipboard!\n\n"
+ " e.g. doom -d install 2>&1 | clipboard-program"))
+ (signal 'doom-error e))))))))
diff --git a/bin/doom-doctor b/bin/doom-doctor
index eb8b55108..626742601 100755
--- a/bin/doom-doctor
+++ b/bin/doom-doctor
@@ -1,41 +1,56 @@
#!/usr/bin/env bash
":"; command -v emacs >/dev/null || { >&2 echo "Emacs isn't installed"; exit 1; } # -*-emacs-lisp-*-
-":"; [[ $(emacs --version | head -n1) == *\ 2[0-2].[0-1].[0-9] ]] && { echo "You're running $(emacs --version | head -n1)"; echo "That version is too old to run the doctor. Check your PATH"; echo; exit 2; } || exec emacs --quick --script "$0"
+":"; VERSION=$(emacs --version | head -n1)
+":"; [[ $VERSION == *\ 2[0-2].[0-1].[0-9] ]] && { echo "You're running $VERSION"; echo "That version is too old to run the doctor. Check your PATH"; echo; exit 2; }
+":"; exec emacs --quick --script "$0"; exit 0
-;; Uses a couple simple heuristics to locate issues with your environment that
-;; could interfere with running or setting up DOOM Emacs.
+;; The Doom doctor is essentially one big, self-contained elisp shell script
+;; that uses a series of simple heuristics to diagnose common issues on your
+;; system. Issues that could intefere with Doom Emacs.
+;;
+;; Doom modules may optionally have a doctor.el file to run their own heuristics
+;; in. Doctor scripts may run in versions of Emacs as old as Emacs 23, so make
+;; no assumptions about the standard library limited to very basic standard
+;; library (e.g. avoid cl/cl-lib, subr-x, map, seq, etc).
-;; In case it isn't defined (in really old versions of Emacs, like the one that
-;; ships with MacOS).
-(defvar user-emacs-directory (expand-file-name "~/.emacs.d/"))
-(defvar doom-debug-mode (getenv "DEBUG"))
-(unless (equal (expand-file-name user-emacs-directory)
- (expand-file-name "~/.emacs.d/"))
- (error "Couldn't find ~/.emacs.d"))
+;; Ensure Doom doctor always runs out of the current Emacs directory (optionally
+;; specified by the EMACSDIR envvar)
+(setq user-emacs-directory
+ (or (getenv "EMACSDIR")
+ (expand-file-name "../" (file-name-directory load-file-name))))
+
+(unless (file-directory-p user-emacs-directory)
+ (error "Couldn't find a Doom config!"))
+(unless noninteractive
+ (error "This script must not be run from an interactive session."))
+(when (getenv "DEBUG")
+ (setq debug-on-error t))
(require 'pp)
-(defsubst string-trim-right (string &optional regexp)
- (if (string-match (concat "\\(?:" (or regexp "[ \t\n\r]+") "\\)\\'") string)
- (replace-match "" t t string)
- string))
-;;
+;;; Helpers
(defvar doom-init-p nil)
+(defvar doom-warnings 0)
(defvar doom-errors 0)
-(defmacro check! (cond &rest body)
+(defmacro when! (cond &rest body)
(declare (indent defun))
`(let ((it ,cond))
- (when it
- ,@body
- (setq doom-errors (1+ doom-errors)))))
+ (when it ,@body)))
(defun indented (spc msg)
(declare (indent defun))
(with-temp-buffer
(insert msg)
- (indent-rigidly (point-min) (point-max) spc)
+ (let ((fill-column 80))
+ (fill-region (point-min) (point-max))
+ (indent-rigidly (point-min) (point-max) spc))
+ (when (> spc 2)
+ (goto-char (point-min))
+ (beginning-of-line-text)
+ (delete-char -2)
+ (insert "> "))
(buffer-string)))
(defun autofill (&rest msgs)
@@ -48,34 +63,34 @@
(fill-region (point-min) (point-max))
(buffer-string))))
-(defun columns (cols length strings)
- (declare (indent defun))
- (with-temp-buffer
- (let ((sub-format (format "%%-%ds " (1- length)))
- col-format)
- (dotimes (i (1- cols))
- (setq col-format (concat col-format sub-format)))
- (setq col-format (concat col-format "%s"))
- (while strings
- (insert (apply #'format col-format
- (let (args)
- (dotimes (i cols (nreverse args))
- (push (if strings (pop strings) "") args))))
- "\n")))
- (buffer-string)))
-
(defun sh (cmd)
(string-trim-right (shell-command-to-string cmd)))
(defun color (code msg &rest args)
(format "\e[%dm%s\e[%dm" code (apply #'format msg args) 0))
-(defalias 'msg! #'message)
-(defmacro error! (&rest args) `(msg! (color 1 (color 31 ,@args))))
-(defmacro warn! (&rest args) `(msg! (color 1 (color 33 ,@args))))
-(defmacro success! (&rest args) `(msg! (color 1 (color 32 ,@args))))
-(defmacro section! (&rest args) `(msg! (color 34 ,@args)))
-(defmacro explain! (&rest args) `(msg! (indented 2 (autofill ,@args))))
+(defvar indent 0)
+(defvar prefix "")
+(defmacro msg! (msg &rest args)
+ `(message
+ (indented indent
+ (format (concat prefix ,msg)
+ ,@args))))
+(defmacro error! (&rest args) `(progn (msg! (color 31 ,@args)) (setq doom-errors (+ doom-errors 1))))
+(defmacro warn! (&rest args) `(progn (msg! (color 33 ,@args)) (setq doom-warnings (+ doom-warnings 1))))
+(defmacro success! (&rest args) `(msg! (color 32 ,@args)))
+(defmacro section! (&rest args)
+ `(msg! (color 1 (color 34 ,@args))))
+
+(defmacro explain! (&rest args)
+ `(message (indented (+ indent 2) (autofill ,@args))))
+
+(defun elc-check-dir (dir)
+ (dolist (file (directory-files-recursively dir "\\.elc$"))
+ (when (file-newer-than-file-p (concat (file-name-sans-extension file) ".el")
+ file)
+ (warn! "%s is out-of-date" (abbreviate-file-name file)))))
+
;;; Polyfills
;; early versions of emacs won't have this
@@ -84,110 +99,139 @@
(save-match-data
(string-match regexp string &optional start))))
+;; subr-x may not exist in the current version of Emacs
+(unless (fboundp 'string-trim-right)
+ (defsubst string-trim-right (string &optional regexp)
+ (if (string-match (concat "\\(?:" (or regexp "[ \t\n\r]+") "\\)\\'") string)
+ (replace-match "" t t string)
+ string)))
+
;; --- start a'doctorin' --------------------------------------
-(msg! "%s\nRunning Emacs v%s, commit %s\n"
- (color 1 "DOOM Doctor")
- (color 1 emacs-version)
- (if (executable-find "git")
- (sh "git rev-parse HEAD")
+(msg! (color 1 "Doom Doctor"))
+(msg! "Emacs v%s" emacs-version)
+(msg! "Doom v%s (%s)"
+ (or (let ((core-file (expand-file-name "core/core.el" user-emacs-directory)))
+ (and (file-exists-p core-file)
+ (with-temp-buffer
+ (insert-file-contents-literally core-file)
+ (goto-char (point-min))
+ (when (re-search-forward "doom-version" nil t)
+ (forward-char)
+ (sexp-at-point)))))
+ "???")
+ (if (and (executable-find "git")
+ (file-directory-p (expand-file-name ".git" user-emacs-directory)))
+ (sh "git log -1 --format=\"%D %h %ci\"")
"n/a"))
-
(msg! "shell: %s%s"
(getenv "SHELL")
(if (equal (getenv "SHELL") (sh "echo $SHELL"))
""
(color 31 " (mismatch)")))
(when (boundp 'system-configuration-features)
- (msg! "Compiled with:\n%s" (indented 2 (autofill system-configuration-features))))
-(msg! "uname -a:\n%s\n" (indented 2 (autofill (sh "uname -a"))))
+ (message "Compiled with:\n%s" (indented 2 system-configuration-features)))
+(message "uname -msrv:\n%s\n" (indented 2 (sh "uname -msrv")))
-(let (doom-core-packages doom-debug-mode)
- (condition-case ex
- (progn
- (let ((inhibit-message t)
- noninteractive)
- (load "~/.emacs.d/init.el" nil t))
- (doom-initialize-packages)
- (doom|finalize)
- (success! "Attempt to load DOOM: success! Loaded v%s" doom-version)
- (when (executable-find "git")
- (msg! "Revision %s\n"
- (ignore-errors
- (let ((default-directory user-emacs-directory))
- (sh "git rev-parse HEAD"))))))
- ('error (warn! "Attempt to load DOOM: failed\n %s\n"
- (or (cdr-safe ex) (car ex))))))
-
-(msg! "----\n")
;; --- is emacs set up properly? ------------------------------
-(section! "test-emacs")
-(check! (version< emacs-version "25.1")
- (error! "Important: Emacs %s detected [%s]" emacs-version (executable-find "emacs"))
- (explain!
- "DOOM only supports >= 25.1. Perhaps your PATH wasn't set up properly."
- (when (eq system-type 'darwin)
- (concat "\nMacOS users should use homebrew (https://brew.sh) to install Emacs\n"
- " brew install emacs --with-modules --with-imagemagick --with-cocoa"))))
+(section! "Checking Emacs")
+(let ((indent 4))
+ (section! "Checking your Emacs version is 25.3 or newer...")
+ (when (version< emacs-version "25.3")
+ (error! "Important: Emacs %s detected [%s]" emacs-version (executable-find "emacs"))
+ (explain!
+ "DOOM only supports >= 25.3. Perhaps your PATH wasn't set up properly."
+ (when (eq system-type 'darwin)
+ (concat "\nMacOS users should use homebrew (https://brew.sh) to install Emacs\n"
+ " brew install emacs --with-modules --with-imagemagick --with-cocoa"))))
+
+ (section! "Checking if your version of Emacs has changed recently...")
+ (let ((version-file (expand-file-name ".local/emacs-version.el" user-emacs-directory))
+ doom--last-emacs-version)
+ (when (and (load version-file 'noerror 'nomessage 'nosuffix)
+ (not (equal emacs-version doom--last-emacs-version)))
+ (warn! "Your version of Emacs has changed from %S to %S. Recompile your packages!"
+ doom--last-emacs-version
+ emacs-version)
+ (explain! "Byte-code compiled in one version of Emacs may not work in another version."
+ "It is recommended that you reinstall your plugins or recompile them with"
+ "`bin/doom compile :plugins'.")))
+
+ (section! "Checking for private config conflicts...")
+ (let ((xdg-dir (concat (or (getenv "XDG_CONFIG_HOME")
+ "~/.config")
+ "/doom/"))
+ (doom-dir (or (getenv "DOOMDIR")
+ "~/.doom.d/")))
+ (when (and (not (file-equal-p xdg-dir doom-dir))
+ (file-directory-p xdg-dir)
+ (file-directory-p doom-dir))
+ (warn! "Detected two private configs, in %s and %s"
+ (abbreviate-file-name xdg-dir)
+ doom-dir)
+ (explain! "The second directory will be ignored, as it has lower precedence.")))
+
+ (section! "Checking for stale elc files...")
+ (elc-check-dir user-emacs-directory))
;; --- is the environment set up properly? --------------------
-;; windows? windows
-(section! "test-windows")
-(check! (memq system-type '(windows-nt ms-dos cygwin))
- (warn! "Warning: Windows detected")
- (explain! "DOOM was designed for MacOS and Linux. Expect a bumpy ride!"))
+(section! "Checking your system...")
+(let ((indent 4))
+ ;; on windows?
+ (when (memq system-type '(windows-nt ms-dos cygwin))
+ (warn! "Warning: Windows detected")
+ (explain! "DOOM was designed for MacOS and Linux. Expect a bumpy ride!"))
-;; are all default fonts present
-(section! "test-fonts")
-(if (not (fboundp 'find-font))
- (progn
- (warn! "Warning: unable to detect font")
- (explain! "The `find-font' function is missing. This could indicate the incorrect "
- "version of Emacs is being used!"))
- ;; all-the-icons fonts
- (let ((font-dest (pcase system-type
- ('gnu/linux (concat (or (getenv "XDG_DATA_HOME")
- (concat (getenv "HOME") "/.local/share"))
- "/fonts/"))
- ('darwin (concat (getenv "HOME") "/Library/Fonts/")))))
- (when (and font-dest (require 'all-the-icons nil t))
- (dolist (font all-the-icons-font-names)
- (if (file-exists-p (expand-file-name font font-dest))
- (success! "Found font %s" font)
- (warn! "Warning: couldn't find %s font in %s"
- font font-dest)
- (explain! "You can install it by running `M-x all-the-icons-install-fonts' within Emacs.\n\n"
- "This could also mean you've installed them in non-standard locations, in which "
- "case, ignore this warning."))))))
+ ;; are all default fonts present?
+ (section! "Checking your fonts...")
+ (if (not (fboundp 'find-font))
+ (progn
+ (warn! "Warning: unable to detect font")
+ (explain! "The `find-font' function is missing. This could indicate the incorrect "
+ "version of Emacs is being used!"))
+ ;; all-the-icons fonts
+ (let ((font-dest (pcase system-type
+ (`gnu/linux (concat (or (getenv "XDG_DATA_HOME")
+ "~/.local/share")
+ "/fonts/"))
+ (`darwin "~/Library/Fonts/"))))
+ (when (and font-dest (require 'all-the-icons nil t))
+ (dolist (font all-the-icons-font-names)
+ (if (file-exists-p (expand-file-name font font-dest))
+ (success! "Found font %s" font)
+ (warn! "Warning: couldn't find %s font in %s"
+ font font-dest)
+ (explain! "You can install it by running `M-x all-the-icons-install-fonts' within Emacs.\n\n"
+ "This could also mean you've installed them in non-standard locations, in which "
+ "case feel free to ignore this warning."))))))
-;; gnutls-cli & openssl
-(section! "test-gnutls")
-(cond ((executable-find "gnutls-cli"))
- ((executable-find "openssl")
- (let* ((output (sh "openssl ciphers -v"))
- (protocols
- (let (protos)
- (mapcar (lambda (row)
- (add-to-list 'protos (cadr (split-string row " " t))))
- (split-string (sh "openssl ciphers -v") "\n"))
- (delq nil protos))))
- (check! (not (or (member "TLSv1.1" protocols)
- (member "TLSv1.2" protocols)))
- (let ((version (cadr (split-string (sh "openssl version") " " t))))
- (warn! "Warning: couldn't find gnutls-cli, and OpenSSL is out-of-date (v%s)" version)
- (explain!
- "This may not affect your Emacs experience, but there are security "
- "vulnerabilities in the SSL2/3 & TLS1.0 protocols. You should use "
- "TLS 1.1+, which wasn't introduced until OpenSSL v1.0.1.\n\n"
+ ;; gnutls-cli & openssl
+ (section! "Checking gnutls/openssl...")
+ (cond ((executable-find "gnutls-cli"))
+ ((executable-find "openssl")
+ (let* ((output (sh "openssl ciphers -v"))
+ (protocols
+ (let (protos)
+ (mapcar (lambda (row)
+ (add-to-list 'protos (cadr (split-string row " " t))))
+ (split-string (sh "openssl ciphers -v") "\n"))
+ (delq nil protos))))
+ (unless (or (member "TLSv1.1" protocols)
+ (member "TLSv1.2" protocols))
+ (let ((version (cadr (split-string (sh "openssl version") " " t))))
+ (warn! "Warning: couldn't find gnutls-cli, and OpenSSL is out-of-date (v%s)" version)
+ (explain!
+ "This may not affect your Emacs experience, but there are security "
+ "vulnerabilities in the SSL2/3 & TLS1.0 protocols. You should use "
+ "TLS 1.1+, which wasn't introduced until OpenSSL v1.0.1.\n\n"
- "Please consider updating (or install gnutls-cli, which is preferred).")))))
- (t
- (check! t
+ "Please consider updating (or install gnutls-cli, which is preferred).")))))
+ (t
(error! "Important: couldn't find either gnutls-cli nor openssl")
(explain!
"You won't be able to install/update packages because Emacs won't be able to "
@@ -200,127 +244,156 @@
"But remember that you're leaving your security in the hands of your "
"network, provider, government, neckbearded mother-in-laws, geeky roommates, "
- "or just about anyone who knows more about computers than you do!"))))
+ "or just about anyone who knows more about computers than you do!")))
-(section! "test-tls")
-(cond ((not (string-match-p "\\_" system-configuration-features))
- (warn! "Warning: You didn't install Emacs with gnutls support")
- (explain!
- "This may cause 'pecular error' errors with the Doom doctor, and is likely to "
- "interfere with package management. Your mileage may vary."
- (when (eq system-type 'darwin)
- (concat "\nMacOS users are advised to install Emacs via homebrew with one of the following:\n"
- " brew install emacs --with-gnutls"
- " or"
- " brew tap d12frosted/emacs-plus"
- " brew install emacs-plus"))))
+ ;; are certificates validated properly?
+ (section! "Testing your root certificates...")
+ (cond ((not (ignore-errors (gnutls-available-p)))
+ (warn! "Warning: Emacs wasn't installed with gnutls support")
+ (explain!
+ "This may cause 'pecular error' errors with the Doom doctor, and is likely to "
+ "interfere with package management. Your mileage may vary."
+ (when (eq system-type 'darwin)
+ (concat "\nMacOS users are advised to install Emacs via homebrew with one of the following:\n"
+ " brew install emacs --with-gnutls"
+ " or"
+ " brew tap d12frosted/emacs-plus"
+ " brew install emacs-plus"))))
- ((not (fboundp 'url-retrieve-synchronously))
- (error! "Can't find url-retrieve-synchronously function. Are you running Emacs 24+?"))
+ ((not (fboundp 'url-retrieve-synchronously))
+ (error! "Can't find url-retrieve-synchronously function. Are you sure you're on Emacs 24+?"))
- ((or (executable-find "gnutls-cli")
- (executable-find "openssl"))
- (let ((tls-checktrust t)
- (gnutls-verify-error t))
- (dolist (url '("https://elpa.gnu.org" "https://melpa.org"))
- (check! (condition-case-unless-debug e
- (if (let ((inhibit-message t)) (url-retrieve-synchronously url))
- (ignore (success! "Validated %s" url))
- 'empty)
- ('timed-out 'timeout)
- ('error e))
- (pcase it
- (`empty (error! "Couldn't reach %s" url))
- (`timeout (error! "Timed out trying to contact %s" ex))
- (_
- (error! "Failed to validate %s" url)
- (when doom-debug-mode
- (explain! (pp-to-string it)))))))
- (dolist (url '("https://self-signed.badssl.com"
- "https://wrong.host.badssl.com/"))
- (check! (condition-case-unless-debug e
- (if (let ((inhibit-message t)) (url-retrieve-synchronously url))
- t
- 'empty)
- ('timed-out 'timeout)
- ('error (ignore (success! "Successfully rejected %s" url))))
- (pcase it
- (`empty (error! "Couldn't reach %s" url))
- (`timeout (error! "Timed out trying to contact %s" ex))
- (_
- (error! "Validated %s (this shouldn't happen!)" url)))))))
+ ((or (executable-find "gnutls-cli")
+ (executable-find "openssl"))
+ (let ((tls-checktrust t)
+ (gnutls-verify-error t))
+ (dolist (url '("https://elpa.gnu.org" "https://melpa.org"))
+ (when! (condition-case-unless-debug e
+ (unless (let ((inhibit-message t)) (url-retrieve-synchronously url))
+ 'empty)
+ ('timed-out 'timeout)
+ ('error e))
+ (pcase it
+ (`empty (error! "Couldn't reach %s" url))
+ (`timeout (error! "Timed out trying to contact %s" ex))
+ (_
+ (error! "Failed to validate %s" url)
+ (explain! (pp-to-string it))))))
+ (dolist (url '("https://self-signed.badssl.com"
+ "https://wrong.host.badssl.com/"))
+ (when! (condition-case-unless-debug e
+ (if (let ((inhibit-message t)) (url-retrieve-synchronously url))
+ t
+ 'empty)
+ ('timed-out 'timeout)
+ ('error))
+ (pcase it
+ (`empty (error! "Couldn't reach %s" url))
+ (`timeout (error! "Timed out trying to contact %s" ex))
+ (_
+ (error! "Validated %s (this shouldn't happen!)" url)))))))
- (t
- (error! "Nope!")))
+ ((error! "Nope!")))
-;; bsd vs gnu tar
-(section! "test-tar")
-(let ((tar-bin (or (executable-find "gtar")
- (executable-find "tar"))))
- (if tar-bin
- (check! (not (string-match-p "(GNU tar)" (sh (format "%s --version" tar-bin))))
- (warn! "Warning: BSD tar detected")
- (explain!
- "QUELPA (through package-build) uses the system tar to build plugins, but it "
- "expects GNU tar. BSD tar *could* cause errors during package installation or "
- "updating from non-ELPA sources."
- (when (eq system-type 'darwin)
- (concat "\nMacOS users can install gnu-tar via homebrew:\n"
- " brew install gnu-tar"))))
- (check! t ; very unlikely
+ ;; which variant of tar is on your system? bsd or gnu tar?
+ (section! "Checking for GNU/BSD tar...")
+ (let ((tar-bin (or (executable-find "gtar")
+ (executable-find "tar"))))
+ (if tar-bin
+ (unless (string-match-p "(GNU tar)" (sh (format "%s --version" tar-bin)))
+ (warn! "Warning: BSD tar detected")
+ (explain!
+ "QUELPA (through package-build) uses the system tar to build plugins, but it "
+ "expects GNU tar. BSD tar *could* cause errors during package installation or "
+ "updating from non-ELPA sources."
+ (when (eq system-type 'darwin)
+ (concat "\nMacOS users can install gnu-tar via homebrew:\n"
+ " brew install gnu-tar"))))
(error! "Important: Couldn't find tar")
(explain!
"This is required by package.el and QUELPA to build packages and will "
"prevent you from installing & updating packages."))))
-;; --- report! ------------------------------------------------
+;; --- is Doom Emacs set up correctly? ------------------------
-(when doom-debug-mode
- (msg! "\n====\nHave some debug information:\n")
+(condition-case-unless-debug ex
+ (let ((after-init-time (current-time))
+ noninteractive)
+ (section! "Checking DOOM Emacs...")
+ (load (concat user-emacs-directory "core/core.el") nil t)
+ (unless (file-directory-p doom-private-dir)
+ (error "No DOOMDIR was found, did you run `doom quickstart` yet?"))
- (when (bound-and-true-p doom-modules)
- (msg! " + enabled modules:\n%s"
- (indented 4
- (columns 3 23
- (mapcar (lambda (x) (format "+%s" x))
- (mapcar #'cdr (doom-module-pairs)))))))
+ (let ((indent 4))
+ ;; Make sure everything is loaded
+ (require 'core-cli)
+ (require 'core-keybinds)
+ (require 'core-ui)
+ (require 'core-projects)
+ (require 'core-editor)
+ (require 'core-packages)
+ (success! "Loaded Doom Emacs %s" doom-version)
- (when (and (bound-and-true-p doom-packages)
- (require 'package nil t))
- (msg! " + enabled packages:\n%s"
- (indented 4
- (columns 2 35
- (delq nil
- (mapcar (lambda (pkg)
- (let ((desc (cadr (assq pkg package-alist))))
- (when desc
- (package-desc-full-name desc))))
- (sort (mapcar #'car doom-packages) #'string-lessp)))))))
+ ;; ...and initialized
+ (doom-initialize)
+ (success! "Initialized Doom Emacs" doom-version)
- (msg! " + byte-compiled files:\n%s"
- (indented 4
- (columns 2 39
- (let ((files (append (directory-files-recursively doom-core-dir ".elc$")
- (directory-files-recursively doom-modules-dir ".elc$"))))
- (or (and files (mapcar (lambda (file) (file-relative-name file doom-emacs-dir))
- (nreverse files)))
- (list "n/a"))))))
+ (doom-initialize-modules)
+ (if (hash-table-p doom-modules)
+ (success! "Initialized %d modules" (hash-table-count doom-modules))
+ (warn! "Failed to load any modules. Do you have an private init.el?"))
- (msg! " + exec-path:\n%s"
- (indented 4
- (columns 1 79 exec-path)))
+ (doom-initialize-packages)
+ (success! "Initialized %d packages" (length doom-packages))
- (msg! " + PATH:\n%s"
- (indented 4
- (columns 1 79 (split-string (getenv "PATH") ":")))))
+ (section! "Checking Doom core for irregularities...")
+ (let ((indent (+ indent 2)))
+ (load (expand-file-name "doctor.el" doom-core-dir) nil 'nomessage))
+
+ (section! "Checking for stale elc files in your DOOMDIR...")
+ (when (file-directory-p doom-private-dir)
+ (let ((indent (+ indent 2)))
+ (elc-check-dir doom-private-dir)))
+
+ (when doom-modules
+ (section! "Checking your enabled modules...")
+ (let ((indent (+ indent 2)))
+ (advice-add #'require :around #'doom*shut-up)
+ (maphash
+ (lambda (key plist)
+ (let ((prefix (format "%s" (color 1 "(%s %s) " (car key) (cdr key)))))
+ (condition-case-unless-debug ex
+ (let ((doctor-file (doom-module-path (car key) (cdr key) "doctor.el"))
+ (packages-file (doom-module-path (car key) (cdr key) "packages.el")))
+ (cl-loop with doom--stage = 'packages
+ for name in (let (doom-packages
+ doom-disabled-packages)
+ (load packages-file 'noerror 'nomessage)
+ (mapcar #'car doom-packages))
+ unless (or (doom-package-prop name :disable)
+ (doom-package-prop name :ignore t)
+ (package-built-in-p name)
+ (package-installed-p name))
+ do (error! "%s is not installed" name))
+ (let ((doom--stage 'doctor))
+ (load doctor-file 'noerror 'nomessage)))
+ (file-missing (error! "%s" (error-message-string ex)))
+ (error (error! "Syntax error: %s" ex)))))
+ doom-modules)))))
+ (error
+ (warn! "Attempt to load DOOM failed\n %s\n"
+ (or (cdr-safe ex) (car ex)))
+ (setq doom-modules nil)))
;;
-(if (= doom-errors 0)
- (success! "Everything seems fine, happy Emacs'ing!")
- (message "\n----")
- (warn! "There were issues!")
- (unless doom-debug-mode
- (msg! "\nHopefully these can help you find problems. If not, run this doctor again with DEBUG=1:")
- (msg! "\n DEBUG=1 make doctor\n")
- (msg! "And file a bug report with its output at https://github.com/hlissner/.emacs.d/issues")))
+(message "\n")
+(dolist (msg (list (list doom-errors "error" 31)
+ (list doom-warnings "warning" 33)))
+ (when (> (car msg) 0)
+ (message (color (nth 2 msg) (if (= (car msg) 1) "There is %d %s!" "There are %d %ss!")
+ (car msg) (nth 1 msg)))))
+
+(when (and (zerop doom-errors)
+ (zerop doom-warnings))
+ (success! "Everything seems fine, happy Emacs'ing!"))
diff --git a/bin/doom.cmd b/bin/doom.cmd
new file mode 100644
index 000000000..132691e4b
--- /dev/null
+++ b/bin/doom.cmd
@@ -0,0 +1,25 @@
+:: Forward the ./doom script to Emacs
+
+@ECHO OFF
+SETLOCAL ENABLEDELAYEDEXPANSION
+
+PUSHD "%~dp0" >NUL
+
+SET args=
+SET command=%1
+
+:LOOP
+SHIFT /1
+IF NOT [%1]==[] (
+ SET args=%args% %1
+ GOTO :LOOP
+)
+
+IF [%command%]==[run] (
+ start runemacs -Q %args% -l ..\init.el -f "doom|run-all-startup-hooks"
+) ELSE (
+ emacs --quick --script .\doom -- %*
+)
+
+POPD >NUL
+ECHO ON
diff --git a/bin/org-capture b/bin/org-capture
index 7f2998481..d04d2e971 100755
--- a/bin/org-capture
+++ b/bin/org-capture
@@ -3,41 +3,40 @@
# Open an org-capture popup frame from the shell. This opens a temporary emacs
# daemon if emacs isn't already running.
#
-# Usage: org-capture [key] [message...]
+# Usage: org-capture [-k KEY] [MESSAGE]
# Examples:
-# org-capture n "To the mind that is still, the whole universe surrenders."
+# org-capture -k n "To the mind that is still, the whole universe surrenders."
set -e
cleanup() {
- emacsclient --eval '(kill-emacs)'
+ emacsclient --eval '(let (kill-emacs-hook) (kill-emacs))'
}
# If emacs isn't running, we start a temporary daemon, solely for this window.
-daemon=
-if ! pgrep emacs >/dev/null; then
- emacs --daemon
- trap cleanup EXIT INT TERM
- daemon=1
+if ! emacsclient -e nil; then
+ emacs --daemon
+ trap cleanup EXIT INT TERM
+ daemon=1
fi
# org-capture key mapped to argument flags
-keys=$(emacsclient -e "(+org-capture-available-keys)" | cut -d '"' -f2)
-while getopts $keys opt; do
- key="\"$opt\""
- break
+# keys=$(emacsclient -e "(+org-capture-available-keys)" | cut -d '"' -f2)
+while getopts hk opt; do
+ key="\"$opt\""
+ break
done
shift $((OPTIND-1))
[ -t 0 ] && str="$*" || str=$(cat)
if [[ $daemon ]]; then
- emacsclient -a "" \
- -c -F '((name . "org-capture") (width . 70) (height . 25))' \
- -e "(+org-capture/open-frame \"$str\" ${key:-nil})"
+ emacsclient -a "" \
+ -c -F '((name . "org-capture") (width . 70) (height . 25) (transient . t))' \
+ -e "(+org-capture/open-frame \"$str\" ${key:-nil})"
else
- # Non-daemon servers flicker a lot if frames are created from terminal, so
- # we do it internally instead.
- emacsclient -a "" \
- -e "(+org-capture/open-frame \"$str\" ${key:-nil})"
+ # Non-daemon servers flicker a lot if frames are created from terminal, so we
+ # do it internally instead.
+ emacsclient -a "" \
+ -e "(+org-capture/open-frame \"$str\" ${key:-nil})"
fi
diff --git a/bin/org-tangle b/bin/org-tangle
index fbe90fb23..ff99e6a2f 100755
--- a/bin/org-tangle
+++ b/bin/org-tangle
@@ -2,42 +2,142 @@
":"; exec emacs --quick --script "$0" -- "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
;;; bin/org-tangle
-;; Extracts source blocks from org files and prints them to stdout. Debug/info
-;; messages are directed to stderr and can be ignored. -l/--lang can be used to
-;; only tangle blocks of a certain language.
+;; Tangles source blocks from org files. Debug/info messages are directed to
+;; stderr and can be ignored.
+;;
+;; -l/--lang LANG
+;; Only include blocks in the specified language (e.g. emacs-lisp).
+;; -a/--all
+;; Tangle all blocks by default (unless it has :tangle nil set or a
+;; :notangle: tag)
+;; -t/--tag TAG
+;; --and TAG
+;; --or TAG
+;; Only include blocks in trees that have these tags. Combine multiple --and
+;; and --or's, or just use --tag (implicit --and).
+;; -p/--print
+;; Prints tangled code to stdout instead of to files
;;
;; Usage: org-tangle [[-l|--lang] LANG] some-file.org another.org
;; Examples:
-;; org-tangle modules/ui/doom/README.org > install_fira_mono.sh
+;; org-tangle -l sh modules/some/module/README.org > install_module.sh
;; org-tangle -l sh modules/lang/go/README.org | sh
+;; org-tangle --and tagA --and tagB my/literate/config.org
-(load (expand-file-name "../core/core.el" (file-name-directory load-file-name)) nil t)
-
-(require 'org-install)
-(require 'org)
+(require 'cl-lib)
(require 'ob-tangle)
+(defun usage ()
+ (with-temp-buffer
+ (insert (format "%s %s [OPTIONS] [TARGETS...]\n"
+ "[1mUsage:[0m"
+ (file-name-nondirectory load-file-name))
+ "\n"
+ "A command line interface for tangling org-mode files. TARGETS can be\n"
+ "files or folders (which are searched for org files recursively).\n"
+ "\n"
+ "This is useful for literate configs that rely on command line\n"
+ "workflows to build it.\n"
+ "\n"
+ "[1mExample:[0m\n"
+ " org-tangle some-file.org\n"
+ " org-tangle literate/config/\n"
+ " org-tangle -p -l sh scripts.org > do_something.sh\n"
+ " org-tangle -p -l python -t tagA -t tagB file.org | python\n"
+ "\n"
+ "[1mOptions:[0m\n"
+ " -a --all\t\tTangle all blocks by default\n"
+ " -l --lang LANG\tOnly tangle blocks written in LANG\n"
+ " -p --print\t\tPrint tangled output to stdout than to files\n"
+ " -t --tag TAG\n"
+ " --and TAG\n"
+ " --or TAG\n"
+ " Lets you tangle org blocks by tag. You may have more than one\n"
+ " of these options.\n")
+ (princ (buffer-string))))
+
(defun *org-babel-tangle (orig-fn &rest args)
"Don't write tangled blocks to files, print them to stdout."
(cl-letf (((symbol-function 'write-region)
(lambda (start end filename &optional append visit lockname mustbenew)
(princ (buffer-string)))))
(apply orig-fn args)))
-(advice-add #'org-babel-tangle :around #'*org-babel-tangle)
-(let (lang srcs)
+(defun *org-babel-tangle-collect-blocks (&optional language tangle-file)
+ "Like `org-babel-tangle-collect-blocks', but will ignore blocks that are in
+trees with the :notangle: tag."
+ (let ((counter 0) last-heading-pos blocks)
+ (org-babel-map-src-blocks (buffer-file-name)
+ (let ((current-heading-pos
+ (org-with-wide-buffer
+ (org-with-limited-levels (outline-previous-heading)))))
+ (if (eq last-heading-pos current-heading-pos) (cl-incf counter)
+ (setq counter 1)
+ (setq last-heading-pos current-heading-pos)))
+ (unless (org-in-commented-heading-p)
+ (require 'org)
+ (let* ((tags (org-get-tags-at))
+ (info (org-babel-get-src-block-info 'light))
+ (src-lang (nth 0 info))
+ (src-tfile (cdr (assq :tangle (nth 2 info)))))
+ (cond ((member "notangle" tags))
+
+ ((and (or or-tags and-tags)
+ (or (not and-tags)
+ (let ((a (cl-intersection and-tags tags :test #'string=))
+ (b and-tags))
+ (not (or (cl-set-difference a b :test #'equal)
+ (cl-set-difference b a :test #'equal)))))
+ (or (not or-tags)
+ (cl-intersection or-tags tags :test #'string=))
+ t))
+
+ ((or (not (or all-blocks src-tfile))
+ (string= src-tfile "no") ; tangle blocks by default
+ (and tangle-file (not (equal tangle-file src-tfile)))
+ (and language (not (string= language src-lang)))))
+
+ ;; Add the spec for this block to blocks under its language.
+ ((let ((by-lang (assoc src-lang blocks))
+ (block (org-babel-tangle-single-block counter)))
+ (if by-lang
+ (setcdr by-lang (cons block (cdr by-lang)))
+ (push (cons src-lang (list block)) blocks))))))))
+ ;; Ensure blocks are in the correct order.
+ (mapcar (lambda (b) (cons (car b) (nreverse (cdr b)))) blocks)))
+(advice-add #'org-babel-tangle-collect-blocks
+ :override #'*org-babel-tangle-collect-blocks)
+
+(defvar all-blocks nil)
+(defvar and-tags nil)
+(defvar or-tags nil)
+(let (lang srcs and-tags or-tags)
(pop argv)
(while argv
(let ((arg (pop argv)))
(pcase arg
- ((or "--lang" "-l")
+ ((or "-h" "--help")
+ (usage)
+ (error ""))
+ ((or "-a" "--all")
+ (setq all-blocks t))
+ ((or "-l" "--lang")
(setq lang (pop argv)))
+ ((or "-p" "--print")
+ (advice-add #'org-babel-tangle :around #'*org-babel-tangle))
+ ((or "-t" "--tag" "--and")
+ (push (pop argv) and-tags))
+ ("--or"
+ (push (pop argv) or-tags))
((guard (string-match-p "^--lang=" arg))
(setq lang (cadr (split-string arg "=" t t))))
+ ((guard (file-directory-p arg))
+ (setq srcs
+ (append (directory-files-recursively arg "\\.org$")
+ srcs)))
((guard (file-exists-p arg))
(push arg srcs))
- (_
- (error "Unknown option or file: %s" arg)))))
+ (_ (error "Unknown option or file: %s" arg)))))
(dolist (file srcs)
(org-babel-tangle-file file nil lang))
diff --git a/core/autoload/buffers.el b/core/autoload/buffers.el
index 83369cd4c..4c69ef253 100644
--- a/core/autoload/buffers.el
+++ b/core/autoload/buffers.el
@@ -1,77 +1,146 @@
;;; core/autoload/buffers.el -*- lexical-binding: t; -*-
-(defvar-local doom-buffer--narrowed-origin nil)
+;;;###autoload
+(defvar doom-real-buffer-functions
+ '(doom-dired-buffer-p)
+ "A list of predicate functions run to determine if a buffer is real, unlike
+`doom-unreal-buffer-functions'. They are passed one argument: the buffer to be
+tested.
+
+Should any of its function returns non-nil, the rest of the functions are
+ignored and the buffer is considered real.
+
+See `doom-real-buffer-p' for more information.")
;;;###autoload
-(defvar doom-real-buffer-functions '()
- "A list of predicate functions run to determine if a buffer is real. These
-functions are iterated over with one argument, the buffer in question. If any
-function returns non-nil, the procession stops and the buffer is qualified as
-real.")
+(defvar doom-unreal-buffer-functions
+ '(minibufferp doom-special-buffer-p doom-non-file-visiting-buffer-p)
+ "A list of predicate functions run to determine if a buffer is *not* real,
+unlike `doom-real-buffer-functions'. They are passed one argument: the buffer to
+be tested.
+
+Should any of these functions return non-nil, the rest of the functions are
+ignored and the buffer is considered unreal.
+
+See `doom-real-buffer-p' for more information.")
;;;###autoload
(defvar-local doom-real-buffer-p nil
- "If non-nil, this buffer should be considered real no matter what.")
+ "If non-nil, this buffer should be considered real no matter what. See
+`doom-real-buffer-p' for more information.")
;;;###autoload
-(defvar doom-fallback-buffer "*scratch*"
+(defvar doom-fallback-buffer-name "*scratch*"
"The name of the buffer to fall back to if no other buffers exist (will create
it if it doesn't exist).")
;;
;; Functions
-;;
+
+;;;###autoload
+(defun doom-buffer-frame-predicate (buf)
+ "To be used as the default frame buffer-predicate parameter. Returns nil if
+BUF should be skipped over by functions like `next-buffer' and `other-buffer'."
+ (or (doom-real-buffer-p buf)
+ (eq buf (doom-fallback-buffer))))
;;;###autoload
(defun doom-fallback-buffer ()
"Returns the fallback buffer, creating it if necessary. By default this is the
-scratch buffer."
- (get-buffer-create doom-fallback-buffer))
+scratch buffer. See `doom-fallback-buffer-name' to change this."
+ (let (buffer-list-update-hook)
+ (get-buffer-create doom-fallback-buffer-name)))
;;;###autoload
(defalias 'doom-buffer-list #'buffer-list)
;;;###autoload
-(defun doom-project-buffer-list ()
- "Return a list of buffers belonging to the current project.
+(defun doom-project-buffer-list (&optional project)
+ "Return a list of buffers belonging to the specified PROJECT.
+
+If PROJECT is nil, default to the current project.
If no project is active, return all buffers."
(let ((buffers (doom-buffer-list)))
- (if-let* ((project-root (if (doom-project-p) (doom-project-root))))
+ (if-let* ((project-root
+ (if project (expand-file-name project)
+ (doom-project-root))))
(cl-loop for buf in buffers
if (projectile-project-buffer-p buf project-root)
collect buf)
buffers)))
;;;###autoload
-(defun doom-real-buffer-list (&optional buffer-list)
- "Return a list of buffers that satify `doom-real-buffer-p'."
- (cl-loop for buf in (or buffer-list (doom-buffer-list))
- if (doom-real-buffer-p buf)
- collect buf))
+(defun doom-open-projects ()
+ "Return a list of projects with open buffers."
+ (cl-loop with projects = (make-hash-table :test 'equal :size 8)
+ for buffer in (doom-buffer-list)
+ if (buffer-live-p buffer)
+ if (doom-real-buffer-p buffer)
+ if (with-current-buffer buffer (doom-project-root))
+ do (puthash (abbreviate-file-name it) t projects)
+ finally return (hash-table-keys projects)))
;;;###autoload
-(defun doom-real-buffer-p (&optional buffer-or-name)
- "Returns t if BUFFER-OR-NAME is a 'real' buffer. The complete criteria for a
-real buffer is:
+(defun doom-dired-buffer-p (buf)
+ "Returns non-nil if BUF is a dired buffer."
+ (with-current-buffer buf (derived-mode-p 'dired-mode)))
- 1. The buffer-local value of `doom-real-buffer-p' (variable) is non-nil OR
- 2. Any function in `doom-real-buffer-functions' must return non-nil when
- passed this buffer OR
- 3. The current buffer:
- a) has a `buffer-file-name' defined AND
- b) is not in a popup window (see `doom-popup-p') AND
- c) is not a special buffer (its name isn't something like *Help*)
+;;;###autoload
+(defun doom-special-buffer-p (buf)
+ "Returns non-nil if BUF's name starts and ends with an *."
+ (equal (substring (buffer-name buf) 0 1) "*"))
+
+;;;###autoload
+(defun doom-temp-buffer-p (buf)
+ "Returns non-nil if BUF is temporary."
+ (equal (substring (buffer-name buf) 0 1) " "))
+
+;;;###autoload
+(defun doom-non-file-visiting-buffer-p (buf)
+ "Returns non-nil if BUF does not have a value for `buffer-file-name'."
+ (not (buffer-file-name buf)))
+
+;;;###autoload
+(defun doom-real-buffer-list (&optional buffer-list)
+ "Return a list of buffers that satify `doom-real-buffer-p'."
+ (cl-remove-if-not #'doom-real-buffer-p (or buffer-list (doom-buffer-list))))
+
+;;;###autoload
+(defun doom-real-buffer-p (buffer-or-name)
+ "Returns t if BUFFER-OR-NAME is a 'real' buffer.
+
+A real buffer is a useful buffer; a first class citizen in Doom. Real ones
+should get special treatment, because we will be spending most of our time in
+them. Unreal ones should be low-profile and easy to cast aside, so we can focus
+on real ones.
+
+The exact criteria for a real buffer is:
+
+ 1. A non-nil value for the buffer-local value of the `doom-real-buffer-p'
+ variable OR
+ 2. Any function in `doom-real-buffer-functions' returns non-nil OR
+ 3. None of the functions in `doom-unreal-buffer-functions' must return
+ non-nil.
If BUFFER-OR-NAME is omitted or nil, the current buffer is tested."
- (when-let* ((buf (ignore-errors (window-normalize-buffer buffer-or-name))))
- (or (buffer-local-value 'doom-real-buffer-p buf)
- (run-hook-with-args-until-success 'doom-real-buffer-functions buf)
- (not (or (doom-popup-p buf)
- (minibufferp buf)
- (string-match-p "^\\s-*\\*" (buffer-name buf))
- (not (buffer-file-name buf)))))))
+ (or (bufferp buffer-or-name)
+ (stringp buffer-or-name)
+ (signal 'wrong-type-argument (list '(bufferp stringp) buffer-or-name)))
+ (when-let* ((buf (get-buffer buffer-or-name)))
+ (and (buffer-live-p buf)
+ (not (doom-temp-buffer-p buf))
+ (or (buffer-local-value 'doom-real-buffer-p buf)
+ (run-hook-with-args-until-success 'doom-real-buffer-functions buf)
+ (not (run-hook-with-args-until-success 'doom-unreal-buffer-functions buf))))))
+
+;;;###autoload
+(defun doom-unreal-buffer-p (buffer-or-name)
+ "Return t if BUFFER-OR-NAME is an 'unreal' buffer.
+
+See `doom-real-buffer-p' for details on what that means."
+ (not (doom-real-buffer-p buffer-or-name)))
;;;###autoload
(defun doom-buffers-in-mode (modes &optional buffer-list derived-p)
@@ -89,10 +158,11 @@ If DERIVED-P, test with `derived-mode-p', otherwise use `eq'."
;;;###autoload
(defun doom-visible-windows (&optional window-list)
- "Return a list of the visible, non-popup windows."
- (cl-loop for win in (or window-list (window-list))
- unless (doom-popup-p win)
- collect win))
+ "Return a list of the visible, non-popup (dedicated) windows."
+ (cl-loop for window in (or window-list (window-list))
+ when (or (window-parameter window 'visible)
+ (not (window-dedicated-p window)))
+ collect window))
;;;###autoload
(defun doom-visible-buffers (&optional buffer-list)
@@ -104,9 +174,7 @@ If DERIVED-P, test with `derived-mode-p', otherwise use `eq'."
;;;###autoload
(defun doom-buried-buffers (&optional buffer-list)
"Get a list of buffers that are buried."
- (cl-loop for buf in (or buffer-list (doom-buffer-list))
- unless (get-buffer-window buf)
- collect buf))
+ (cl-remove-if #'get-buffer-window (or buffer-list (doom-buffer-list))))
;;;###autoload
(defun doom-matching-buffers (pattern &optional buffer-list)
@@ -115,71 +183,12 @@ If DERIVED-P, test with `derived-mode-p', otherwise use `eq'."
when (string-match-p pattern (buffer-name buf))
collect buf))
-(defun doom--cycle-real-buffers (&optional n)
- "Switch to the next buffer N times (previous, if N < 0), skipping over unreal
-buffers. If there's nothing left, switch to `doom-fallback-buffer'. See
-`doom-real-buffer-p' for what 'real' means."
- (let ((buffers (delq (current-buffer) (doom-real-buffer-list))))
- (cond ((or (not buffers)
- (zerop (% n (1+ (length buffers)))))
- (switch-to-buffer (doom-fallback-buffer) nil t))
- ((= (length buffers) 1)
- (switch-to-buffer (car buffers) nil t))
- (t
- ;; Why this instead of switching straight to the Nth buffer in
- ;; BUFFERS? Because `switch-to-next-buffer' and
- ;; `switch-to-prev-buffer' properly update buffer list order.
- (cl-loop with move-func =
- (if (> n 0) #'switch-to-next-buffer #'switch-to-prev-buffer)
- for i to 20
- while (not (memq (current-buffer) buffers))
- do
- (dotimes (_i (abs n))
- (funcall move-func)))))
- (force-mode-line-update)
- (current-buffer)))
-
;;;###autoload
(defun doom-set-buffer-real (buffer flag)
"Forcibly mark BUFFER as FLAG (non-nil = real)."
(with-current-buffer buffer
(setq doom-real-buffer-p flag)))
-;;;###autoload
-(defun doom-kill-buffer (&optional buffer dont-save)
- "Kill BUFFER (defaults to current buffer), but make sure we land on a real
-buffer. Bury the buffer if the buffer is present in another window.
-
-Will prompt to save unsaved buffers when attempting to kill them, unless
-DONT-SAVE is non-nil.
-
-See `doom-real-buffer-p' for what 'real' means."
- (unless buffer
- (setq buffer (current-buffer)))
- (when (and (bufferp buffer)
- (buffer-live-p buffer))
- (let ((buffer-win (get-buffer-window buffer)))
- ;; deal with modified buffers
- (when (and (buffer-file-name buffer)
- (buffer-modified-p buffer))
- (with-current-buffer buffer
- (if (and (not dont-save)
- (yes-or-no-p "Buffer is unsaved, save it?"))
- (save-buffer)
- (set-buffer-modified-p nil))))
- ;; kill the buffer (or close dedicated window)
- (cond ((not buffer-win)
- (kill-buffer buffer))
- ((window-dedicated-p buffer-win)
- (unless (window--delete buffer-win t t)
- (split-window buffer-win)
- (window--delete buffer-win t t)))
- (t ; cycle to a real buffer
- (with-selected-window buffer-win
- (doom--cycle-real-buffers -1)
- (kill-buffer buffer)))))
- (not (eq (current-buffer) buffer))))
-
;;;###autoload
(defun doom-kill-buffer-and-windows (buffer)
"Kill the buffer and delete all the windows it's displayed in."
@@ -188,54 +197,123 @@ See `doom-real-buffer-p' for what 'real' means."
(delete-window window)))
(kill-buffer buffer))
+;;;###autoload
+(defun doom-fixup-windows (windows)
+ "Ensure that each of WINDOWS is showing a real buffer or the fallback buffer."
+ (dolist (window windows)
+ (with-selected-window window
+ (when (doom-unreal-buffer-p (window-buffer))
+ (previous-buffer)
+ (when (doom-unreal-buffer-p (window-buffer))
+ (switch-to-buffer (doom-fallback-buffer)))))))
+
+;;;###autoload
+(defun doom-kill-buffer-fixup-windows (buffer)
+ "Kill the BUFFER and ensure all the windows it was displayed in have switched
+to a real buffer or the fallback buffer."
+ (let ((windows (get-buffer-window-list buffer)))
+ (kill-buffer buffer)
+ (doom-fixup-windows (cl-remove-if-not #'window-live-p windows))))
+
+;;;###autoload
+(defun doom-kill-buffers-fixup-windows (buffers)
+ "Kill the BUFFERS and ensure all the windows they were displayed in have
+switched to a real buffer or the fallback buffer."
+ (let ((seen-windows (make-hash-table :test 'eq :size 8)))
+ (dolist (buffer buffers)
+ (let ((windows (get-buffer-window-list buffer)))
+ (kill-buffer buffer)
+ (dolist (window (cl-remove-if-not #'window-live-p windows))
+ (puthash window t seen-windows))))
+ (doom-fixup-windows (hash-table-keys seen-windows))))
+
;;;###autoload
(defun doom-kill-matching-buffers (pattern &optional buffer-list)
"Kill all buffers (in current workspace OR in BUFFER-LIST) that match the
regex PATTERN. Returns the number of killed buffers."
(let ((buffers (doom-matching-buffers pattern buffer-list)))
(dolist (buf buffers (length buffers))
- (doom-kill-buffer buf t))))
+ (kill-buffer buf))))
+
+
+;;
+;; Hooks
+
+;;;###autoload
+(defun doom|mark-buffer-as-real ()
+ "Hook function that marks the current buffer as real."
+ (doom-set-buffer-real (current-buffer) t))
+
+
+;;
+;; Advice
+
+;;;###autoload
+(defun doom*switch-to-fallback-buffer-maybe (orig-fn)
+ "Advice for `kill-this-buffer'. If in a dedicated window, delete it. If there
+are no real buffers left OR if all remaining buffers are visible in other
+windows, switch to `doom-fallback-buffer'. Otherwise, delegate to original
+`kill-this-buffer'."
+ (let ((buf (current-buffer)))
+ (cond ((window-dedicated-p)
+ (delete-window))
+ ((eq buf (doom-fallback-buffer))
+ (message "Can't kill the fallback buffer."))
+ ((doom-real-buffer-p buf)
+ (if (and buffer-file-name
+ (buffer-modified-p buf)
+ (not (y-or-n-p
+ (format "Buffer %s is modified; kill anyway?" buf))))
+ (message "Aborted")
+ (set-buffer-modified-p nil)
+ (let (buffer-list-update-hook)
+ (when (or ;; if there aren't more real buffers than visible buffers,
+ ;; then there are no real, non-visible buffers left.
+ (not (cl-set-difference (doom-real-buffer-list)
+ (doom-visible-buffers)))
+ ;; if we end up back where we start (or previous-buffer
+ ;; returns nil), we have nowhere left to go
+ (memq (switch-to-prev-buffer nil t) (list buf 'nil)))
+ (switch-to-buffer (doom-fallback-buffer)))
+ (unless (delq (selected-window) (get-buffer-window-list buf nil t))
+ (kill-buffer buf)))))
+ ((funcall orig-fn)))))
;;
;; Interactive commands
-;;
-
-;;;###autoload
-(defun doom/kill-this-buffer (&optional interactive-p)
- "Use `doom-kill-buffer' on the current buffer."
- (interactive (list 'interactive))
- (when (and (not (doom-kill-buffer)) interactive-p)
- (message "Nowhere left to go!")))
;;;###autoload
(defun doom/kill-this-buffer-in-all-windows (buffer &optional dont-save)
"Kill BUFFER globally and ensure all windows previously showing this buffer
-have switched to a real buffer.
+have switched to a real buffer or the fallback buffer.
If DONT-SAVE, don't prompt to save modified buffers (discarding their changes)."
(interactive
(list (current-buffer) current-prefix-arg))
(cl-assert (bufferp buffer) t)
- (let ((windows (get-buffer-window-list buffer nil t)))
- (doom-kill-buffer buffer dont-save)
- (cl-loop for win in windows
- if (doom-real-buffer-p (window-buffer win))
- do (with-selected-window win (doom/previous-buffer)))))
+ (when (and (buffer-modified-p buffer) dont-save)
+ (with-current-buffer buffer
+ (set-buffer-modified-p nil)))
+ (doom-kill-buffer-fixup-windows buffer))
;;;###autoload
(defun doom/kill-all-buffers (&optional project-p)
"Kill all buffers and closes their windows.
-If PROJECT-P (universal argument), kill only buffers that belong to the current
-project."
+If PROJECT-P (universal argument), don't close windows and only kill buffers
+that belong to the current project."
(interactive "P")
- (doom/popup-kill-all)
+ (save-some-buffers)
+ (unless project-p
+ (delete-other-windows))
+ (switch-to-buffer (doom-fallback-buffer))
(let ((buffers (if project-p (doom-project-buffer-list) (doom-buffer-list))))
- (mapc #'doom-kill-buffer-and-windows buffers)
- (unless (doom-real-buffer-p)
- (switch-to-buffer (doom-fallback-buffer)))
- (message "Killed %s buffers" (length buffers))))
+ (mapc #'kill-buffer buffers)
+ (when (called-interactively-p 'interactive)
+ (message "Killed %s buffers"
+ (- (length buffers)
+ (length (cl-remove-if-not #'buffer-live-p buffers)))))))
;;;###autoload
(defun doom/kill-other-buffers (&optional project-p)
@@ -244,13 +322,14 @@ project."
If PROJECT-P (universal argument), kill only buffers that belong to the current
project."
(interactive "P")
- (let ((buffers (if project-p (doom-project-buffer-list) (doom-buffer-list)))
- (current-buffer (current-buffer)))
- (dolist (buf buffers)
- (unless (eq buf current-buffer)
- (doom-kill-buffer-and-windows buf)))
+ (let ((buffers
+ (delq (current-buffer)
+ (if project-p (doom-project-buffer-list) (doom-buffer-list)))))
+ (mapc #'doom-kill-buffer-and-windows buffers)
(when (called-interactively-p 'interactive)
- (message "Killed %s buffers" (length buffers)))))
+ (message "Killed %s buffers"
+ (- (length buffers)
+ (length (cl-remove-if-not #'buffer-live-p buffers)))))))
;;;###autoload
(defun doom/kill-matching-buffers (pattern &optional project-p)
@@ -261,52 +340,45 @@ project."
(interactive
(list (read-regexp "Buffer pattern: ")
current-prefix-arg))
- (let* ((buffers (if project-p (doom-project-buffer-list) (doom-buffer-list)))
- (n (doom-kill-matching-buffers pattern buffers)))
+ (let* ((buffers (if project-p (doom-project-buffer-list) (doom-buffer-list))))
+ (doom-kill-matching-buffers pattern buffers)
(when (called-interactively-p 'interactive)
- (message "Killed %s buffers" n))))
+ (message "Killed %d buffer(s)"
+ (- (length buffers)
+ (length (cl-remove-if-not #'buffer-live-p buffers)))))))
;;;###autoload
-(defun doom/cleanup-session (&optional all-p)
- "Clean up buried buries and orphaned processes in the current workspace. If
-ALL-P (universal argument), clean them up globally."
+(defun doom/kill-buried-buffers (&optional project-p)
+ "Kill buffers that are buried.
+
+If PROJECT-P (universal argument), only kill buried buffers belonging to the
+current project."
(interactive "P")
- (run-hooks 'doom-cleanup-hook)
- (let ((buffers (doom-buried-buffers (if all-p (buffer-list))))
- (n 0)
- kill-buffer-query-functions)
+ (let ((buffers (doom-buried-buffers (if project-p (doom-project-buffer-list)))))
(mapc #'kill-buffer buffers)
- (setq n (+ n (length buffers) (doom/cleanup-processes)))
(when (called-interactively-p 'interactive)
- (message "Cleaned up %s buffers" n))))
+ (message "Killed %d buffer(s)"
+ (- (length buffers)
+ (length (cl-remove-if-not #'buffer-live-p buffers)))))))
;;;###autoload
-(defun doom/cleanup-processes ()
- "Kill all processes that have no visible associated buffers. Return number of
-processes killed."
- (interactive)
- (let ((n 0))
- (dolist (p (process-list))
- (let ((process-buffer (process-buffer p)))
- (when (and (process-live-p p)
- (not (string= (process-name p) "server"))
- (or (not process-buffer)
- (and (bufferp process-buffer)
- (not (buffer-live-p process-buffer)))))
- (delete-process p)
- (cl-incf n))))
- n))
-
-;;;###autoload
-(defun doom/next-buffer ()
- "Switch to the next real buffer, skipping non-real buffers. See
-`doom-real-buffer-p' for what 'real' means."
- (interactive)
- (doom--cycle-real-buffers +1))
-
-;;;###autoload
-(defun doom/previous-buffer ()
- "Switch to the previous real buffer, skipping non-real buffers. See
-`doom-real-buffer-p' for what 'real' means."
- (interactive)
- (doom--cycle-real-buffers -1))
+(defun doom/kill-project-buffers (project)
+ "Kill buffers for the specified PROJECT."
+ (interactive
+ (list (if-let* ((open-projects (doom-open-projects)))
+ (completing-read
+ "Kill buffers for project: " open-projects
+ nil t nil nil
+ (if-let* ((project-root (doom-project-root))
+ (project-root (abbreviate-file-name project-root))
+ ((member project-root open-projects)))
+ project-root))
+ (message "No projects are open!")
+ nil)))
+ (when project
+ (let ((buffers (doom-project-buffer-list project)))
+ (doom-kill-buffers-fixup-windows buffers)
+ (when (called-interactively-p 'interactive)
+ (message "Killed %d buffer(s)"
+ (- (length buffers)
+ (length (cl-remove-if-not #'buffer-live-p buffers))))))))
diff --git a/core/autoload/cache.el b/core/autoload/cache.el
new file mode 100644
index 000000000..235f6243a
--- /dev/null
+++ b/core/autoload/cache.el
@@ -0,0 +1,95 @@
+;;; ../core/autoload/cache.el -*- lexical-binding: t; -*-
+
+;; This little library thinly wraps around persistent-soft (which is a pcache
+;; wrapper, how about that). It has three purposes:
+;;
+;; + To encapsulate the cache backend (persistent-soft/pcache in this case), in
+;; case it needs to change.
+;; + To provide `doom-cache-persist': a mechanism for easily persisting
+;; variables across Emacs sessions.
+;; + To lazy-load persistent-soft until it is really needed.
+;;
+;; Like persistent-soft, caches assume a 2-tier structure, where all caches are
+;; namespaced by location.
+
+(defvar doom-cache-alists '(t)
+ "An alist of alists, containing lists of variables for the doom cache library
+to persist across Emacs sessions.")
+
+(defvar doom-cache-location 'doom
+ "The default location for cache files. This symbol is translated into a file
+name under `pcache-directory' (by default a subdirectory under
+`doom-cache-dir'). One file may contain multiple cache entries.")
+
+(defun doom|save-persistent-cache ()
+ "Hook to run when an Emacs session is killed. Saves all persisted variables
+listed in `doom-cache-alists' to files."
+ (dolist (alist (butlast doom-cache-alists 1))
+ (cl-loop with key = (car alist)
+ for var in (cdr alist)
+ if (symbol-value var)
+ do (doom-cache-set var it nil key))))
+(add-hook 'kill-emacs-hook #'doom|save-persistent-cache)
+
+
+;;
+;; Library
+
+;;;###autoload
+(defmacro with-cache! (location &rest body)
+ "Runs BODY with a different default `doom-cache-location'."
+ (declare (indent defun))
+ `(let ((doom-cache-location ',location))
+ ,@body))
+
+;;;###autoload
+(defun doom-cache-persist (location variables)
+ "Persist VARIABLES (list of symbols) in LOCATION (symbol).
+
+This populates these variables with cached values, if one exists, and saves them
+to file when Emacs quits.
+
+Warning: this is incompatible with buffer-local variables."
+ (dolist (var variables)
+ (when (doom-cache-exists var location)
+ (set var (doom-cache-get var location))))
+ (setf (alist-get location doom-cache-alists)
+ (append variables (cdr (assq location doom-cache-alists)))))
+
+;;;###autoload
+(defun doom-cache-desist (location &optional variables)
+ "Unregisters VARIABLES (list of symbols) in LOCATION (symbol) from
+`doom-cache-alists', thus preventing them from being saved between sessions.
+Does not affect the actual variables themselves or their values."
+ (if variables
+ (setf (alist-get location doom-cache-alists)
+ (cl-set-difference (cdr (assq location doom-cache-alists))
+ variables))
+ (delq (assq location doom-cache-alists)
+ doom-cache-alists)))
+
+;;;###autoload
+(defun doom-cache-get (key &optional location)
+ "Retrieve KEY from LOCATION (defaults to `doom-cache-location'), if it exists
+and hasn't expired."
+ (persistent-soft-fetch
+ key (symbol-name (or location doom-cache-location))))
+
+;;;###autoload
+(defun doom-cache-set (key value &optional ttl location)
+ "Set KEY to VALUE in the cache. TTL is the time (in seconds) until this cache
+entry expires. LOCATION is the super-key to store this cache item under; the
+default is `doom-cache-location'. "
+ (persistent-soft-store
+ key value
+ (symbol-name (or location doom-cache-location)) ttl))
+
+;;;###autoload
+(defun doom-cache-exists (key &optional location)
+ "Returns t if KEY exists at LOCATION (defaults to `doom-cache-location')."
+ (persistent-soft-exists-p key (or location doom-cache-location)))
+
+;;;###autoload
+(defun doom-cache-clear (&optional location)
+ "Clear a cache LOCATION (defaults to `doom-cache-location')."
+ (persistent-soft-flush (or location doom-cache-location)))
diff --git a/core/autoload/cli.el b/core/autoload/cli.el
new file mode 100644
index 000000000..ec9481937
--- /dev/null
+++ b/core/autoload/cli.el
@@ -0,0 +1,68 @@
+;;; core/autoload/cli.el -*- lexical-binding: t; -*-
+
+(require 'core-cli)
+
+(defun doom--run (command &optional yes)
+ (let* ((default-directory doom-emacs-dir)
+ (doom-auto-accept yes)
+ (buf (get-buffer-create " *bin/doom*"))
+ (wconf (current-window-configuration))
+ (ignore-window-parameters t)
+ (noninteractive t)
+ (standard-output
+ (lambda (char)
+ (with-current-buffer buf
+ (insert char)
+ (when (memq char '(?\n ?\r))
+ (ansi-color-apply-on-region (line-beginning-position -1) (line-end-position))
+ (redisplay))))))
+ (doom-initialize t)
+ (setq doom-modules (doom-modules))
+ (doom-initialize-modules t)
+ (doom-initialize-packages t)
+ (with-current-buffer (switch-to-buffer buf)
+ (erase-buffer)
+ (require 'package)
+ (redisplay)
+ (doom-dispatch command nil)
+ (print! (green "\nDone!"))))
+ (message (format! (green "Done!"))))
+
+
+;;;###autoload
+(defun doom//autoloads (&optional yes)
+ "TODO"
+ (interactive "P")
+ (doom--run "autoloads" yes))
+
+;;;###autoload
+(defun doom//update (&optional yes)
+ "TODO"
+ (interactive "P")
+ (doom--run "update" yes))
+
+;;;###autoload
+(defun doom//upgrade (&optional yes)
+ "TODO"
+ (interactive "P")
+ (doom--run "upgrade" yes)
+ (when (y-or-n-p "You must restart Emacs for the upgrade to take effect. Restart?")
+ (doom/restart-and-restore)))
+
+;;;###autoload
+(defun doom//install (&optional yes)
+ "TODO"
+ (interactive "P")
+ (doom--run "install" yes))
+
+;;;###autoload
+(defun doom//autoremove (&optional yes)
+ "TODO"
+ (interactive "P")
+ (doom--run "autoremove" yes))
+
+;;;###autoload
+(defun doom//refresh (&optional yes)
+ "TODO"
+ (interactive "P")
+ (doom--run "refresh" yes))
diff --git a/core/autoload/config.el b/core/autoload/config.el
new file mode 100644
index 000000000..2c61d176e
--- /dev/null
+++ b/core/autoload/config.el
@@ -0,0 +1,83 @@
+;;; core/autoload/config.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defvar doom-reloading-p nil
+ "TODO")
+
+;;;###autoload
+(defun doom/open-private-config ()
+ "TODO"
+ (interactive)
+ (unless (file-directory-p doom-private-dir)
+ (make-directory doom-private-dir t))
+ (doom-project-browse doom-private-dir))
+
+;;;###autoload
+(defun doom/find-file-in-private-config ()
+ "TODO"
+ (interactive)
+ (doom-project-find-file doom-private-dir))
+
+;;;###autoload
+(defun doom/reload (&optional force-p)
+ "Reloads your private config.
+
+This is experimental! It will try to do as `bin/doom refresh' does, but from
+within this Emacs session. i.e. it reload autoloads files (if necessary),
+reloads your package list, and lastly, reloads your private config.el.
+
+Runs `doom-reload-hook' afterwards."
+ (interactive "P")
+ (require 'core-cli)
+ (let ((doom-reloading-p t))
+ (when (getenv "DOOMENV")
+ (doom-reload-env-file 'force))
+ (doom-reload-autoloads force-p)
+ (let (doom-init-p)
+ (doom-initialize))
+ (with-demoted-errors "PRIVATE CONFIG ERROR: %s"
+ (let (doom-init-modules-p)
+ (doom-initialize-modules)))
+ (when (bound-and-true-p doom-packages)
+ (doom/reload-packages))
+ (run-hook-wrapped 'doom-reload-hook #'doom-try-run-hook))
+ (message "Finished!"))
+
+;;;###autoload
+(defun doom/reload-env ()
+ "Regenerates and reloads your shell environment.
+
+Uses the same mechanism as 'bin/doom env reload'."
+ (interactive)
+ (compile (format "%s env refresh" (expand-file-name "bin/doom" doom-emacs-dir)))
+ (while compilation-in-progress
+ (sit-for 1))
+ (unless (file-readable-p doom-env-file)
+ (error "Failed to generate env file"))
+ (load-env-vars doom-env-file)
+ (setq-default
+ exec-path (append (split-string (getenv "PATH") ":")
+ (list exec-directory))
+ shell-file-name (or (getenv "SHELL")
+ shell-file-name)))
+
+;;;###autoload
+(defun doom/reload-font ()
+ "Reload `doom-font', `doom-variable-pitch-font', and `doom-unicode-font', if
+set."
+ (interactive)
+ (when doom-font
+ (set-frame-font doom-font t))
+ (doom|init-fonts))
+
+;;;###autoload
+(defun doom/reload-theme ()
+ "Reset the current color theme and fonts."
+ (interactive)
+ (let ((theme (or (car-safe custom-enabled-themes) doom-theme)))
+ (when theme
+ (mapc #'disable-theme custom-enabled-themes))
+ (when (and doom-theme (not (memq doom-theme custom-enabled-themes)))
+ (let (doom--prefer-theme-elc)
+ (load-theme doom-theme t)))
+ (doom|init-fonts)))
diff --git a/core/autoload/debug.el b/core/autoload/debug.el
index 9cd01271f..d830ad4c6 100644
--- a/core/autoload/debug.el
+++ b/core/autoload/debug.el
@@ -1,57 +1,90 @@
;;; core/autoload/debug.el -*- lexical-binding: t; -*-
-;;;###autoload
-(defun doom/what-face (&optional pos)
- "Shows all faces and overlay faces at point.
+(defun doom-template-insert (template)
+ "TODO"
+ (let ((file (expand-file-name (format "templates/%s" template) doom-core-dir)))
+ (when (file-exists-p file)
+ (insert-file-contents file))))
-Interactively prints the list to the echo area. Noninteractively, returns a list
-whose car is the list of faces and cadr is the list of overlay faces."
+;;;###autoload
+(defun doom-info ()
+ "Returns diagnostic information about the current Emacs session in markdown,
+ready to be pasted in a bug report on github."
+ (require 'vc-git)
+ (let ((default-directory doom-emacs-dir)
+ (doom-modules (doom-modules)))
+ (format
+ (concat "- OS: %s (%s)\n"
+ "- Emacs: %s (%s)\n"
+ "- Doom: %s (%s)\n"
+ "- Graphic display: %s (daemon: %s)\n"
+ "- System features: %s\n"
+ "- Details:\n"
+ " ```elisp\n"
+ " env bootstrapper: %s\n"
+ " elc count: %s\n"
+ " uname -a: %s\n"
+ " modules: %s\n"
+ " packages: %s\n"
+ " exec-path: %s\n"
+ " ```")
+ system-type system-configuration
+ emacs-version (format-time-string "%b %d, %Y" emacs-build-time)
+ doom-version
+ (or (string-trim (shell-command-to-string "git log -1 --format=\"%D %h %ci\""))
+ "n/a")
+ (display-graphic-p) (daemonp)
+ (bound-and-true-p system-configuration-features)
+ (cond ((file-exists-p doom-env-file) 'envvar-file)
+ ((featurep 'exec-path-from-shell) 'exec-path-from-shell))
+ ;; details
+ (length (doom-files-in `(,@doom-modules-dirs
+ ,doom-core-dir
+ ,doom-private-dir)
+ :type 'files :match "\\.elc$"))
+ (if IS-WINDOWS
+ "n/a"
+ (with-temp-buffer
+ (unless (zerop (call-process "uname" nil t nil "-msrv"))
+ (insert (format "%s" system-type)))
+ (string-trim (buffer-string))))
+ (or (cl-loop with cat = nil
+ for key being the hash-keys of doom-modules
+ if (or (not cat) (not (eq cat (car key))))
+ do (setq cat (car key))
+ and collect cat
+ and collect (cdr key)
+ else collect
+ (let ((flags (doom-module-get cat (cdr key) :flags)))
+ (if flags
+ `(,(cdr key) ,@flags)
+ (cdr key))))
+ "n/a")
+ (or (ignore-errors
+ (require 'use-package)
+ (cl-loop for (name . plist) in (doom-find-packages :private t)
+ if (use-package-plist-delete (copy-sequence plist) :modules)
+ collect (format "%s" (cons name it))
+ else
+ collect (symbol-name name)))
+ "n/a")
+ ;; abbreviate $HOME to hide username
+ (mapcar #'abbreviate-file-name exec-path))))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun doom/info ()
+ "Collects some debug information about your Emacs session, formats it into
+markdown and copies it to your clipboard, ready to be pasted into bug reports!"
(interactive)
- (let* ((pos (or pos (point)))
- (faces (let ((face (get-text-property pos 'face)))
- (if (keywordp (car-safe face))
- (list face)
- (cl-loop for f in (doom-enlist face) collect f))))
- (overlays (cl-loop for ov in (overlays-at pos (1+ pos))
- nconc (doom-enlist (overlay-get ov 'face)))))
- (cond ((called-interactively-p 'any)
- (message "%s %s\n%s %s"
- (propertize "Faces:" 'face 'font-lock-comment-face)
- (if faces
- (cl-loop for face in faces
- if (listp face)
- concat (format "'%s " face)
- else
- concat (concat (propertize (symbol-name face) 'face face) " "))
- "n/a ")
- (propertize "Overlays:" 'face 'font-lock-comment-face)
- (if overlays
- (cl-loop for ov in overlays
- concat (concat (propertize (symbol-name ov) 'face ov) " "))
- "n/a")))
- (t
- (and (or faces overlays)
- (list faces overlays))))))
-
-;;;###autoload
-(defun doom-active-minor-modes ()
- "Get a list of active minor-mode symbols."
- (cl-loop for mode in minor-mode-list
- unless (and (boundp mode) (symbol-value mode))
- collect mode))
-
-;;;###autoload
-(defun doom/what-minor-mode (mode)
- "Get information on an active minor mode. Use `describe-minor-mode' for a
-selection of all minor-modes, active or not."
- (interactive
- (list (completing-read "Minor mode: "
- (doom-active-minor-modes))))
- (describe-minor-mode-from-symbol
- (cl-typecase mode
- (string (intern mode))
- (symbol mode)
- (t (error "Expected a symbol/string, got a %s" (type-of mode))))))
+ (message "Generating Doom info...")
+ (if noninteractive
+ (print! (doom-info))
+ (kill-new (doom-info))
+ (message "Done! Copied to clipboard.")))
;;;###autoload
(defun doom/am-i-secure ()
@@ -68,8 +101,8 @@ selection of all minor-modes, active or not."
(url-retrieve-synchronously bad)
(error nil))
collect bad)))
- (error (format "tls seems to be misconfigured (it got %s)."
- bad-hosts))
+ (error "tls seems to be misconfigured (it got %s)."
+ bad-hosts)
(url-retrieve "https://badssl.com"
(lambda (status)
(if (or (not status) (plist-member status :error))
@@ -77,9 +110,216 @@ selection of all minor-modes, active or not."
(message "Your trust roots are set up properly.\n\n%s" (pp-to-string status))
t)))))
+;;;###autoload
+(defun doom/version ()
+ "Display the current version of Doom & Emacs, including the current Doom
+branch and commit."
+ (interactive)
+ (require 'vc-git)
+ (print! "Doom v%s (Emacs v%s)\nBranch: %s\nCommit: %s"
+ doom-version
+ emacs-version
+ (or (vc-git--symbolic-ref doom-core-dir)
+ "n/a")
+ (or (vc-git-working-revision doom-core-dir)
+ "n/a")))
+
+;;;###autoload
+(defun doom/copy-backtrace ()
+ "Copy the contents of the *Backtrace* window into your clipboard for easy
+pasting into a bug report or discord."
+ (interactive)
+ (if-let* ((buf (get-buffer "*Backtrace*")))
+ (with-current-buffer buf
+ (kill-new
+ (string-trim (buffer-string))))
+ (user-error "No backtrace buffer detected")))
+
+
+;;; Vanilla sandbox
+
+(defvar doom--sandbox-init-doom-p nil)
+
+(defun doom--run-vanilla-sandbox (&optional mode)
+ (interactive)
+ (let ((contents (buffer-string))
+ (file (make-temp-file "doom-sandbox-")))
+ (require 'package)
+ (with-temp-file file
+ (insert
+ (prin1-to-string
+ (macroexp-progn
+ (append `((setq debug-on-error t
+ package--init-file-ensured t
+ package-user-dir ,package-user-dir
+ package-archives ',package-archives
+ user-emacs-directory ,doom-emacs-dir
+ doom-modules ,doom-modules))
+ (pcase mode
+ (`vanilla-doom+ ; Doom core + modules - private config
+ `((setq doom-private-dir "/tmp/does/not/exist")
+ (load-file ,user-init-file)
+ (doom|run-all-startup-hooks)))
+ (`vanilla-doom ; only Doom core
+ `((setq doom-private-dir "/tmp/does/not/exist"
+ doom-init-modules-p t)
+ (load-file ,user-init-file)
+ (doom|run-all-startup-hooks)))
+ (`vanilla ; nothing loaded
+ `((package-initialize)))))))
+ "\n(unwind-protect (progn\n" contents "\n)\n"
+ (format "(delete-file %S))" file)))
+ (let ((args (if (eq mode 'doom)
+ (list "-l" file)
+ (list "-Q" "-l" file))))
+ (require 'restart-emacs)
+ (condition-case e
+ (cond ((display-graphic-p)
+ (if (memq system-type '(windows-nt ms-dos))
+ (restart-emacs--start-gui-on-windows args)
+ (restart-emacs--start-gui-using-sh args)))
+ ((memq system-type '(windows-nt ms-dos))
+ (user-error "Cannot start another Emacs from Windows shell."))
+ ((suspend-emacs
+ (format "%s %s -nw; fg"
+ (shell-quote-argument (restart-emacs--get-emacs-binary))
+ (string-join (mapcar #'shell-quote-argument args) " ")))))
+ (error
+ (delete-file file)
+ (signal (car e) (cdr e)))))))
+
+(fset 'doom--run-vanilla-emacs (lambda! (doom--run-vanilla-sandbox 'vanilla)))
+(fset 'doom--run-vanilla-doom (lambda! (doom--run-vanilla-sandbox 'vanilla-doom)))
+(fset 'doom--run-vanilla-doom+ (lambda! (doom--run-vanilla-sandbox 'vanilla-doom+)))
+(fset 'doom--run-full-doom (lambda! (doom--run-vanilla-sandbox 'doom)))
+
+(defvar doom-sandbox-emacs-lisp-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-c C-c") #'doom--run-vanilla-emacs)
+ (define-key map (kbd "C-c C-d") #'doom--run-vanilla-doom)
+ (define-key map (kbd "C-c C-p") #'doom--run-vanilla-doom+)
+ (define-key map (kbd "C-c C-f") #'doom--run-full-doom)
+ (define-key map (kbd "C-c C-k") #'kill-this-buffer)
+ map))
+
+(define-derived-mode doom-sandbox-emacs-lisp-mode emacs-lisp-mode "Sandbox Elisp"
+ "TODO")
+
+;;;###autoload
+(defun doom/open-vanilla-sandbox ()
+ "Open the Emacs Lisp sandbox.
+
+This is a test bed for running Emacs Lisp in an instance of Emacs with varying
+amounts of Doom loaded, including:
+
+ a) vanilla Emacs (nothing loaded),
+ b) vanilla Doom (only Doom core) and
+ c) Doom + modules - your private config.
+
+This is done without sacrificing access to installed packages. Use the sandbox
+to reproduce bugs and determine if Doom is to blame."
+ (interactive)
+ (let* ((buffer-name "*doom:vanilla-sandbox*")
+ (exists (get-buffer buffer-name))
+ (buf (get-buffer-create buffer-name)))
+ (with-current-buffer buf
+ (doom-sandbox-emacs-lisp-mode)
+ (setq header-line-format
+ (concat "C-c C-c = vanilla Emacs"
+ " / "
+ "C-c C-d = Doom core"
+ " / "
+ "C-c C-p = Doom core + modules - private config"
+ " / "
+ "C-c C-f = Full Doom"
+ " / "
+ "C-c C-k to abort"))
+ (setq-local default-directory doom-emacs-dir)
+ (unless (buffer-live-p exists)
+ (doom-template-insert "VANILLA_SANDBOX")
+ (let ((contents (substitute-command-keys (buffer-string))))
+ (erase-buffer)
+ (insert contents "\n")))
+ (goto-char (point-max)))
+ (pop-to-buffer buf)))
+
+
+;;; Reporting bugs
+
+(defun doom--open-bug-report ()
+ "TODO"
+ (interactive)
+ (let ((url "https://github.com/hlissner/doom-emacs/issues/new?body="))
+ ;; TODO Refactor me
+ (save-restriction
+ (widen)
+ (goto-char (point-min))
+ (re-search-forward "^-------------------------------------------------------------------\n" nil t)
+ (skip-chars-forward " \n\t")
+ (condition-case e
+ (progn
+ (save-excursion
+ (when (and (re-search-backward "\\+ [ ] " nil t)
+ (not (y-or-n-p "You haven't checked all the boxes. Continue anyway?")))
+ (error "Aborted submit")))
+ (narrow-to-region (point) (point-max))
+ (let ((text (buffer-string)))
+ ;; `url-encode-url' doesn't encode ampersands
+ (setq text (replace-regexp-in-string "&" "%26" text))
+ (setq url (url-encode-url (concat url text)))
+ ;; HACK: encode some characters according to HTML URL Encoding Reference
+ ;; http://www.w3schools.com/tags/ref_urlencode.asp
+ (setq url (replace-regexp-in-string "#" "%23" url))
+ (setq url (replace-regexp-in-string ";" "%3B" url))
+ (browse-url url)))
+ (error (signal (car e) (car e)))))))
+
+;;;###autoload
+(defun doom/open-bug-report ()
+ "Open a markdown buffer destinated to populate the New Issue page on Doom
+Emacs' issue tracker.
+
+If called when a backtrace buffer is present, it and the output of `doom-info'
+will be automatically appended to the result."
+ (interactive)
+ ;; TODO Refactor me
+ (let ((backtrace
+ (when (get-buffer "*Backtrace*")
+ (with-current-buffer "*Backtrace*"
+ (string-trim
+ (buffer-substring-no-properties
+ (point-min)
+ (min (point-max) 1000))))))
+ (buf (get-buffer-create "*doom:vanilla-sandbox*")))
+ (with-current-buffer buf
+ (erase-buffer)
+ (condition-case _ (gfm-mode)
+ (error (text-mode)))
+ (doom-template-insert "SUBMIT_BUG_REPORT")
+ (goto-char (point-max))
+ (let ((pos (point)))
+ (save-excursion
+ (insert
+ "\n" (doom-info) "\n"
+ (if (and backtrace (not (string-empty-p backtrace)))
+ (format "\n\nBacktrace
\n\n```\n%s\n```\n \n"
+ backtrace)
+ "")))
+ (local-set-key (kbd "C-c C-c") #'doom--open-bug-report)
+ (local-set-key (kbd "C-c C-k") #'kill-this-buffer)
+ (setq header-line-format "C-c C-c to submit / C-c C-k to close")
+ ;;
+ (narrow-to-region (point-min) pos)
+ (goto-char (point-min)))
+ (pop-to-buffer buf))))
+
+
+;;; Profiling
+
(defvar doom--profiler nil)
;;;###autoload
(defun doom/toggle-profiler ()
+ "Toggle the Emacs profiler. Run it again to see the profiling report."
(interactive)
(if (not doom--profiler)
(profiler-start 'cpu+mem)
@@ -88,13 +328,48 @@ selection of all minor-modes, active or not."
(setq doom--profiler (not doom--profiler)))
;;;###autoload
-(defun doom/info ()
- "Collects information about this session of Doom Emacs and copies it to the
-clipboard. Helpful when filing bug reports!"
+(defun doom/profile-emacs ()
+ "Profile the startup time of Emacs in the background with ESUP.
+If INIT-FILE is non-nil, profile that instead of USER-INIT-FILE."
(interactive)
- (with-temp-buffer
- (message "Producing information about your system...")
- (call-process (expand-file-name "bin/doom-doctor" doom-emacs-dir) nil t)
- (ansi-color-apply-on-region (point-min) (point-max))
- (kill-new (buffer-string))
- (message "Done. Copied to clipboard!")))
+ (require 'esup)
+ (let ((init-file esup-user-init-file))
+ (message "Starting esup...")
+ (esup-reset)
+ (setq esup-server-process (esup-server-create (esup-select-port)))
+ (setq esup-server-port (process-contact esup-server-process :service))
+ (message "esup process started on port %s" esup-server-port)
+ (let ((process-args
+ (append `("*esup-child*"
+ "*esup-child*"
+ ,esup-emacs-path
+ "-Q"
+ "--eval=(setq after-init-time nil)"
+ "-L" ,esup-load-path)
+ (when (bound-and-true-p early-init-file)
+ `("-l" ,early-init-file))
+ `("-l" "esup-child"
+ ,(format "--eval=(let ((load-file-name \"%s\")) (esup-child-run \"%s\" \"%s\" %d))"
+ init-file
+ init-file
+ esup-server-port
+ esup-depth)
+ "--eval=(doom|run-all-startup-hooks)"))))
+ (when esup-run-as-batch-p
+ (setq process-args (append process-args '("--batch"))))
+ (setq esup-child-process (apply #'start-process process-args)))
+ (set-process-sentinel esup-child-process 'esup-child-process-sentinel)))
+
+;;;###autoload
+(advice-add #'esup :override #'doom/profile-emacs)
+
+;;;###autoload
+(defun doom/toggle-debug-mode (&optional arg)
+ "Toggle `debug-on-error' and `doom-debug-mode' for verbose logging."
+ (interactive (list (or current-prefix-arg 'toggle)))
+ (let ((value
+ (cond ((eq arg 'toggle) (not doom-debug-mode))
+ ((> (prefix-numeric-value arg) 0)))))
+ (setq doom-debug-mode value
+ debug-on-error value)
+ (message "Debug mode %s" (if value "on" "off"))))
diff --git a/core/autoload/editor.el b/core/autoload/editor.el
deleted file mode 100644
index b98d53e75..000000000
--- a/core/autoload/editor.el
+++ /dev/null
@@ -1,246 +0,0 @@
-;;; core/autoload/editor.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun doom/sudo-find-file (file)
- "Open FILE as root."
- (interactive
- (list (read-file-name "Open as root: ")))
- (find-file (if (file-writable-p file)
- file
- (concat "/sudo:root@localhost:" file))))
-
-;;;###autoload
-(defun doom/sudo-this-file ()
- "Open the current file as root."
- (interactive)
- (doom/sudo-find-file (file-truename buffer-file-name)))
-
-;;;###autoload
-(defun doom/backward-to-bol-or-indent ()
- "Move back to the current line's indentation. If already there, move to the
-beginning of the line instead. If at bol, do nothing."
- (interactive)
- (if (bound-and-true-p visual-line-mode)
- (beginning-of-visual-line)
- (let ((ci (current-indentation))
- (cc (current-column)))
- (cond ((or (> cc ci) (= cc 0))
- (back-to-indentation))
- ((<= cc ci)
- (beginning-of-visual-line))))))
-
-;;;###autoload
-(defun doom/forward-to-last-non-comment-or-eol ()
- "Move forward to the last non-blank character in the line, ignoring comments
-and trailing whitespace. If already there, move to the real end of the line.
-If already there, do nothing."
- (interactive)
- (let* ((point (point))
- (eol (save-excursion (end-of-visual-line) (point)))
- (bol (save-excursion (beginning-of-visual-line) (point)))
- (eoc (or (if (not comment-use-syntax)
- (when (re-search-forward comment-start-skip eol t)
- (or (match-end 1) (match-beginning 0)))
- (save-excursion
- (goto-char eol)
- (while (and (sp-point-in-comment)
- (> (point) point))
- (backward-char))
- (when (> (point) point)
- (skip-chars-backward " " bol)
- (point))))
- eol))
- (goto-char-fn (if (featurep 'evil) #'evil-goto-char #'goto-char)))
- (if (= eoc point)
- (funcall goto-char-fn eol)
- (unless (= eol point)
- (funcall goto-char-fn eoc)))))
-
-(defun doom--surrounded-p ()
- (and (looking-back "[[{(]\\(\s+\\|\n\\)?\\(\s\\|\t\\)*" (line-beginning-position))
- (let* ((whitespace (match-string 1))
- (match-str (concat whitespace (match-string 2) "[])}]")))
- (looking-at-p match-str))))
-
-;;;###autoload
-(defun doom/dumb-indent ()
- "Inserts a tab character (or spaces x tab-width)."
- (interactive)
- (if indent-tabs-mode
- (insert "\t")
- (let* ((movement (% (current-column) tab-width))
- (spaces (if (= 0 movement) tab-width (- tab-width movement))))
- (insert (make-string spaces ? )))))
-
-;;;###autoload
-(defun doom/dumb-dedent ()
- "Dedents the current line."
- (interactive)
- (if indent-tabs-mode
- (call-interactively #'backward-delete-char)
- (unless (bolp)
- (save-excursion
- (when (> (current-column) (current-indentation))
- (back-to-indentation))
- (let ((movement (% (current-column) tab-width)))
- (delete-char
- (- (if (= 0 movement)
- tab-width
- (- tab-width movement)))))))))
-
-;;;###autoload
-(defun doom/backward-kill-to-bol-and-indent ()
- "Kill line to the first non-blank character. If invoked again
-afterwards, kill line to column 1."
- (interactive)
- (let ((empty-line-p (save-excursion (beginning-of-line)
- (looking-at-p "[ \t]*$"))))
- (funcall (if (featurep 'evil)
- #'evil-delete
- #'delete-region)
- (point-at-bol) (point))
- (unless empty-line-p
- (indent-according-to-mode))))
-
-;;;###autoload
-(defun doom/backward-delete-whitespace-to-column ()
- "Delete back to the previous column of whitespace, or as much whitespace as
-possible, or just one char if that's not possible."
- (interactive)
- (let* ((delete-backward-char (if (derived-mode-p 'org-mode)
- #'org-delete-backward-char
- #'delete-backward-char))
- (context (sp--get-pair-list-context 'navigate))
- (open-pair-re (sp--get-opening-regexp context))
- (close-pair-re (sp--get-closing-regexp context))
- open-len close-len)
- (cond ;; When in strings (sp acts weird with quotes; this is the fix)
- ;; Also, skip closing delimiters
- ((and (and (sp--looking-back open-pair-re)
- (setq open-len (- (match-beginning 0) (match-end 0))))
- (and (looking-at close-pair-re)
- (setq close-len (- (match-beginning 0) (match-end 0))))
- (string= (plist-get (sp-get-thing t) :op)
- (plist-get (sp-get-thing) :cl)))
- (delete-char (- 0 open-len))
- (delete-char close-len))
-
- ;; Delete up to the nearest tab column IF only whitespace between
- ;; point and bol.
- ((save-match-data (looking-back "^[\\t ]*" (line-beginning-position)))
- (let ((movement (% (current-column) tab-width))
- (p (point)))
- (when (= movement 0)
- (setq movement tab-width))
- (save-match-data
- (if (string-match "\\w*\\(\\s-+\\)$"
- (buffer-substring-no-properties (max (point-min) (- p movement)) p))
- (sp-delete-char
- (- 0 (- (match-end 1)
- (match-beginning 1))))
- (call-interactively delete-backward-char)))))
-
- ;; Otherwise do a regular delete
- (t (call-interactively delete-backward-char)))))
-
-;;;###autoload
-(defun doom/inflate-space-maybe ()
- "Checks if point is surrounded by {} [] () delimiters and adds a
-space on either side of the point if so."
- (interactive)
- (let ((command (or (command-remapping #'self-insert-command)
- #'self-insert-command)))
- (cond ((doom--surrounded-p)
- (call-interactively command)
- (save-excursion (call-interactively command)))
- (t
- (call-interactively command)))))
-
-;;;###autoload
-(defun doom/deflate-space-maybe ()
- "Checks if point is surrounded by {} [] () delimiters, and deletes
-spaces on either side of the point if so. Resorts to
-`doom/backward-delete-whitespace-to-column' otherwise."
- (interactive)
- (save-match-data
- (if (doom--surrounded-p)
- (let ((whitespace-match (match-string 1)))
- (cond ((not whitespace-match)
- (call-interactively #'delete-backward-char))
- ((string-match "\n" whitespace-match)
- (funcall (if (featurep 'evil)
- #'evil-delete
- #'delete-region)
- (point-at-bol) (point))
- (call-interactively #'delete-backward-char)
- (save-excursion (call-interactively #'delete-char)))
- (t (just-one-space 0))))
- (doom/backward-delete-whitespace-to-column))))
-
-;;;###autoload
-(defun doom/newline-and-indent ()
- "Inserts a newline and possibly indents it. Also continues comments if
-executed from a commented line; handling special cases for certain languages
-with weak native support."
- (interactive)
- (cond ((sp-point-in-string)
- (newline))
- ((sp-point-in-comment)
- (pcase major-mode
- ((or 'js2-mode 'rjsx-mode)
- (call-interactively #'js2-line-break))
- ((or 'java-mode 'php-mode)
- (c-indent-new-comment-line))
- ((or 'c-mode 'c++-mode 'objc-mode 'css-mode 'scss-mode 'js2-mode)
- (newline-and-indent)
- (insert "* ")
- (indent-according-to-mode))
- (_
- ;; Fix an off-by-one cursor-positioning issue
- ;; with `indent-new-comment-line'
- (let ((col (save-excursion (comment-beginning) (current-column))))
- (indent-new-comment-line)
- (unless (= col (current-column))
- (insert " "))))))
- (t
- (newline nil t)
- (indent-according-to-mode))))
-
-;;;###autoload
-(defun doom/retab (&optional beg end)
- "Changes all tabs to spaces or spaces to tabs, so that indentation is
-consistent throughout a selected region, depending on `indent-tab-mode'."
- (interactive "r")
- (unless (and beg end)
- (setq beg (point-min)
- end (point-max)))
- (if indent-tabs-mode
- (tabify beg end)
- (untabify beg end)))
-
-;;;###autoload
-(defun doom/narrow-buffer (beg end &optional clone-p)
- "Restrict editing in this buffer to the current region, indirectly. With CLONE-P,
-clone the buffer and hard-narrow the selection. If mark isn't active, then widen
-the buffer (if narrowed).
-
-Inspired from http://demonastery.org/2013/04/emacs-evil-narrow-region/"
- (interactive "r")
- (cond ((region-active-p)
- (deactivate-mark)
- (when clone-p
- (let ((old-buf (current-buffer)))
- (switch-to-buffer (clone-indirect-buffer nil nil))
- (setq doom-buffer--narrowed-origin old-buf)))
- (narrow-to-region beg end))
- (doom-buffer--narrowed-origin
- (kill-this-buffer)
- (switch-to-buffer doom-buffer--narrowed-origin)
- (setq doom-buffer--narrowed-origin nil))
- (t
- (widen))))
-
-;;;###autoload
-(defun doom|enable-delete-trailing-whitespace ()
- "Attaches `delete-trailing-whitespace' to a buffer-local `before-save-hook'."
- (add-hook 'before-save-hook #'delete-trailing-whitespace nil t))
diff --git a/core/autoload/files.el b/core/autoload/files.el
new file mode 100644
index 000000000..89a6d1128
--- /dev/null
+++ b/core/autoload/files.el
@@ -0,0 +1,210 @@
+;;; core/autoload/files.el -*- lexical-binding: t; -*-
+
+;;
+;; Public library
+
+;;;###autoload
+(cl-defun doom-files-in
+ (path-or-paths &rest rest
+ &key
+ filter
+ map
+ full
+ nosort
+ (follow-symlinks t)
+ (type 'files)
+ (relative-to (unless full default-directory))
+ (depth 99999)
+ (mindepth 0)
+ (match "/[^.]"))
+ "Returns a list of files/directories in PATH-OR-PATHS (one string path or a
+list of them).
+
+FILTER is a function or symbol that takes one argument (the path). If it returns
+non-nil, the entry will be excluded.
+
+MAP is a function or symbol which will be used to transform each entry in the
+results.
+
+TYPE determines what kind of path will be included in the results. This can be t
+(files and folders), 'files or 'dirs.
+
+By default, this function returns paths relative to PATH-OR-PATHS if it is a
+single path. If it a list of paths, this function returns absolute paths.
+Otherwise, by setting RELATIVE-TO to a path, the results will be transformed to
+be relative to it.
+
+The search recurses up to DEPTH and no further. DEPTH is an integer.
+
+MATCH is a string regexp. Only entries that match it will be included."
+ (cond
+ ((listp path-or-paths)
+ (cl-loop for path in path-or-paths
+ if (file-directory-p path)
+ nconc (apply #'doom-files-in path (plist-put rest :relative-to relative-to))))
+ ((let ((path path-or-paths)
+ result)
+ (when (file-directory-p path)
+ (dolist (file (directory-files path nil "." nosort))
+ (unless (member file '("." ".."))
+ (let ((fullpath (expand-file-name file path)))
+ (cond ((file-directory-p fullpath)
+ (when (and (memq type '(t dirs))
+ (string-match-p match fullpath)
+ (not (and filter (funcall filter fullpath)))
+ (not (and (file-symlink-p fullpath)
+ (not follow-symlinks)))
+ (<= mindepth 0))
+ (setq result
+ (nconc result
+ (list (cond (map (funcall map fullpath))
+ (relative-to (file-relative-name fullpath relative-to))
+ (fullpath))))))
+ (unless (< depth 1)
+ (setq result
+ (nconc result (apply #'doom-files-in fullpath
+ (append `(:mindepth ,(1- mindepth)
+ :depth ,(1- depth)
+ :relative-to ,relative-to)
+ rest))))))
+ ((and (memq type '(t files))
+ (string-match-p match fullpath)
+ (not (and filter (funcall filter fullpath)))
+ (<= mindepth 0))
+ (push (if relative-to
+ (file-relative-name fullpath relative-to)
+ fullpath)
+ result))))))
+ result)))))
+
+
+;;
+;; Helpers
+
+(defun doom--forget-file (old-path &optional new-path)
+ "Ensure `recentf', `projectile' and `save-place' forget OLD-PATH."
+ (when (bound-and-true-p recentf-mode)
+ (when new-path
+ (recentf-add-file new-path))
+ (recentf-remove-if-non-kept old-path))
+ (when (and (bound-and-true-p projectile-mode)
+ (doom-project-p)
+ (projectile-file-cached-p old-path (doom-project-root)))
+ (projectile-purge-file-from-cache old-path))
+ (when (bound-and-true-p save-place-mode)
+ (save-place-forget-unreadable-files)))
+
+(defun doom--update-file (path)
+ (when (featurep 'vc)
+ (vc-file-clearprops path)
+ (vc-resynch-buffer path nil t))
+ (when (featurep 'magit)
+ (magit-refresh)))
+
+(defun doom--copy-file (old-path new-path &optional force-p)
+ (let* ((new-path (expand-file-name new-path))
+ (old-path (file-truename old-path))
+ (new-path (apply #'expand-file-name
+ (if (or (directory-name-p new-path)
+ (file-directory-p new-path))
+ (list (file-name-nondirectory old-path) new-path)
+ (list new-path))))
+ (new-path-dir (file-name-directory new-path))
+ (project-root (doom-project-root))
+ (short-new-name (if (and project-root (file-in-directory-p new-path project-root))
+ (file-relative-name new-path project-root)
+ (abbreviate-file-name new-path))))
+ (unless (file-directory-p new-path-dir)
+ (make-directory new-path-dir t))
+ (when (buffer-modified-p)
+ (save-buffer))
+ (cond ((file-equal-p old-path new-path)
+ (throw 'status 'overwrite-self))
+ ((and (file-exists-p new-path)
+ (not force-p)
+ (not (y-or-n-p (format "File already exists at %s, overwrite?" short-new-name))))
+ (throw 'status 'aborted))
+ ((file-exists-p old-path)
+ (copy-file old-path new-path t)
+ short-new-name)
+ (short-new-name))))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun doom/delete-this-file (&optional path force-p)
+ "Delete FILENAME (defaults to the file associated with current buffer) and
+kills the buffer. If FORCE-P, force the deletion (don't ask for confirmation)."
+ (interactive
+ (list (file-truename (buffer-file-name))
+ current-prefix-arg))
+ (let* ((fbase (file-name-sans-extension (file-name-nondirectory path)))
+ (buf (current-buffer)))
+ (cond ((not (file-exists-p path))
+ (error "File doesn't exist: %s" path))
+ ((not (or force-p (y-or-n-p (format "Really delete %s?" fbase))))
+ (message "Aborted")
+ nil)
+ ((unwind-protect
+ (progn (delete-file path) t)
+ (let ((short-path (file-relative-name path (doom-project-root))))
+ (if (file-exists-p path)
+ (error "Failed to delete %s" short-path)
+ ;; Ensures that windows displaying this buffer will be switched
+ ;; to real buffers (`doom-real-buffer-p')
+ (doom/kill-this-buffer-in-all-windows buf t)
+ (doom--forget-file path)
+ (doom--update-file path)
+ (message "Successfully deleted %s" short-path))))))))
+
+;;;###autoload
+(defun doom/copy-this-file (new-path &optional force-p)
+ "Copy current buffer's file to NEW-PATH. If FORCE-P, overwrite the destination
+file if it exists, without confirmation."
+ (interactive "F")
+ (pcase (catch 'status
+ (when-let* ((dest (doom--copy-file (buffer-file-name) new-path force-p)))
+ (doom--update-file new-path)
+ (message "File successfully copied to %s" dest)))
+ (`overwrite-self (error "Cannot overwrite self"))
+ (`aborted (message "Aborted"))
+ (_ t)))
+
+;;;###autoload
+(defun doom/move-this-file (new-path &optional force-p)
+ "Move current buffer's file to NEW-PATH. If FORCE-P, overwrite the destination
+file if it exists, without confirmation."
+ (interactive "FP")
+ (pcase (catch 'status
+ (let ((old-path (buffer-file-name))
+ (new-path (expand-file-name new-path)))
+ (when-let* ((dest (doom--copy-file old-path new-path force-p)))
+ (when (file-exists-p old-path)
+ (delete-file old-path))
+ (kill-this-buffer)
+ (doom--forget-file old-path new-path)
+ (doom--update-file new-path)
+ (find-file new-path)
+ (message "File successfully moved to %s" dest))))
+ (`overwrite-self (error "Cannot overwrite self"))
+ (`aborted (message "Aborted"))
+ (_ t)))
+
+;;;###autoload
+(defun doom/sudo-find-file (file)
+ "Open FILE as root."
+ (interactive
+ (list (read-file-name "Open as root: ")))
+ (when (file-writable-p file)
+ (user-error "File is user writeable, aborting sudo"))
+ (find-file (if (file-remote-p file)
+ (concat "/" (file-remote-p file 'method) ":" (file-remote-p file 'user) "@" (file-remote-p file 'host) "|sudo:root@" (file-remote-p file 'host) ":" (file-remote-p file 'localname))
+ (concat "/sudo:root@localhost:" file))))
+
+;;;###autoload
+(defun doom/sudo-this-file ()
+ "Open the current file as root."
+ (interactive)
+ (doom/sudo-find-file (file-truename buffer-file-name)))
diff --git a/core/autoload/help.el b/core/autoload/help.el
index 52ee799b6..feba3d080 100644
--- a/core/autoload/help.el
+++ b/core/autoload/help.el
@@ -1,33 +1,337 @@
;;; core/autoload/help.el -*- lexical-binding: t; -*-
-;;;###autoload
-(defun doom/describe-setting (setting)
- "Open the documentation of SETTING (a keyword defined with `def-setting!')."
- (interactive
- ;; TODO try to read setting from whole line
- (list (completing-read "Describe setting%s: "
- (sort (mapcar #'car doom-settings) #'string-lessp)
- nil t nil nil)))
- (let ((fn (cdr (assq (intern setting) doom-settings))))
- (unless fn
- (error "'%s' is not a valid DOOM setting" setting))
- (describe-function fn)))
+(defvar doom--module-mode-alist
+ '((dockerfile-mode :tools docker)
+ (haxor-mode :lang assembly)
+ (mips-mode :lang assembly)
+ (nasm-mode :lang assembly)
+ (c-mode :lang cc)
+ (c++-mode :lang cc)
+ (objc++-mode :lang cc)
+ (crystal-mode :lang crystal)
+ (lisp-mode :lang common-lisp)
+ (csharp-mode :lang csharp)
+ (clojure-mode :lang clojure)
+ (graphql-mode :lang data)
+ (toml-mode :lang data)
+ (json-mode :lang data)
+ (yaml-mode :lang data)
+ (csv-mode :lang data)
+ (dhall-mode :lang data)
+ (erlang-mode :lang erlang)
+ (elixir-mode :lang elixir)
+ (elm-mode :lang elm)
+ (emacs-lisp-mode :lang emacs-lisp)
+ (ess-r-mode :lang ess)
+ (ess-julia-mode :lang ess)
+ (go-mode :lang go)
+ (haskell-mode :lang haskell)
+ (hy-mode :lang hy)
+ (java-mode :lang java)
+ (js2-mode :lang javascript)
+ (rjsx-mode :lang javascript)
+ (typescript-mode :lang javascript)
+ (coffee-mode :lang javascript)
+ (julia-mode :lang julia)
+ (latex-mode :lang latex)
+ (LaTeX-mode :lang latex)
+ (ledger-mode :lang ledger)
+ (lua-mode :lang lua)
+ (markdown-mode :lang markdown)
+ (gfm-mode :lang markdown)
+ (nim-mode :lang nim)
+ (nix-mode :lang nix)
+ (taureg-mode :lang ocaml)
+ (org-mode :lang org)
+ (perl-mode :lang perl)
+ (php-mode :lang php)
+ (hack-mode :lang php)
+ (plantuml-mode :lang plantuml)
+ (purescript-mode :lang purescript)
+ (python-mode :lang python)
+ (restclient-mode :lang rest)
+ (ruby-mode :lang ruby)
+ (enh-ruby-mode :lang ruby)
+ (rust-mode :lang rust)
+ (scala-mode :lang scala)
+ (sh-mode :lang sh)
+ (swift-mode :lang swift)
+ (web-mode :lang web)
+ (css-mode :lang web)
+ (scss-mode :lang web)
+ (sass-mode :lang web)
+ (less-css-mode :lang web)
+ (stylus-mode :lang web))
+ "TODO")
+
+
+;;
+;; Helpers
;;;###autoload
-(defun doom/describe-module (module)
- "Open the documentation of MODULE (a string that represents the category and
-submodule in the format, e.g. ':feature evil')."
+(defun doom-active-minor-modes ()
+ "Return a list of active minor-mode symbols."
+ (cl-loop for mode in minor-mode-list
+ if (and (boundp mode) (symbol-value mode))
+ collect mode))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun doom/describe-autodefs (autodef)
+ "Open the documentation of Doom autodefs.
+
+What is an autodef? It's a function or macro that is always defined, even if its
+containing module is disabled (in which case it will safely no-op). This
+syntactic sugar lets you use them without needing to check if they are
+available."
(interactive
- ;; TODO try to read module from whole line
- (list (completing-read "Describe module: "
- (cl-loop for (module . sub) in (reverse (hash-table-keys doom-modules))
- collect (format "%s %s" module sub))
- nil t)))
- (cl-destructuring-bind (category submodule)
- (mapcar #'intern (split-string module " "))
- (unless (member (cons category submodule) (doom-module-pairs))
- (error "'%s' isn't a valid module" module))
- (let ((doc-path (expand-file-name "README.org" (doom-module-path category submodule))))
- (unless (file-exists-p doc-path)
- (error "There is no documentation for this module"))
- (find-file doc-path))))
+ (let* ((settings
+ (cl-loop with case-fold-search = nil
+ for sym being the symbols of obarray
+ for sym-name = (symbol-name sym)
+ if (and (or (functionp sym)
+ (macrop sym))
+ (string-match-p "[a-z]!$" sym-name))
+ collect sym))
+ (sym (symbol-at-point))
+ (autodef
+ (completing-read
+ "Describe setter: "
+ ;; TODO Could be cleaner (refactor me!)
+ (cl-loop with maxwidth = (apply #'max (mapcar #'length (mapcar #'symbol-name settings)))
+ for def in (sort settings #'string-lessp)
+ if (get def 'doom-module)
+ collect
+ (format (format "%%-%ds%%s" (+ maxwidth 4))
+ def (propertize (format "%s %s" (car it) (cdr it))
+ 'face 'font-lock-comment-face))
+ else if (and (string-match-p "^set-.+!$" (symbol-name def))
+ (symbol-file def)
+ (file-in-directory-p (symbol-file def) doom-core-dir))
+ collect
+ (format (format "%%-%ds%%s" (+ maxwidth 4))
+ def (propertize (format "core/%s.el" (file-name-sans-extension (file-relative-name (symbol-file def) doom-core-dir)))
+ 'face 'font-lock-comment-face)))
+ nil t
+ (when (and (symbolp sym)
+ (string-match-p "!$" (symbol-name sym)))
+ (symbol-name sym)))))
+ (list (and autodef (car (split-string autodef " "))))))
+ (or (stringp autodef)
+ (functionp autodef)
+ (signal 'wrong-type-argument (list '(stringp functionp) autodef)))
+ (let ((fn (if (functionp autodef)
+ autodef
+ (intern-soft autodef))))
+ (or (fboundp fn)
+ (error "'%s' is not a valid DOOM autodef" autodef))
+ (if (fboundp 'helpful-callable)
+ (helpful-callable fn)
+ (describe-function fn))))
+
+;;;###autoload
+(defun doom/describe-active-minor-mode (mode)
+ "Get information on an active minor mode. Use `describe-minor-mode' for a
+selection of all minor-modes, active or not."
+ (interactive
+ (list (completing-read "Minor mode: " (doom-active-minor-modes))))
+ (describe-minor-mode-from-symbol
+ (cond ((stringp mode) (intern mode))
+ ((symbolp mode) mode)
+ ((error "Expected a symbol/string, got a %s" (type-of mode))))))
+
+;;;###autoload
+(defun doom/describe-module (category module)
+ "Open the documentation of CATEGORY MODULE.
+
+CATEGORY is a keyword and MODULE is a symbol. e.g. :feature and 'evil.
+
+Automatically selects a) the module at point (in private init files), b) the
+module derived from a `featurep!' or `require!' call, c) the module that the
+current file is in, or d) the module associated with the current major mode (see
+`doom--module-mode-alist')."
+ (interactive
+ (let* ((module
+ (cond ((and buffer-file-name
+ (eq major-mode 'emacs-lisp-mode)
+ (file-in-directory-p buffer-file-name doom-private-dir)
+ (save-excursion (goto-char (point-min))
+ (re-search-forward "^\\s-*(doom! " nil t))
+ (thing-at-point 'sexp t)))
+ ((save-excursion
+ (require 'smartparens)
+ (ignore-errors
+ (sp-beginning-of-sexp)
+ (unless (eq (char-after) ?\()
+ (backward-char))
+ (let ((sexp (sexp-at-point)))
+ (when (memq (car-safe sexp) '(featurep! require!))
+ (format "%s %s" (nth 1 sexp) (nth 2 sexp)))))))
+ ((and buffer-file-name
+ (when-let* ((mod (doom-module-from-path buffer-file-name)))
+ (format "%s %s" (car mod) (cdr mod)))))
+ ((when-let* ((mod (cdr (assq major-mode doom--module-mode-alist))))
+ (format "%s %s"
+ (symbol-name (car mod))
+ (symbol-name (cadr mod)))))))
+ (module-string
+ (completing-read
+ "Describe module: "
+ (cl-loop for path in (doom-module-load-path 'all)
+ for (cat . mod) = (doom-module-from-path path)
+ for format = (format "%s %s" cat mod)
+ if (doom-module-p cat mod)
+ collect format
+ else
+ collect (propertize format 'face 'font-lock-comment-face))
+ nil t nil nil module))
+ (key (split-string module-string " ")))
+ (list (intern (car key))
+ (intern (cadr key)))))
+ (cl-check-type category symbol)
+ (cl-check-type module symbol)
+ (let ((path (doom-module-locate-path category module)))
+ (unless (file-readable-p path)
+ (error "'%s %s' isn't a valid module; it doesn't exist" category module))
+ (if-let* ((readme-path (doom-module-locate-path category module "README.org")))
+ (find-file readme-path)
+ (if (y-or-n-p (format "The '%s %s' module has no README file. Explore its directory?"
+ category module))
+ (doom-project-browse path)
+ (user-error "Aborted module lookup")))))
+
+(defun doom--describe-package-insert-button (label path &optional regexp)
+ (declare (indent defun))
+ (insert-text-button
+ (string-trim label)
+ 'face 'link
+ 'follow-link t
+ 'action
+ `(lambda (_)
+ (unless (file-exists-p ,path)
+ (user-error "Module doesn't exist"))
+ (when (window-dedicated-p)
+ (other-window 1))
+ (let ((buffer (find-file ,path)))
+ (when ,(stringp regexp)
+ (with-current-buffer buffer
+ (goto-char (point-min))
+ (if (re-search-forward ,regexp nil t)
+ (recenter)
+ (message "Couldn't find the config block"))))))))
+
+;;;###autoload
+(global-set-key [remap describe-package] #'doom/describe-package)
+
+(defvar doom--describe-package-list-cache nil)
+;;;###autoload
+(defun doom/describe-package (package)
+ "Like `describe-packages', but is Doom aware.
+
+Only shows installed packages. Includes information about where packages are
+defined and configured.
+
+If prefix arg is prsent, refresh the cache."
+ (interactive
+ (list
+ (let* ((guess (or (function-called-at-point)
+ (symbol-at-point))))
+ (require 'finder-inf nil t)
+ (require 'core-packages)
+ (doom-initialize-packages)
+ (let ((packages
+ (or (unless current-prefix-arg doom--describe-package-list-cache)
+ (cl-loop for pkg
+ in (cl-delete-duplicates
+ (sort (append (mapcar #'car package-alist)
+ (mapcar #'car package-archive-contents)
+ (mapcar #'car package--builtins))
+ #'string-greaterp))
+ if (assq pkg package-alist)
+ collect (symbol-name pkg)
+ else
+ collect (propertize (symbol-name pkg) 'face 'font-lock-comment-face)))))
+ (unless (memq guess packages)
+ (setq guess nil))
+ (setq doom--describe-package-list-cache packages)
+ (intern
+ (completing-read
+ (if guess
+ (format "Describe package (default %s): "
+ guess)
+ "Describe package: ")
+ packages nil t nil nil
+ (if guess (symbol-name guess))))))))
+ (describe-package package)
+ (save-excursion
+ (with-current-buffer (help-buffer)
+ (let ((inhibit-read-only t))
+ (goto-char (point-min))
+ (when (and (doom-package-installed-p package)
+ (re-search-forward "^ *Status: " nil t))
+ (end-of-line)
+ (let ((indent (make-string (length (match-string 0)) ? )))
+ (insert "\n" indent "Installed by the following Doom modules:\n")
+ (dolist (m (get package 'doom-module))
+ (insert indent)
+ (doom--describe-package-insert-button
+ (format " %s %s" (car m) (or (cdr m) ""))
+ (pcase (car m)
+ (:core doom-core-dir)
+ (:private doom-private-dir)
+ (category (doom-module-path category (cdr m)))))
+ (insert "\n"))
+
+ (package--print-help-section "Source")
+ (pcase (doom-package-backend package)
+ (`elpa (insert "[M]ELPA"))
+ (`quelpa (insert (format "QUELPA %s" (prin1-to-string (doom-package-prop package :recipe)))))
+ (`emacs (insert "Built-in")))
+ (insert "\n")
+
+ (package--print-help-section "Configs")
+ (dolist (file (get package 'doom-files))
+ (doom--describe-package-insert-button
+ (abbreviate-file-name file)
+ file
+ (format "\\((\\(:?after!\\|def-package!\\)[ \t\n]*%s\\|^[ \t]*;; `%s'$\\)"
+ package package))
+ (insert "\n" indent))
+ (delete-char -1)))))))
+
+;;;###autoload
+(defun doom/describe-symbol (symbol)
+ "Show help for SYMBOL, a variable, function or macro."
+ (interactive
+ (list (helpful--read-symbol "Symbol: " #'helpful--bound-p)))
+ (let* ((sym (intern-soft symbol))
+ (bound (boundp sym))
+ (fbound (fboundp sym)))
+ (cond ((and sym bound (not fbound))
+ (helpful-variable sym))
+ ((and sym fbound (not bound))
+ (helpful-callable sym))
+ ((apropos (format "^%s\$" symbol)))
+ ((apropos (format "%s" symbol))))))
+
+;;;###autoload
+(defalias 'doom/help 'doom/open-manual)
+
+;;;###autoload
+(defun doom/open-manual ()
+ "TODO"
+ (interactive)
+ (user-error "This command isn't implemented yet")
+ ;; (find-file (expand-file-name "index.org" doom-docs-dir))
+ )
+
+;;;###autoload
+(defun doom/open-news ()
+ "TODO"
+ (interactive)
+ (user-error "This command isn't implemented yet")
+ ;; (find-file (expand-file-name (concat "news/" doom-version) doom-docs-dir))
+ )
diff --git a/core/autoload/hydras.el b/core/autoload/hydras.el
new file mode 100644
index 000000000..407289148
--- /dev/null
+++ b/core/autoload/hydras.el
@@ -0,0 +1,47 @@
+;;; core/autoload/hydras.el -*- lexical-binding: t; -*-
+
+;;;###autoload (autoload 'doom-text-zoom-hydra/body "core/autoload/hydras" nil t)
+(defhydra doom-text-zoom-hydra (:hint t :color red)
+ "
+ Text zoom: _j_:zoom in, _k_:zoom out, _0_:reset
+"
+ ("j" text-scale-increase "in")
+ ("k" text-scale-decrease "out")
+ ("0" (text-scale-set 0) "reset"))
+
+;;;###autoload (autoload 'doom-window-nav-hydra/body "core/autoload/hydras" nil t)
+(defhydra doom-window-nav-hydra (:hint nil)
+ "
+ Split: _v_ert _s_:horz
+ Delete: _c_lose _o_nly
+ Switch Window: _h_:left _j_:down _k_:up _l_:right
+ Buffers: _p_revious _n_ext _b_:select _f_ind-file
+ Resize: _H_:splitter left _J_:splitter down _K_:splitter up _L_:splitter right
+ Move: _a_:up _z_:down _i_menu
+"
+ ("z" scroll-up-line)
+ ("a" scroll-down-line)
+ ("i" idomenu)
+
+ ("h" windmove-left)
+ ("j" windmove-down)
+ ("k" windmove-up)
+ ("l" windmove-right)
+
+ ("p" previous-buffer)
+ ("n" next-buffer)
+ ("b" switch-to-buffer)
+ ("f" find-file)
+
+ ("s" split-window-below)
+ ("v" split-window-right)
+
+ ("c" delete-window)
+ ("o" delete-other-windows)
+
+ ("H" hydra-move-splitter-left)
+ ("J" hydra-move-splitter-down)
+ ("K" hydra-move-splitter-up)
+ ("L" hydra-move-splitter-right)
+
+ ("q" nil))
diff --git a/core/autoload/line-numbers.el b/core/autoload/line-numbers.el
new file mode 100644
index 000000000..a926c0a2b
--- /dev/null
+++ b/core/autoload/line-numbers.el
@@ -0,0 +1,92 @@
+;;; core/autoload/line-numbers.el -*- lexical-binding: t; -*-
+;;;###if (version< emacs-version "26.1")
+
+;; This was lifted out of the display-line-numbers library in Emacs 26.1 and
+;; modified to use nlinum for Emacs 25.x users. It should be removed should
+;; Emacs 25 support be removed.
+
+;;;###autoload
+(defvar display-line-numbers t
+ "Non-nil means display line numbers.
+
+If the value is t, display the absolute number of each line of a buffer
+shown in a window. Absolute line numbers count from the beginning of
+the current narrowing, or from buffer beginning. If the value is
+relative, display for each line not containing the window's point its
+relative number instead, i.e. the number of the line relative to the
+line showing the window's point.
+
+In either case, line numbers are displayed at the beginning of each
+non-continuation line that displays buffer text, i.e. after each newline
+character that comes from the buffer. The value visual is like
+relative but counts screen lines instead of buffer lines. In practice
+this means that continuation lines count as well when calculating the
+relative number of a line.
+
+Lisp programs can disable display of a line number of a particular
+buffer line by putting the display-line-numbers-disable text property
+or overlay property on the first visible character of that line.")
+
+(defgroup display-line-numbers nil "Display line number preferences"
+ :group 'emacs)
+
+;;;###autoload
+(defcustom display-line-numbers-type t
+ "The default type of line numbers to use in `display-line-numbers-mode'.
+See `display-line-numbers' for value options."
+ :type '(choice (const :tag "Relative line numbers" relative)
+ (const :tag "Relative visual line numbers" visual)
+ (other :tag "Absolute line numbers" t)))
+
+;;;###autoload
+(defcustom display-line-numbers-grow-only nil
+ "If non-nil, do not shrink line number width."
+ :type 'boolean)
+
+;;;###autoload
+(defcustom display-line-numbers-width-start nil
+ "If non-nil, count number of lines to use for line number width.
+When `display-line-numbers-mode' is turned on,
+`display-line-numbers-width' is set to the minimum width necessary
+to display all line numbers in the buffer."
+ :type 'boolean)
+
+;;;###autoload
+(defun line-number-display-width ()
+ "Return the width used for displaying line numbers in the
+selected window."
+ (length (save-excursion (goto-char (point-max))
+ (format-mode-line "%l"))))
+
+(defun display-line-numbers-update-width ()
+ "Prevent the line number width from shrinking."
+ (let ((width (line-number-display-width)))
+ (when (> width (or display-line-numbers-width 1))
+ (setq display-line-numbers-width width))))
+
+;;;###autoload
+(define-minor-mode display-line-numbers-mode
+ "Toggle display of line numbers in the buffer.
+This uses `display-line-numbers' internally.
+
+To change the type of line numbers displayed by default,
+customize `display-line-numbers-type'. To change the type while
+the mode is on, set `display-line-numbers' directly."
+ :lighter nil
+ (cond ((null display-line-numbers-type))
+ ((eq display-line-numbers-type 'relative)
+ (if display-line-numbers-mode
+ (nlinum-relative-off)
+ (nlinum-relative-on)))
+ ((nlinum-mode (if display-line-numbers-mode +1 -1)))))
+
+(defun display-line-numbers--turn-on ()
+ "Turn on `display-line-numbers-mode'."
+ (unless (or (minibufferp)
+ ;; taken from linum.el
+ (and (daemonp) (null (frame-parameter nil 'client))))
+ (display-line-numbers-mode)))
+
+;;;###autoload
+(define-globalized-minor-mode global-display-line-numbers-mode
+ display-line-numbers-mode display-line-numbers--turn-on)
diff --git a/core/autoload/memoize.el b/core/autoload/memoize.el
deleted file mode 100644
index 8d82c3d77..000000000
--- a/core/autoload/memoize.el
+++ /dev/null
@@ -1,31 +0,0 @@
-;;; core/autoload/memoize.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defvar doom-memoized-table (make-hash-table :test 'equal :size 10)
- "A lookup table containing memoized functions. The keys are argument lists,
-and the value is the function's return value.")
-
-;;;###autoload
-(defun doom-memoize (name)
- "Memoizes an existing function. NAME is a symbol."
- (let ((func (symbol-function name)))
- (put name 'function-documentation
- (concat (documentation func) " (memoized)"))
- (fset name
- `(lambda (&rest args)
- (let ((key (cons ',name args)))
- (or (gethash key doom-memoized-table)
- (puthash key (apply ',func args)
- doom-memoized-table)))))))
-
-;;;###autoload
-(defmacro def-memoized! (name arglist &rest body)
- "Create a memoize'd function. NAME, ARGLIST, DOCSTRING and BODY
-have the same meaning as in `defun'."
- (declare (indent defun) (doc-string 3))
- `(,(if (bound-and-true-p byte-compile-current-file)
- 'with-no-warnings
- 'progn)
- (defun ,name ,arglist ,@body)
- (doom-memoize ',name)))
-
diff --git a/core/autoload/menu.el b/core/autoload/menu.el
deleted file mode 100644
index c0ad798f2..000000000
--- a/core/autoload/menu.el
+++ /dev/null
@@ -1,104 +0,0 @@
-;;; ../core/autoload/menu.el -*- lexical-binding: t; -*-
-
-;; Command dispatchers: basically M-x, but context sensitive, customizable and
-;; persistent across Emacs sessions.
-
-(defvar doom-menu-display-fn #'doom-menu-read-default
- "The method to use to prompt the user with the menu. This takes two arguments:
-PROMPT (a string) and COMMAND (a list of command plists; see `def-menu!').")
-
-(defun doom-menu-read-default (prompt commands)
- "Default method for displaying a completion-select prompt."
- (completing-read prompt (mapcar #'car commands)))
-
-(defun doom--menu-read (prompt commands)
- (if-let* ((choice (funcall doom-menu-display-fn prompt commands)))
- (cdr (assoc choice commands))
- (user-error "Aborted")))
-
-(defun doom--menu-exec (plist)
- (let ((command (plist-get plist :exec))
- (cwd (plist-get plist :cwd)))
- (let ((default-directory
- (cond ((eq cwd t) (doom-project-root))
- ((stringp cwd) cwd)
- (t default-directory))))
- (cond ((stringp command)
- (with-current-buffer (get-buffer-create "*compilation*")
- (setq command (doom-resolve-vim-path command))
- (save-window-excursion
- (compile command))
- (setq header-line-format
- (concat (propertize "$ " 'face 'font-lock-doc-face)
- (propertize command 'face 'font-lock-preprocessor-face)))
- (doom-resize-window
- (doom-popup-buffer (current-buffer)
- '(:autokill t :autoclose t)) 12)))
- ((or (symbolp command)
- (functionp command))
- (call-interactively command))
- ((and command (listp command))
- (eval command t))
- (t
- (error "Not a valid command: %s" command))))))
-
-;;;###autoload
-(defmacro def-menu! (name desc commands &rest plist)
- "Defines a menu and returns a function symbol for invoking it.
-
-A dispatcher is an interactive command named NAME (a symbol). When called, this
-dispatcher prompts you to select a command to run. This list is filtered
-depending on its properties. Each command is takes the form of:
-
- (DESCRIPTION :exec COMMAND &rest PROPERTIES)
-
-PROPERTIES accepts the following properties:
-
- :when FORM
- :unless FORM
- :region BOOL
- :cwd t|PATH
- :project BOOL|DIRECTORY
-
-COMMAND can be a string (a shell command), a symbol (an elisp function) or a
-lisp form.
-
-`def-menu!'s PLIST supports the following properties:
-
- :prompt STRING"
- (declare (indent defun) (doc-string 2))
- (let ((commands-var (intern (format "%s-commands" name)))
- (prop-prompt (or (plist-get plist :prompt) "> "))
- (prop-sort (plist-get plist :sort)))
- `(progn
- (defvar ,commands-var
- ,(if prop-sort
- `(cl-sort ,commands #'string-lessp :key #'car)
- commands)
- ,(format "Menu for %s" name))
- (defun ,name ()
- ,desc
- (interactive)
- (unless ,commands-var
- (user-error "The '%s' menu is empty" ',name))
- (doom--menu-exec
- (or (doom--menu-read
- ,prop-prompt
- (or (cl-remove-if-not
- (let ((project-root (doom-project-root)))
- (lambda (cmd)
- (let ((plist (cdr cmd)))
- (and (cond ((not (plist-member plist :region)) t)
- ((plist-get plist :region) (use-region-p))
- (t (not (use-region-p))))
- (let ((when (plist-get plist :when))
- (unless (plist-get plist :unless))
- (project (plist-get plist :project)))
- (or (or (not when) (eval when))
- (or (not unless) (not (eval unless)))
- (and (stringp project)
- (file-in-directory-p buffer-file-name project-root))))))))
- ,commands-var)
- (user-error "No commands available here")))
- (user-error "No command selected")))))))
-
diff --git a/core/autoload/message.el b/core/autoload/message.el
index 8924264ca..340e98954 100644
--- a/core/autoload/message.el
+++ b/core/autoload/message.el
@@ -1,87 +1,91 @@
;;; core/autoload/message.el -*- lexical-binding: t; -*-
-(defconst doom-message-fg
- '((reset . 0)
- (black . 30)
- (red . 31)
- (green . 32)
- (yellow . 33)
- (blue . 34)
- (magenta . 35)
- (cyan . 36)
- (white . 37))
- "List of text colors.")
+(defvar doom-ansi-alist
+ '(;; fx
+ (bold 1 :weight bold)
+ (dark 2)
+ (italic 3 :slant italic)
+ (underscore 4 :underline t)
+ (blink 5)
+ (rapid 6)
+ (contrary 7)
+ (concealed 8)
+ (strike 9 :strike-through t)
+ ;; fg
+ (black 30 term-color-black)
+ (red 31 term-color-red)
+ (green 32 term-color-green)
+ (yellow 33 term-color-yellow)
+ (blue 34 term-color-blue)
+ (magenta 35 term-color-magenta)
+ (cyan 36 term-color-cyan)
+ (white 37 term-color-white)
+ ;; bg
+ (on-black 40 term-color-black)
+ (on-red 41 term-color-red)
+ (on-green 42 term-color-green)
+ (on-yellow 43 term-color-yellow)
+ (on-blue 44 term-color-blue)
+ (on-magenta 45 term-color-magenta)
+ (on-cyan 46 term-color-cyan)
+ (on-white 47 term-color-white))
+ "TODO")
-(defconst doom-message-bg
- '((on-black . 40)
- (on-red . 41)
- (on-green . 42)
- (on-yellow . 43)
- (on-blue . 44)
- (on-magenta . 45)
- (on-cyan . 46)
- (on-white . 47))
- "List of colors to draw text on.")
+;;;###autoload
+(defun doom-color-apply (style text)
+ "Apply CODE to formatted MESSAGE with ARGS. CODE is derived from any of
+`doom-message-fg', `doom-message-bg' or `doom-message-fx'.
-(defconst doom-message-fx
- '((bold . 1)
- (dark . 2)
- (italic . 3)
- (underscore . 4)
- (blink . 5)
- (rapid . 6)
- (contrary . 7)
- (concealed . 8)
- (strike . 9))
- "List of styles.")
+In a noninteractive session, this wraps the result in ansi color codes.
+Otherwise, it maps colors to a term-color-* face."
+ (let ((code (cadr (assq style doom-ansi-alist))))
+ (if noninteractive
+ (format "\e[%dm%s\e[%dm"
+ (cadr (assq style doom-ansi-alist))
+ text 0)
+ (require 'term) ; piggyback on term's color faces
+ (propertize
+ text 'face
+ (append (get-text-property 0 'face text)
+ (cond ((>= code 40)
+ `(:background ,(caddr (assq style doom-ansi-alist))))
+ ((>= code 30)
+ `(:foreground ,(face-foreground (caddr (assq style doom-ansi-alist)))))
+ ((cddr (assq style doom-ansi-alist)))))))))
+
+(defun doom--short-color-replace (forms)
+ "Replace color-name functions with calls to `doom-color-apply'."
+ (cond ((null forms) nil)
+ ((listp forms)
+ (append (cond ((not (symbolp (car forms)))
+ (list (doom--short-color-replace (car forms))))
+ ((assq (car forms) doom-ansi-alist)
+ `(doom-color-apply ',(car forms)))
+ ((eq (car forms) 'color)
+ (pop forms)
+ `(doom-color-apply ,(car forms)))
+ ((list (car forms))))
+ (doom--short-color-replace (cdr forms))
+ nil))
+ (forms)))
;;;###autoload
(defmacro format! (message &rest args)
- "An alternative to `format' that strips out ANSI codes if used in an
-interactive session."
- `(cl-flet*
- (,@(cl-loop for rule
- in (append doom-message-fg doom-message-bg doom-message-fx)
- collect
- `(,(car rule)
- (lambda (message &rest args)
- (apply #'doom-ansi-apply ',(car rule) message args))))
- (color
- (lambda (code format &rest args)
- (apply #'doom-ansi-apply code format args))))
- (format ,message ,@args)))
+ "An alternative to `format' that understands (color ...) and converts them
+into faces or ANSI codes depending on the type of sesssion we're in."
+ `(format ,@(doom--short-color-replace `(,message ,@args))))
;;;###autoload
-(defmacro message! (message &rest args)
- "An alternative to `message' that strips out ANSI codes if used in an
-interactive session."
- `(if noninteractive
- (message (format! ,message ,@args))
- (let ((buf (get-buffer-create " *doom messages*")))
- (with-current-buffer buf
- (goto-char (point-max))
- (let ((beg (point))
- end)
- (insert (format! ,message ,@args))
- (insert "\n")
- (setq end (point))
- (ansi-color-apply-on-region beg end)))
- (with-selected-window (doom-popup-buffer buf)
- (goto-char (point-max))))))
+(defmacro print! (message &rest args)
+ "Uses `message' in interactive sessions and `princ' otherwise (prints to
+standard out).
-;;;###autoload
-(defmacro debug! (message &rest args)
- "Out a debug message if `doom-debug-mode' is non-nil. Otherwise, ignore this."
- (when doom-debug-mode
- `(message ,message ,@args)))
+Can be colored using (color ...) blocks:
-;;;###autoload
-(defun doom-ansi-apply (code format &rest args)
- (let ((rule (or (assq code doom-message-fg)
- (assq code doom-message-bg)
- (assq code doom-message-fx))))
- (format "\e[%dm%s\e[%dm"
- (cdr rule)
- (apply #'format format args)
- 0)))
+ (print! \"Hello %s\" (bold (blue \"How are you?\")))
+ (print! \"Hello %s\" (red \"World\"))
+ (print! (green \"Great %s!\") \"success\")
+Uses faces in interactive sessions and ANSI codes otherwise."
+ `(progn (princ (format! ,message ,@args))
+ (terpri)))
diff --git a/core/autoload/minibuffer.el b/core/autoload/minibuffer.el
deleted file mode 100644
index 0eaba15c3..000000000
--- a/core/autoload/minibuffer.el
+++ /dev/null
@@ -1,25 +0,0 @@
-;;; core/autoload/minibuffer.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun doom/minibuffer-kill-word ()
- "Kill a word, backwards, but only if the cursor is after
-`minibuffer-prompt-end', to prevent the 'Text is read-only' warning from
-monopolizing the minibuffer."
- (interactive)
- (when (> (point) (minibuffer-prompt-end))
- (call-interactively #'backward-kill-word)))
-
-;;;###autoload
-(defun doom/minibuffer-kill-line ()
- "Kill the entire line, but only if the cursor is after
-`minibuffer-prompt-end', to prevent the 'Text is read-only' warning from
-monopolizing the minibuffer."
- (interactive)
- (when (> (point) (minibuffer-prompt-end))
- (call-interactively #'backward-kill-sentence)))
-
-;;;###autoload
-(defun doom/minibuffer-undo ()
- "Undo an edit in the minibuffer without throwing errors."
- (interactive)
- (ignore-errors (call-interactively #'undo)))
diff --git a/core/autoload/packages.el b/core/autoload/packages.el
index c2049afdd..77fee86d9 100644
--- a/core/autoload/packages.el
+++ b/core/autoload/packages.el
@@ -1,56 +1,60 @@
;;; core/autoload/packages.el -*- lexical-binding: t; -*-
-(require 'use-package)
-(require 'quelpa)
+(load! "cache")
-(defvar doom--last-refresh nil)
+;;; Private functions
+(defun doom--packages-choose (prompt)
+ (let ((table (cl-loop for pkg in package-alist
+ unless (package-built-in-p (cdr pkg))
+ collect (cons (package-desc-full-name (cdr pkg))
+ (cdr pkg)))))
+ (cdr (assoc (completing-read prompt
+ (mapcar #'car table)
+ nil t)
+ table))))
+
+(defun doom--refresh-pkg-cache ()
+ "Clear the cache for `doom-refresh-packages-maybe'."
+ (setq doom--refreshed-p nil)
+ (doom-cache-set 'last-pkg-refresh nil))
+
+
+;;
+;; Library
;;;###autoload
-(defun doom-refresh-packages (&optional force-p)
- "Refresh ELPA packages."
+(defun doom-refresh-packages-maybe (&optional force-p)
+ "Refresh ELPA packages, if it hasn't been refreshed recently."
(when force-p
- (doom-refresh-clear-cache))
- (unless (or (persistent-soft-fetch 'last-pkg-refresh "emacs")
+ (doom--refresh-pkg-cache))
+ (unless (or (doom-cache-get 'last-pkg-refresh)
doom--refreshed-p)
- (condition-case-unless-debug ex
+ (condition-case e
(progn
(message "Refreshing package archives")
(package-refresh-contents)
- (persistent-soft-store 'last-pkg-refresh t "emacs" 900))
- ('error
- (doom-refresh-clear-cache)
- (message "Failed to refresh packages: (%s) %s"
- (car ex) (error-message-string ex))))))
-
-;;;###autoload
-(defun doom-refresh-clear-cache ()
- "Clear the cache for `doom-refresh-packages'."
- (setq doom--refreshed-p nil)
- (persistent-soft-store 'last-pkg-refresh nil "emacs"))
+ (doom-cache-set 'last-pkg-refresh t 1200))
+ ((debug error)
+ (doom--refresh-pkg-cache)
+ (signal 'doom-error e)))))
;;;###autoload
(defun doom-package-backend (name &optional noerror)
- "Get which backend the package NAME was installed with. Can either be elpa or
-quelpa. Throws an error if NOERROR is nil and the package isn't installed."
- (cl-assert (symbolp name) t)
- (cond ((and (or (quelpa-setup-p)
- (error "Could not initialize quelpa"))
- (assq name quelpa-cache))
- 'quelpa)
- ((assq name package-alist)
- 'elpa)
- ((package-built-in-p name)
- 'emacs)
- ((not noerror)
- (error "%s package is not installed" name))))
+ "Get which backend the package NAME was installed with. Can either be elpa,
+quelpa or emacs (built-in). Throws an error if NOERROR is nil and the package
+isn't installed."
+ (cl-check-type name symbol)
+ (cond ((assq name quelpa-cache) 'quelpa)
+ ((assq name package-alist) 'elpa)
+ ((package-built-in-p name) 'emacs)
+ ((not noerror) (error "%s package is not installed" name))))
;;;###autoload
(defun doom-package-outdated-p (name)
"Determine whether NAME (a symbol) is outdated or not. If outdated, returns a
list, whose car is NAME, and cdr the current version list and latest version
list of the package."
- (cl-assert (symbolp name) t)
- (doom-initialize-packages)
+ (cl-check-type name symbol)
(when-let* ((desc (cadr (assq name package-alist))))
(let* ((old-version (package-desc-version desc))
(new-version
@@ -67,24 +71,32 @@ list of the package."
(let ((desc (cadr (assq name package-archive-contents))))
(when (package-desc-p desc)
(package-desc-version desc)))))))
- (when (and (listp old-version) (listp new-version)
- (version-list-< old-version new-version))
+ (unless (and (listp old-version) (listp new-version))
+ (error "Couldn't get version for %s" name))
+ (when (version-list-< old-version new-version)
(list name old-version new-version)))))
;;;###autoload
-(defun doom-package-prop (name prop)
+(defun doom-package-installed-p (name)
+ "TODO"
+ (and (package-installed-p name)
+ (when-let* ((desc (cadr (assq name package-alist))))
+ (let ((dir (package-desc-dir desc)))
+ (file-directory-p dir)))))
+
+;;;###autoload
+(defun doom-package-prop (name prop &optional eval)
"Return PROPerty in NAME's plist."
- (cl-assert (symbolp name) t)
- (cl-assert (keywordp prop) t)
- (doom-initialize-packages)
- (plist-get (cdr (assq name doom-packages)) prop))
+ (cl-check-type name symbol)
+ (cl-check-type prop keyword)
+ (let ((value (plist-get (cdr (assq name doom-packages)) prop)))
+ (if eval (eval value) value)))
;;;###autoload
(defun doom-package-different-backend-p (name)
- "Return t if NAME (a package's symbol) has a new backend than what it was
-installed with. Returns nil otherwise, or if package isn't installed."
- (cl-assert (symbolp name) t)
- (doom-initialize-packages)
+ "Return t if a package named NAME (a symbol) has a new backend than what it
+was installed with. Returns nil otherwise, or if package isn't installed."
+ (cl-check-type name symbol)
(and (package-installed-p name)
(let* ((plist (cdr (assq name doom-packages)))
(old-backend (doom-package-backend name 'noerror))
@@ -92,38 +104,207 @@ installed with. Returns nil otherwise, or if package isn't installed."
(not (eq old-backend new-backend)))))
;;;###autoload
-(defun doom-get-packages (&optional installed-only-p)
- "Retrieves a list of explicitly installed packages (i.e. non-dependencies).
-Each element is a cons cell, whose car is the package symbol and whose cdr is
-the quelpa recipe (if any).
-
-BACKEND can be 'quelpa or 'elpa, and will instruct this function to return only
-the packages relevant to that backend.
-
-Warning: this function is expensive; it re-evaluates all of doom's config files.
-Be careful not to use it in a loop.
-
-If INSTALLED-ONLY-P, only return packages that are installed."
- (doom-initialize-packages t)
- (cl-loop with packages = (append doom-core-packages (mapcar #'car doom-packages))
- for sym in (cl-delete-duplicates packages)
- if (and (or (not installed-only-p)
- (package-installed-p sym))
- (or (assq sym doom-packages)
- (and (assq sym package-alist)
- (list sym))))
- collect it))
+(defun doom-package-different-recipe-p (name)
+ "Return t if a package named NAME (a symbol) has a different recipe than it
+was installed with."
+ (cl-check-type name symbol)
+ (and (package-installed-p name)
+ (when-let* ((quelpa-recipe (assq name quelpa-cache))
+ (doom-recipe (assq name doom-packages)))
+ (not (equal (cdr quelpa-recipe)
+ (cdr (plist-get (cdr doom-recipe) :recipe)))))))
;;;###autoload
-(defun doom-get-depending-on (name)
+(cl-defun doom-find-packages (&key (installed 'any)
+ (private 'any)
+ (disabled 'any)
+ (pinned 'any)
+ (ignored 'any)
+ (core 'any)
+ sort
+ changed
+ backend
+ deps)
+ "Retrieves a list of primary packages (i.e. non-dependencies). Each element is
+a cons cell, whose car is the package symbol and whose cdr is the quelpa recipe
+(if any).
+
+You can build a filtering criteria using one or more of the following
+properties:
+
+ :backend 'quelpa|'elpa|'emacs|'any
+ Include packages installed through 'quelpa, 'elpa or 'emacs. 'any is the
+ wildcard.
+ :installed BOOL|'any
+ t = only include installed packages
+ nil = exclude installed packages
+ :private BOOL|'any
+ t = only include user-installed packages
+ nil = exclude user-installed packages
+ :core BOOL|'any
+ t = only include Doom core packages
+ nil = exclude Doom core packages
+ :disabled BOOL|'any
+ t = only include disabled packages
+ nil = exclude disabled packages
+ :ignored BOOL|'any
+ t = only include ignored packages
+ nil = exclude ignored packages
+ :pinned BOOL|ARCHIVE
+ Only return packages that are pinned (t), not pinned (nil) or pinned to a
+ specific archive (stringp)
+ :deps BOOL
+ Includes the package's dependencies (t) or not (nil).
+
+The resulting list is sorted unless :sort nil is passed to this function.
+
+Warning: this function is expensive, as it re-evaluates your all packages.el
+files."
+ (cl-loop with packages = doom-packages
+ for (sym . plist)
+ in (if sort
+ (cl-sort (copy-sequence doom-packages) #'string-lessp :key #'car)
+ packages)
+ if (and (or (not backend)
+ (eq (doom-package-backend sym t) backend))
+ (or (eq ignored 'any)
+ (let* ((form (plist-get plist :ignore))
+ (value (eval form)))
+ (if ignored value (not value))))
+ (or (eq disabled 'any)
+ (if disabled
+ (plist-get plist :disable)
+ (not (plist-get plist :disable))))
+ (or (eq installed 'any)
+ (if installed
+ (doom-package-installed-p sym)
+ (not (doom-package-installed-p sym))))
+ (or (eq private 'any)
+ (let ((modules (plist-get plist :modules)))
+ (if private
+ (assq :private modules)
+ (not (assq :private modules)))))
+ (or (eq core 'any)
+ (let ((modules (plist-get plist :modules)))
+ (if core
+ (assq :core modules)
+ (not (assq :core modules)))))
+ (or (eq pinned 'any)
+ (cond ((eq pinned 't)
+ (plist-get plist :pin))
+ ((null pinned)
+ (not (plist-get plist :pin)))
+ ((equal (plist-get plist :pin) pinned)))))
+ collect (cons sym plist)
+ and if (and deps (not (package-built-in-p sym)))
+ nconc
+ (cl-loop for pkg in (doom-get-dependencies-for sym 'recursive 'noerror)
+ if (or (eq installed 'any)
+ (if installed
+ (doom-package-installed-p pkg)
+ (not (doom-package-installed-p pkg))))
+ collect (cons pkg (cdr (assq pkg doom-packages))))))
+
+(defun doom--read-module-packages-file (file &optional raw noerror)
+ (with-temp-buffer ; prevent buffer-local settings from propagating
+ (condition-case e
+ (if (not raw)
+ (load file noerror t t)
+ (when (file-readable-p file)
+ (insert-file-contents file)
+ (while (re-search-forward "(package! " nil t)
+ (save-excursion
+ (goto-char (match-beginning 0))
+ (cl-destructuring-bind (name . plist) (cdr (sexp-at-point))
+ (push (cons name
+ (plist-put plist :modules
+ (cond ((file-in-directory-p file doom-private-dir)
+ (list :private))
+ ((file-in-directory-p file doom-core-dir)
+ (list :core))
+ ((doom-module-from-path file)))))
+ doom-packages))))))
+ ((debug error)
+ (signal 'doom-package-error
+ (list (or (doom-module-from-path file)
+ '(:private . packages))
+ e))))))
+
+;;;###autoload
+(defun doom-package-list (&optional all-p)
+ "Retrieve a list of explicitly declared packages from enabled modules.
+
+This excludes core packages listed in `doom-core-packages'.
+
+If ALL-P, gather packages unconditionally across all modules, including disabled
+ones."
+ (let ((noninteractive t)
+ (doom--stage 'packages)
+ (doom-modules (doom-modules))
+ doom-packages
+ doom-disabled-packages
+ package-pinned-packages)
+ (doom--read-module-packages-file (expand-file-name "packages.el" doom-core-dir) all-p)
+ (let ((private-packages (expand-file-name "packages.el" doom-private-dir)))
+ (unless all-p
+ ;; We load the private packages file twice to ensure disabled packages
+ ;; are seen ASAP, and a second time to ensure privately overridden
+ ;; packages are properly overwritten.
+ (doom--read-module-packages-file private-packages nil t))
+ (if all-p
+ (mapc #'doom--read-module-packages-file
+ (doom-files-in doom-modules-dir
+ :depth 2
+ :full t
+ :match "/packages\\.el$"))
+ (cl-loop for key being the hash-keys of doom-modules
+ for path = (doom-module-path (car key) (cdr key) "packages.el")
+ for doom--current-module = key
+ do (doom--read-module-packages-file path nil t)))
+ (doom--read-module-packages-file private-packages all-p t))
+ (append (cl-loop for package in doom-core-packages
+ collect (list package :modules '((:core internal))))
+ (nreverse doom-packages))))
+
+;;;###autoload
+(defun doom-get-package-alist ()
+ "Returns a list of all desired packages, their dependencies and their desc
+objects, in the order of their `package! blocks.'"
+ (cl-remove-duplicates
+ (cl-loop for name in (mapcar #'car doom-packages)
+ if (assq name package-alist)
+ nconc (cl-loop for dep in (package--get-deps name)
+ if (assq dep package-alist)
+ collect (cons dep (cadr it)))
+ and collect (cons name (cadr it)))
+ :key #'car
+ :from-end t))
+
+;;;###autoload
+(defun doom-get-depending-on (name &optional noerror)
"Return a list of packages that depend on the package named NAME."
- (when-let* ((desc (cadr (assq name package-alist))))
- (mapcar #'package-desc-name (package--used-elsewhere-p desc nil t))))
+ (cl-check-type name symbol)
+ (unless (package-built-in-p name)
+ (if-let* ((desc (cadr (assq name package-alist))))
+ (mapcar #'package-desc-name (package--used-elsewhere-p desc nil t))
+ (unless noerror
+ (error "Couldn't find %s, is it installed?" name)))))
;;;###autoload
-(defun doom-get-dependencies-for (name &optional only)
+(defun doom-get-dependencies-for (name &optional recursive noerror)
"Return a list of dependencies for a package."
- (package--get-deps name only))
+ (cl-check-type name symbol)
+ ;; can't get dependencies for built-in packages
+ (unless (package-built-in-p name)
+ (if-let* ((desc (cadr (assq name package-alist))))
+ (let* ((deps (mapcar #'car (package-desc-reqs desc)))
+ (deps (cl-remove-if #'package-built-in-p deps)))
+ (if recursive
+ (nconc deps (mapcan (lambda (dep) (doom-get-dependencies-for dep t t))
+ deps))
+ deps))
+ (unless noerror
+ (error "Couldn't find %s, is it installed?" name)))))
;;;###autoload
(defun doom-get-outdated-packages (&optional include-frozen-p)
@@ -132,44 +313,56 @@ containing (PACKAGE-SYMBOL OLD-VERSION-LIST NEW-VERSION-LIST).
If INCLUDE-FROZEN-P is non-nil, check frozen packages as well.
-Used by `doom//packages-update'."
- (let (quelpa-pkgs elpa-pkgs)
- ;; Separate quelpa from elpa packages
- (dolist (pkg (doom-get-packages t))
- (let ((sym (car pkg)))
- (when (and (or (not (doom-package-prop sym :freeze))
- include-frozen-p)
- (not (doom-package-prop sym :ignore))
- (not (doom-package-different-backend-p sym)))
- (push sym
- (if (eq (doom-package-backend sym) 'quelpa)
- quelpa-pkgs
- elpa-pkgs)))))
+Used by `doom-packages-update'."
+ (doom-refresh-packages-maybe doom-debug-mode)
+ (let-alist
+ (seq-group-by
+ #'doom-package-backend
+ (cl-loop for package in (mapcar #'car package-alist)
+ when (and (or (not (doom-package-prop package :freeze 'eval))
+ include-frozen-p)
+ (not (doom-package-prop package :ignore 'eval))
+ (not (doom-package-different-backend-p package)))
+ collect package))
;; The bottleneck in this process is quelpa's version checks, so check them
;; asynchronously.
- (let (futures)
- (dolist (pkg quelpa-pkgs)
- (debug! "New thread for: %s" pkg)
- (push (async-start
- `(lambda ()
- (setq user-emacs-directory ,user-emacs-directory)
- (let ((noninteractive t))
- (load ,(expand-file-name "core.el" doom-core-dir)))
- (doom-package-outdated-p ',pkg)))
- futures))
- (delq nil
- (append (mapcar #'doom-package-outdated-p elpa-pkgs)
- (mapcar #'async-get (reverse futures)))))))
+ (cl-loop with partitions = (min 2 (/ (length .quelpa) 4))
+ for package-list in (seq-partition .quelpa partitions)
+ do (doom-log "New thread for: %s" package-list)
+ collect
+ (async-start
+ `(lambda ()
+ (let ((gc-cons-threshold ,doom-gc-cons-upper-limit)
+ (doom-init-p t)
+ (noninteractive t)
+ (load-path ',load-path)
+ (package-alist ',package-alist)
+ (package-archive-contents ',package-archive-contents)
+ (package-selected-packages ',package-selected-packages)
+ (doom-packages ',doom-packages)
+ (doom-modules ',doom-modules)
+ (quelpa-cache ',quelpa-cache)
+ (user-emacs-directory ,user-emacs-directory)
+ doom-private-dir)
+ (load ,(expand-file-name "core.el" doom-core-dir))
+ (load ,(expand-file-name "autoload/packages.el" doom-core-dir))
+ (require 'package)
+ (require 'quelpa)
+ (delq nil (mapcar #'doom-package-outdated-p ',package-list)))))
+ into futures
+ finally return
+ (append (delq nil (mapcar #'doom-package-outdated-p .elpa))
+ (mapcan #'async-get futures)
+ nil))))
;;;###autoload
(defun doom-get-orphaned-packages ()
"Return a list of symbols representing packages that are no longer needed or
depended on.
-Used by `doom//packages-autoremove'."
- (doom-initialize-packages t)
+Used by `doom-packages-autoremove'."
(let ((package-selected-packages
- (append (mapcar #'car doom-packages) doom-core-packages)))
+ (mapcar #'car (doom-find-packages :ignored nil :disabled nil))))
(append (package--removable-packages)
(cl-loop for pkg in package-selected-packages
if (and (doom-package-different-backend-p pkg)
@@ -177,114 +370,84 @@ Used by `doom//packages-autoremove'."
collect pkg))))
;;;###autoload
-(defun doom-get-missing-packages (&optional include-ignored-p)
+(defun doom-get-missing-packages ()
"Return a list of requested packages that aren't installed or built-in, but
are enabled (with a `package!' directive). Each element is a list whose CAR is
the package symbol, and whose CDR is a plist taken from that package's
`package!' declaration.
-If INCLUDE-IGNORED-P is non-nil, includes missing packages that are ignored,
-i.e. they have an :ignore property.
-
-Used by `doom//packages-install'."
- (cl-loop for desc in (doom-get-packages)
- for (name . plist) = desc
- if (and (or include-ignored-p
- (not (plist-get plist :ignore)))
- (or (plist-get plist :pin)
- (not (assq name package--builtins)))
- (or (not (assq name package-alist))
- (doom-package-different-backend-p name)))
- collect desc))
-
-;;;###autoload
-(defun doom*package-delete (desc &rest _)
- "Update `quelpa-cache' upon a successful `package-delete'."
- (let ((name (package-desc-name desc)))
- (when (and (not (package-installed-p name))
- (quelpa-setup-p)
- (assq name quelpa-cache))
- (setq quelpa-cache (assq-delete-all name quelpa-cache))
- (quelpa-save-cache)
- (let ((path (expand-file-name (symbol-name name) quelpa-build-dir)))
- (when (file-exists-p path)
- (delete-directory path t))))))
-
-;;; Private functions
-(defsubst doom--sort-alpha (it other)
- (string-lessp (symbol-name (car it))
- (symbol-name (car other))))
-
-(defun doom--packages-choose (prompt)
- (let ((table (cl-loop for pkg in package-alist
- unless (package-built-in-p (cdr pkg))
- collect (cons (package-desc-full-name (cdr pkg))
- (cdr pkg)))))
- (cdr (assoc (completing-read prompt
- (mapcar #'car table)
- nil t)
- table))))
-
-(defmacro doom--condition-case! (&rest body)
- `(condition-case-unless-debug ex
- (condition-case ex2
- (progn ,@body)
- ('file-error
- (message! (bold (red " FILE ERROR: %s" (error-message-string ex2))))
- (message! " Trying again...")
- (quiet! (doom-refresh-packages t))
- ,@body))
- ('user-error
- (message! (bold (red " ERROR: (%s) %s"
- (car ex)
- (error-message-string ex)))))
- ('error
- (doom-refresh-clear-cache)
- (message! (bold (red " FATAL ERROR: (%s) %s"
- (car ex)
- (error-message-string ex)))))))
+Used by `doom-packages-install'."
+ (cl-loop for (name . plist)
+ in (doom-find-packages :ignored nil
+ :disabled nil
+ :deps t)
+ if (and (or (plist-get plist :pin)
+ (not (package-built-in-p name)))
+ (or (not (doom-package-installed-p name))
+ (doom-package-different-backend-p name)
+ (doom-package-different-recipe-p name)))
+ collect (cons name plist)))
;;
;; Main functions
-;;
+(defun doom--delete-package-files (name-or-desc)
+ (let ((pkg-build-dir
+ (if (package-desc-p name-or-desc)
+ (package-desc-dir name-or-desc)
+ (expand-file-name (symbol-name name-or-desc) quelpa-build-dir))))
+ (when (file-directory-p pkg-build-dir)
+ (delete-directory pkg-build-dir t))))
+
+;;;###autoload
(defun doom-install-package (name &optional plist)
"Installs package NAME with optional quelpa RECIPE (see `quelpa-recipe' for an
example; the package name can be omitted)."
- (doom-initialize-packages)
- (when (package-installed-p name)
- (when (doom-package-different-backend-p name)
- (doom-delete-package name t))
- (user-error "%s is already installed" name))
+ (cl-check-type name symbol)
+ (when (and (package-installed-p name)
+ (not (package-built-in-p name)))
+ (if (or (doom-package-different-backend-p name)
+ (doom-package-different-recipe-p name))
+ (doom-delete-package name t)
+ (user-error "%s is already installed" name)))
(let* ((inhibit-message (not doom-debug-mode))
- (plist (or plist (cdr (assq name doom-packages))))
- (recipe (plist-get plist :recipe))
- quelpa-upgrade-p)
- (if recipe
- (quelpa recipe)
+ (plist (or plist (cdr (assq name doom-packages)))))
+ (if-let* ((recipe (plist-get plist :recipe)))
+ (condition-case e
+ (let (quelpa-upgrade-p)
+ (quelpa recipe))
+ ((debug error)
+ (doom--delete-package-files name)
+ (signal (car e) (cdr e))))
(package-install name))
- (when (package-installed-p name)
- (cl-pushnew (cons name plist) doom-packages :test #'eq :key #'car)
- t)))
+ (if (not (package-installed-p name))
+ (doom--delete-package-files name)
+ (add-to-list 'package-selected-packages name nil 'eq)
+ (setf (alist-get name doom-packages) plist)
+ name)))
+;;;###autoload
(defun doom-update-package (name &optional force-p)
"Updates package NAME (a symbol) if it is out of date, using quelpa or
package.el as appropriate."
+ (cl-check-type name symbol)
(unless (package-installed-p name)
- (user-error "%s isn't installed" name))
+ (error "%s isn't installed" name))
(when (doom-package-different-backend-p name)
(user-error "%s's backend has changed and must be uninstalled first" name))
(when (or force-p (doom-package-outdated-p name))
(let ((inhibit-message (not doom-debug-mode))
(desc (cadr (assq name package-alist))))
(pcase (doom-package-backend name)
- ('quelpa
- (or (quelpa-setup-p)
- (error "Failed to initialize quelpa"))
- (let ((quelpa-upgrade-p t))
- (quelpa (assq name quelpa-cache))))
- ('elpa
+ (`quelpa
+ (condition-case e
+ (let ((quelpa-upgrade-p t))
+ (quelpa (assq name quelpa-cache)))
+ ((debug error)
+ (doom--delete-package-files name)
+ (signal (car e) (cdr e)))))
+ (`elpa
(let* ((archive (cadr (assq name package-archive-contents)))
(packages
(if (package-desc-p archive)
@@ -292,201 +455,37 @@ package.el as appropriate."
(package-compute-transaction () (list (list archive))))))
(package-download-transaction packages))))
(unless (doom-package-outdated-p name)
- (when-let* ((old-dir (package-desc-dir desc)))
- (when (file-directory-p old-dir)
- (delete-directory old-dir t)))
+ (doom--delete-package-files desc)
t))))
+;;;###autoload
(defun doom-delete-package (name &optional force-p)
"Uninstalls package NAME if it exists, and clears it from `quelpa-cache'."
+ (cl-check-type name symbol)
(unless (package-installed-p name)
(user-error "%s isn't installed" name))
(let ((inhibit-message (not doom-debug-mode))
+ (spec (assq name quelpa-cache))
quelpa-p)
- (unless (quelpa-setup-p)
- (error "Could not initialize QUELPA"))
- (when (assq name quelpa-cache)
- (setq quelpa-cache (assq-delete-all name quelpa-cache))
+ (when spec
+ (setq quelpa-cache (delq spec quelpa-cache))
(quelpa-save-cache)
(setq quelpa-p t))
(package-delete (cadr (assq name package-alist)) force-p)
- (unless (package-installed-p name)
- (let ((pkg-build-dir (expand-file-name (symbol-name name) quelpa-build-dir)))
- (when (and quelpa-p (file-directory-p pkg-build-dir))
- (delete-directory pkg-build-dir t)))
- t)))
-
-
-;;
-;; Batch/interactive commands
-;;
-
-;;;###autoload
-(defun doom//packages-install ()
- "Interactive command for installing missing packages."
- (interactive)
- (message! "Looking for packages to install...")
- (let ((packages (doom-get-missing-packages)))
- (cond ((not packages)
- (message! (green "No packages to install!")))
-
- ((not (or (getenv "YES")
- (y-or-n-p
- (format "%s packages will be installed:\n\n%s\n\nProceed?"
- (length packages)
- (mapconcat
- (lambda (pkg)
- (format "+ %s (%s)"
- (car pkg)
- (cond ((doom-package-different-backend-p (car pkg))
- (if (plist-get (cdr pkg) :recipe)
- "ELPA -> QUELPA"
- "QUELPA -> ELPA"))
- ((plist-get (cdr pkg) :recipe)
- "QUELPA")
- (t
- "ELPA"))))
- (sort (cl-copy-list packages) #'doom--sort-alpha)
- "\n")))))
- (message! (yellow "Aborted!")))
-
- (t
- (doom-refresh-packages doom-debug-mode)
- (dolist (pkg packages)
- (message! "Installing %s" (car pkg))
- (doom--condition-case!
- (message! "%s%s"
- (cond ((and (package-installed-p (car pkg))
- (not (doom-package-different-backend-p (car pkg))))
- (dark (white "⚠ ALREADY INSTALLED")))
- ((doom-install-package (car pkg) (cdr pkg))
- (green "✓ DONE"))
- (t
- (red "✕ FAILED")))
- (if (plist-member (cdr pkg) :pin)
- (format " [pinned: %s]" (plist-get (cdr pkg) :pin))
- ""))))
-
- (message! (bold (green "Finished!")))
- (doom//reload-load-path)))))
-
-;;;###autoload
-(defun doom//packages-update ()
- "Interactive command for updating packages."
- (interactive)
- (doom-refresh-packages doom-debug-mode)
- (message! "Looking for outdated packages...")
- (let ((packages (sort (doom-get-outdated-packages) #'doom--sort-alpha)))
- (cond ((not packages)
- (message! (green "Everything is up-to-date")))
-
- ((not (or (getenv "YES")
- (y-or-n-p
- (format "%s packages will be updated:\n\n%s\n\nProceed?"
- (length packages)
- (let ((max-len
- (or (car (sort (mapcar (lambda (it) (length (symbol-name (car it)))) packages)
- (lambda (it other) (> it other))))
- 10)))
- (mapconcat
- (lambda (pkg)
- (format (format "+ %%-%ds %%-%ds -> %%s" (+ max-len 2) 14)
- (symbol-name (car pkg))
- (package-version-join (cadr pkg))
- (package-version-join (cl-caddr pkg))))
- packages
- "\n"))))))
- (message! (yellow "Aborted!")))
-
- (t
- (dolist (pkg packages)
- (message! "Updating %s" (car pkg))
- (doom--condition-case!
- (message!
- (let ((result (doom-update-package (car pkg) t)))
- (color (if result 'green 'red)
- (if result "✓ DONE" "✕ FAILED"))))))
-
- (message! (bold (green "Finished!")))
- (doom//reload-load-path)))))
-
-;;;###autoload
-(defun doom//packages-autoremove ()
- "Interactive command for auto-removing orphaned packages."
- (interactive)
- (message! "Looking for orphaned packages...")
- (let ((packages (doom-get-orphaned-packages)))
- (cond ((not packages)
- (message! (green "No unused packages to remove")))
-
- ((not
- (or (getenv "YES")
- (y-or-n-p
- (format
- "%s packages will be deleted:\n\n%s\n\nProceed?"
- (length packages)
- (mapconcat
- (lambda (sym)
- (format "+ %s (%s)" sym
- (let ((backend (doom-package-backend sym)))
- (if (doom-package-different-backend-p sym)
- (if (eq backend 'quelpa)
- "QUELPA->ELPA"
- "ELPA->QUELPA")
- (upcase (symbol-name backend))))))
- (sort (cl-copy-list packages) #'string-lessp)
- "\n")))))
- (message! (yellow "Aborted!")))
-
- (t
- (dolist (pkg packages)
- (doom--condition-case!
- (message!
- (let ((result (doom-delete-package pkg t)))
- (color (if result 'green 'red)
- "%s %s"
- (if result "✓ Removed" "✕ Failed to remove")
- pkg)))))
-
- (message! (bold (green "Finished!")))
- (doom//reload-load-path)))))
+ (doom--delete-package-files name)
+ (not (package-installed-p name))))
;;
;; Interactive commands
-;;
;;;###autoload
-(defalias 'doom/install-package #'package-install)
-
-;;;###autoload
-(defun doom/reinstall-package (desc)
- "Reinstalls package package with optional quelpa RECIPE (see `quelpa-recipe' for
-an example; the package package can be omitted)."
- (declare (interactive-only t))
- (interactive
- (list (doom--packages-choose "Reinstall package: ")))
- (let ((package (package-desc-name desc)))
- (doom-delete-package package t)
- (doom-install-package package (cdr (assq package doom-packages)))))
-
-;;;###autoload
-(defun doom/delete-package (desc)
- "Prompts the user with a list of packages and deletes the selected package.
-Use this interactively. Use `doom-delete-package' for direct calls."
- (declare (interactive-only t))
- (interactive
- (list (doom--packages-choose "Delete package: ")))
- (let ((package (package-desc-name desc)))
- (if (package-installed-p package)
- (if (y-or-n-p (format "%s will be deleted. Confirm?" package))
- (message "%s %s"
- (if (doom-delete-package package t)
- "Deleted"
- "Failed to delete")
- package)
- (message "Aborted"))
- (message "%s isn't installed" package))))
+(defun doom/reload-packages ()
+ "Reload `doom-packages', `package' and `quelpa'."
+ (interactive)
+ (message "Reloading packages")
+ (doom-initialize-packages t)
+ (message "Reloading packages...DONE"))
;;;###autoload
(defun doom/update-package (pkg)
@@ -496,12 +495,15 @@ calls."
(declare (interactive-only t))
(interactive
(let* ((packages (doom-get-outdated-packages))
- (package (if packages
- (completing-read "Update package: "
- (mapcar #'car packages)
- nil t)
- (user-error "All packages are up to date"))))
- (list (cdr (assq (car (assoc package package-alist)) packages)))))
+ (selection (if packages
+ (completing-read "Update package: "
+ (mapcar #'car packages)
+ nil t)
+ (user-error "All packages are up to date")))
+ (name (car (assoc (intern selection) package-alist))))
+ (unless name
+ (user-error "'%s' is already up-to-date" selection))
+ (list (assq name packages))))
(cl-destructuring-bind (package old-version new-version) pkg
(if-let* ((desc (doom-package-outdated-p package)))
(let ((old-v-str (package-version-join old-version))
@@ -514,10 +516,31 @@ calls."
(message "Aborted")))
(message "%s is up-to-date" package))))
+
+;;
+;; Advice
+
;;;###autoload
-(defun doom/refresh-packages (&optional force-p)
- "Synchronize package metadata with the sources in `package-archives'. If
-FORCE-P (the universal argument) is set, ignore the cache."
- (declare (interactive-only t))
- (interactive "P")
- (doom-refresh-packages force-p))
+(defun doom*package-delete (desc &rest _)
+ "Update `quelpa-cache' upon a successful `package-delete'."
+ (let ((name (package-desc-name desc)))
+ (unless (package-installed-p name)
+ (when-let* ((spec (assq name quelpa-cache)))
+ (setq quelpa-cache (delq spec quelpa-cache))
+ (quelpa-save-cache)
+ (doom--delete-package-files name)))))
+
+
+;;
+;; Make package.el cooperate with Doom
+
+;; Updates QUELPA after deleting a package
+;;;###autoload
+(advice-add #'package-delete :after #'doom*package-delete)
+
+;; Replace with Doom variants
+;;;###autoload
+(advice-add #'package-autoremove :override #'doom//autoremove)
+
+;;;###autoload
+(advice-add #'package-install-selected-packages :override #'doom//install)
diff --git a/core/autoload/popups.el b/core/autoload/popups.el
deleted file mode 100644
index c903a964b..000000000
--- a/core/autoload/popups.el
+++ /dev/null
@@ -1,416 +0,0 @@
-;;; core/autoload/popups.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun doom-popup-p (&optional target)
- "Return t if TARGET (a window or buffer) is a popup. Uses current window if
-omitted."
- (when-let* ((target (or target (selected-window))))
- (cond ((bufferp target)
- (and (buffer-live-p target)
- (buffer-local-value 'doom-popup-mode target)))
- ((windowp target)
- (and (window-live-p target)
- (window-parameter target 'popup))))))
-
-;;;###autoload
-(defun doom-popup-buffer (buffer &optional plist extend-p)
- "Display BUFFER in a shackle popup with PLIST rules. See `shackle-rules' for
-possible rules. If EXTEND-P is non-nil, don't overwrite the original rules for
-this popup, just the specified properties. Returns the new popup window."
- (declare (indent defun))
- (unless (bufferp buffer)
- (error "%s is not a valid buffer" buffer))
- (shackle-display-buffer
- buffer
- nil (or (if extend-p
- (append plist (shackle-match buffer))
- plist)
- (shackle-match buffer))))
-
-;;;###autoload
-(defun doom-popup-switch-to-buffer (buffer)
- "Switch the current (or closest) pop-up window to BUFFER."
- (unless (doom-popup-p)
- (if-let* ((popups (doom-popup-windows)))
- (select-window (car popups))
- (error "No popups to switch to")))
- (set-window-dedicated-p nil nil)
- (switch-to-buffer buffer nil t)
- (prog1 (selected-window)
- (set-window-dedicated-p nil t)))
-
-;;;###autoload
-(defun doom-popup-fit-to-buffer (&optional window max-size)
- "Fit WINDOW to the size of its content."
- (unless (string-empty-p (buffer-string))
- (let* ((window-size (doom-popup-size window))
- (max-size (or max-size (doom-popup-property :size window)))
- (size (+ 2 (if (floatp max-size) (truncate (* max-size window-size)) window-size))))
- (fit-window-to-buffer window size nil size))))
-
-;;;###autoload
-(defun doom-popup-move (direction)
- "Move a popup window to another side of the frame, in DIRECTION, which can be
-one of the following: 'left 'right 'above 'below"
- (when (doom-popup-p)
- (let ((buffer (current-buffer))
- (doom-popup-inhibit-autokill t))
- (doom/popup-close)
- (doom-popup-buffer buffer `(:align ,direction) 'extend))))
-
-;;;###autoload
-(defun doom-popup-file (file &optional plist extend-p)
- "Display FILE in a shackle popup, with PLIST rules. See `shackle-rules' for
-possible rules."
- (unless (file-exists-p file)
- (user-error "Can't display file in popup, it doesn't exist: %s" file))
- (doom-popup-buffer (find-file-noselect file t) plist extend-p))
-
-;;;###autoload
-(defun doom-popup-windows (&optional filter-static-p)
- "Get a list of open pop up windows."
- (cl-loop for window in doom-popup-windows
- if (and (doom-popup-p window)
- (not (and filter-static-p
- (doom-popup-property :static window))))
- collect window))
-
-;;;###autoload
-(defun doom-popup-properties (window-or-buffer)
- "Returns a window's popup property list, if possible. The buffer-local
-`doom-popup-rules' always takes priority, but this will fall back to the popup
-window parameter."
- (cond ((windowp window-or-buffer)
- (or (window-parameter window-or-buffer 'popup)
- (doom-popup-properties (window-buffer window-or-buffer))))
- ((bufferp window-or-buffer)
- (buffer-local-value 'doom-popup-rules window-or-buffer))))
-
-;;;###autoload
-(defun doom-popup-property (prop &optional window)
- "Returns a `doom-popup-rules' PROPerty from WINDOW."
- (or (plist-get (doom-popup-properties (or window (selected-window)))
- prop)
- (pcase prop
- (:size shackle-default-size)
- (:align shackle-default-alignment))))
-
-;;;###autoload
-(defun doom-popup-side (&optional window)
- "Return what side a popup WINDOW came from ('left 'right 'above or 'below)."
- (let ((align (doom-popup-property :align window)))
- (when (eq align t)
- (setq align shackle-default-alignment))
- (when (functionp align)
- (setq align (funcall align)))
- align))
-
-;;;###autoload
-(defun doom-popup-size (&optional window)
- "Return the size of a popup WINDOW."
- (pcase (doom-popup-side window)
- ((or 'left 'right) (window-width window))
- ((or 'above 'below) (window-height window))))
-
-(defun doom--popup-data (window)
- (when-let* ((buffer (window-buffer window)))
- `(,(buffer-name buffer)
- :file ,(buffer-file-name buffer)
- :rules ,(window-parameter window 'popup)
- :size ,(doom-popup-size window))))
-
-;;;###autoload
-(defmacro with-popup-rules! (rules &rest body)
- "TODO"
- (declare (indent defun))
- `(let (shackle-rules)
- ,@(cl-loop for rule in rules
- collect `(set! :popup ,@rule))
- ,@body))
-
-;;;###autoload
-(defmacro save-popups! (&rest body)
- "Sets aside all popups before executing the original function, usually to
-prevent the popup(s) from messing up the UI (or vice versa)."
- `(let ((in-popup-p (doom-popup-p))
- (popups (doom-popup-windows))
- (doom-popup-remember-history t)
- (doom-popup-inhibit-autokill t))
- (when popups
- (mapc #'doom/popup-close popups))
- (unwind-protect
- (progn ,@body)
- (when popups
- (let ((origin (selected-window)))
- (doom/popup-restore)
- (unless in-popup-p
- (select-window origin)))))))
-
-
-;; --- Commands ---------------------------
-
-;;;###autoload
-(defun doom/popup-restore ()
- "Restore the last open popups. If the buffers have been killed, and
-represented real files, they will be restored. Dead special buffers or buffers
-with non-nil :autokill properties will not be.
-
-Returns t if popups were restored, nil otherwise."
- (interactive)
- (unless doom-popup-history
- (error "No popups to restore"))
- (let (any-p)
- (dolist (spec doom-popup-history)
- (let ((buffer (get-buffer (car spec)))
- (file (plist-get (cdr spec) :file))
- (rules (plist-get (cdr spec) :rules))
- (size (plist-get (cdr spec) :size)))
- (when (and (not buffer) file)
- (setq buffer
- (if-let* ((buf (get-file-buffer file)))
- (clone-indirect-buffer (buffer-name buf) nil t)
- (find-file-noselect file t))))
- (when size
- (setq rules (plist-put rules :size size)))
- (when (and buffer (doom-popup-buffer buffer rules) (not any-p))
- (setq any-p t))))
- (when any-p
- (setq doom-popup-history '()))
- any-p))
-
-;;;###autoload
-(defun doom/popup-toggle ()
- "Toggle popups on and off. If used outside of popups (and popups are
-available), it will select the nearest popup window."
- (interactive)
- (when (doom-popup-p)
- (if doom-popup-other-window
- (select-window doom-popup-other-window)
- (other-window 1)))
- (if (doom-popup-windows t)
- (let ((doom-popup-inhibit-autokill t))
- (doom/popup-close-all t))
- (doom/popup-restore)))
-
-;;;###autoload
-(defun doom/popup-close (&optional window)
- "Find and close WINDOW if it's a popup. If WINDOW is omitted, defaults to
-`selected-window'. The contained buffer is buried, unless it has an :autokill
-property."
- (interactive)
- (when (doom-popup-p window)
- (delete-window (or window (selected-window)))))
-
-;;;###autoload
-(defun doom/popup-close-all (&optional force-p)
- "Closes most open popups.
-
-Does not close popups that are :static or don't have an :autoclose property (see
-`shackle-rules').
-
-If FORCE-P is non-nil (or this function is called interactively), ignore popups'
-:autoclose property. This command will never close :static popups."
- (interactive
- (list (called-interactively-p 'interactive)))
- (when-let* ((popups (doom-popup-windows t)))
- (let (success doom-popup-remember-history)
- (setq doom-popup-history (delq nil (mapcar #'doom--popup-data popups)))
- (dolist (window popups success)
- (when (or force-p (doom-popup-property :autoclose window))
- (delete-window window)
- (setq success t))))))
-
-;;;###autoload
-(defun doom/popup-kill-all ()
- "Like `doom/popup-close-all', but kill *all* popups, including :static ones,
-without leaving any trace behind (muahaha)."
- (interactive)
- (when-let* ((popups (doom-popup-windows)))
- (let (doom-popup-remember-history)
- (setq doom-popup-history nil)
- (mapc #'delete-window popups))))
-
-;;;###autoload
-(defun doom/popup-close-maybe ()
- "Close the current popup *if* its window doesn't have a noesc parameter."
- (interactive)
- (if (doom-popup-property :noesc)
- (call-interactively
- (if (featurep 'evil)
- #'evil-force-normal-state
- #'keyboard-quit))
- (quit-restore-window nil 'kill)))
-
-;;;###autoload
-(defun doom/popup-this-buffer ()
- "Display currently selected buffer in a popup window."
- (interactive)
- (doom-popup-buffer (current-buffer) '(:align t :autokill t)))
-
-;;;###autoload
-(defun doom/popup-toggle-messages ()
- "Toggle *Messages* buffer."
- (interactive)
- (if-let* ((win (get-buffer-window "*Messages*")))
- (doom/popup-close win)
- (doom-popup-buffer (get-buffer "*Messages*"))))
-
-;;;###autoload
-(defun doom/other-popup (count)
- "Cycle through popup windows. Like `other-window', but for popups."
- (interactive "p")
- (if-let* ((popups (if (doom-popup-p)
- (cdr (memq (selected-window) doom-popup-windows))
- (setq doom-popup-other-window (selected-window))
- doom-popup-windows)))
- (ignore-errors (select-window (nth (mod (1- count) (length popups)) popups)))
- (unless (eq (selected-window) doom-popup-other-window)
- (when doom-popup-other-window
- (select-window doom-popup-other-window t)
- (cl-decf count))
- (when (/= count 0)
- (other-window count)))))
-
-;;;###autoload
-(defalias 'other-popup #'doom/other-popup)
-
-;;;###autoload
-(defun doom/popup-raise (&optional window)
- "Turn a popup window into a normal window."
- (interactive)
- (let ((window (or window (selected-window))))
- (unless (doom-popup-p window)
- (user-error "Not a valid popup to raise"))
- (with-selected-window window
- (doom-popup-mode -1))))
-
-;;;###autoload
-(defun doom/popup-move-top () "See `doom-popup-move'." (interactive) (doom-popup-move 'above))
-;;;###autoload
-(defun doom/popup-move-bottom () "See `doom-popup-move'." (interactive) (doom-popup-move 'below))
-;;;###autoload
-(defun doom/popup-move-left () "See `doom-popup-move'." (interactive) (doom-popup-move 'left))
-;;;###autoload
-(defun doom/popup-move-right () "See `doom-popup-move'." (interactive) (doom-popup-move 'right))
-
-
-;; --- doom-popup-mode --------------------
-
-;;;###autoload
-(define-minor-mode doom-popup-mode
- "Minor mode for popup windows."
- :init-value nil
- :keymap doom-popup-mode-map
- (let ((window (selected-window)))
- ;; If `doom-popup-rules' isn't set for some reason, try to set it
- (setq-local doom-popup-rules (doom-popup-properties window))
- ;; Ensure that buffer-opening functions/commands (like
- ;; `switch-to-buffer-other-window' won't use this window).
- (set-window-parameter window 'no-other-window doom-popup-mode)
- ;; Makes popup window resist interactively changing its buffer.
- (set-window-dedicated-p window doom-popup-mode)
- (cond (doom-popup-mode
- (when doom-popup-no-fringes
- (set-window-fringes window 0 0 fringes-outside-margins))
- ;; Save metadata into window parameters so it can be saved by window
- ;; config persisting plugins like workgroups or persp-mode.
- (set-window-parameter window 'popup (or doom-popup-rules t))
- (when doom-popup-rules
- (cl-loop for param in doom-popup-window-parameters
- when (plist-get doom-popup-rules param)
- do (set-window-parameter window param it))))
-
- (t
- (when doom-popup-no-fringes
- (set-window-fringes window
- doom-fringe-size doom-fringe-size
- fringes-outside-margins))
- ;; Ensure window parameters are cleaned up
- (set-window-parameter window 'popup nil)
- (dolist (param doom-popup-window-parameters)
- (set-window-parameter window param nil))))))
-(put 'doom-popup-mode 'permanent-local t)
-
-;;;###autoload
-(defun doom|hide-modeline-in-popup ()
- "Don't show modeline in popup windows without a :modeline rule. If one exists
-and it's a symbol, use `doom-modeline' to grab the format. If non-nil, show the
-mode-line as normal. If nil (or omitted, by default), then hide the modeline
-entirely."
- (if doom-popup-mode
- (let ((modeline (plist-get doom-popup-rules :modeline)))
- (cond ((or (eq modeline 'nil)
- (not modeline))
- (doom-hide-modeline-mode +1))
- ((and (symbolp modeline)
- (not (eq modeline 't)))
- (setq-local doom--modeline-format (doom-modeline modeline))
- (when doom--modeline-format
- (doom-hide-modeline-mode +1)))))
- (when doom-hide-modeline-mode
- (doom-hide-modeline-mode -1))))
-
-
-;; --- Advice functions -------------------
-
-;;;###autoload
-(defun doom*shackle-always-align (plist)
- "Ensure popups are always aligned and selected by default. Eliminates the need
-for :align t on every rule."
- (when plist
- (unless (or (plist-member plist :align)
- (plist-member plist :same)
- (plist-member plist :frame))
- (plist-put plist :align t))
- (unless (or (plist-member plist :select)
- (plist-member plist :noselect))
- (plist-put plist :select t)))
- plist)
-
-;;;###autoload
-(defun doom*popup-init (orig-fn &rest args)
- "Initializes a window as a popup window by enabling `doom-popup-mode' in it
-and setting `doom-popup-rules' within it. Returns the window."
- (unless (doom-popup-p)
- (setq doom-popup-other-window (selected-window)))
- (let* ((target (car args))
- (plist (or (nth 2 args)
- (cond ((windowp target)
- (and (window-live-p target)
- (shackle-match (window-buffer target))))
- ((bufferp target)
- (and (buffer-live-p target)
- (shackle-match target))))))
- (buffer (get-buffer target))
- (window-min-height (if (plist-get plist :modeline) 4 2))
- window)
- (when (and (doom-real-buffer-p buffer)
- (get-buffer-window-list buffer nil t))
- (setq plist (append (list :autokill t) plist))
- (setcar args (clone-indirect-buffer (buffer-name target) nil t)))
- (unless (setq window (apply orig-fn args))
- (error "No popup window was found for %s: %s" target plist))
- (cl-pushnew window doom-popup-windows :test #'eq)
- (with-selected-window window
- (unless (eq plist t)
- (setq-local doom-popup-rules plist))
- (doom-popup-mode +1)
- (when (plist-get plist :autofit)
- (doom-popup-fit-to-buffer window)))
- window))
-
-;;;###autoload
-(defun doom*popups-save (orig-fn &rest args)
- "Sets aside all popups before executing the original function, usually to
-prevent the popup(s) from messing up the UI (or vice versa)."
- (save-popups! (apply orig-fn args)))
-
-;;;###autoload
-(defun doom*delete-popup-window (&optional window)
- "Ensure that popups are deleted properly, and killed if they have :autokill
-properties."
- (or window (setq window (selected-window)))
- (when (doom-popup-p window)
- (setq doom-popup-windows (delq window doom-popup-windows))
- (when doom-popup-remember-history
- (setq doom-popup-history (list (doom--popup-data window))))))
diff --git a/core/autoload/projects.el b/core/autoload/projects.el
new file mode 100644
index 000000000..a27778ec1
--- /dev/null
+++ b/core/autoload/projects.el
@@ -0,0 +1,127 @@
+;;; core/autoload/projects.el -*- lexical-binding: t; -*-
+
+(defvar projectile-project-root nil)
+
+;;;###autoload
+(autoload 'projectile-relevant-known-projects "projectile")
+
+
+;;
+;; Macros
+
+;;;###autoload
+(defmacro without-project-cache! (&rest body)
+ "Run BODY with projectile's project-root cache disabled. This is necessary if
+you want to interactive with a project other than the one you're in."
+ `(let ((projectile-project-root-cache (make-hash-table :test 'equal))
+ projectile-project-name
+ projectile-project-root
+ projectile-require-project-root)
+ ,@body))
+
+;;;###autoload
+(defmacro project-file-exists-p! (files)
+ "Checks if the project has the specified FILES.
+Paths are relative to the project root, unless they start with ./ or ../ (in
+which case they're relative to `default-directory'). If they start with a slash,
+they are absolute."
+ `(file-exists-p! ,files (doom-project-root)))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun doom/find-file-in-other-project (project-root)
+ "Preforms `projectile-find-file' in a known project of your choosing."
+ (interactive
+ (list
+ (completing-read "Find file in project: " (projectile-relevant-known-projects)
+ nil nil nil nil (doom-project-root))))
+ (unless (file-directory-p project-root)
+ (error "Project directory '%s' doesn't exist" project-root))
+ (doom-project-find-file project-root))
+
+;;;###autoload
+(defun doom/browse-in-other-project (project-root)
+ "Preforms `find-file' in a known project of your choosing."
+ (interactive
+ (list
+ (completing-read "Browse in project: " (projectile-relevant-known-projects)
+ nil nil nil nil (doom-project-root))))
+ (unless (file-directory-p project-root)
+ (error "Project directory '%s' doesn't exist" project-root))
+ (doom-project-browse project-root))
+
+
+;;
+;; Library
+
+;;;###autoload
+(defun doom-project-p (&optional dir)
+ "Return t if DIR (defaults to `default-directory') is a valid project."
+ (and (doom-project-root dir)
+ t))
+
+;;;###autoload
+(defun doom-project-root (&optional dir)
+ "Return the project root of DIR (defaults to `default-directory').
+Returns nil if not in a project."
+ (let ((projectile-project-root (unless dir projectile-project-root))
+ projectile-require-project-root)
+ (projectile-project-root dir)))
+
+;;;###autoload
+(defun doom-project-name (&optional dir)
+ "Return the name of the current project.
+
+Returns '-' if not in a valid project."
+ (if-let* ((project-root (or (doom-project-root dir)
+ (if dir (expand-file-name dir)))))
+ (funcall projectile-project-name-function project-root)
+ "-"))
+
+;;;###autoload
+(defun doom-project-expand (name &optional dir)
+ "Expand NAME to project root."
+ (expand-file-name name (doom-project-root dir)))
+
+;;;###autoload
+(defun doom-project-find-file (dir)
+ "Jump to a file in DIR (searched recursively).
+
+If DIR is not a project, it will be indexed (but not cached)."
+ (unless (file-directory-p dir)
+ (error "Directory %S does not exist" dir))
+ (let* ((default-directory (file-truename (expand-file-name dir)))
+ (project-root (doom-project-root default-directory))
+ (projectile-project-root default-directory)
+ (projectile-enable-caching projectile-enable-caching))
+ (cond ((and project-root (file-equal-p project-root projectile-project-root))
+ (unless (doom-project-p projectile-project-root)
+ ;; Disable caching if this is not a real project; caching
+ ;; non-projects easily has the potential to inflate the projectile
+ ;; cache beyond reason.
+ (setq projectile-enable-caching nil))
+ (call-interactively
+ ;; Intentionally avoid `helm-projectile-find-file', because it runs
+ ;; asynchronously, and thus doesn't see the lexical `default-directory'
+ (if (featurep! :completion ivy)
+ #'counsel-projectile-find-file
+ #'projectile-find-file)))
+ ((fboundp 'project-find-file-in) ; emacs 26.1+ only
+ (project-find-file-in nil (list default-directory) nil))
+ ((fboundp 'counsel-file-jump) ; ivy only
+ (call-interactively #'counsel-file-jump))
+ ((call-interactively #'find-file)))))
+
+;;;###autoload
+(defun doom-project-browse (dir)
+ "Traverse a file structure starting linearly from DIR."
+ (let ((default-directory (file-truename (expand-file-name dir))))
+ (call-interactively
+ (cond ((featurep! :completion ivy)
+ #'counsel-find-file)
+ ((featurep! :completion helm)
+ #'helm-find-files)
+ (#'find-file)))))
diff --git a/core/autoload/scratch.el b/core/autoload/scratch.el
index b0bd510c5..660ad30d0 100644
--- a/core/autoload/scratch.el
+++ b/core/autoload/scratch.el
@@ -1,53 +1,146 @@
;;; core/autoload/scratch.el -*- lexical-binding: t; -*-
-(defvar doom-scratch-files-dir (concat doom-etc-dir "scratch/")
- "Where to store project scratch files, created by
-`doom/open-project-scratch-buffer'.")
+(defvar doom-scratch-default-file "__default"
+ "The default file name for a project-less scratch buffer.
+
+Will be saved in `doom-scratch-dir'.")
+
+(defvar doom-scratch-dir (concat doom-etc-dir "scratch")
+ "Where to save persistent scratch buffers.")
+
+(defvar doom-scratch-buffer-major-mode nil
+ "What major mode to use in scratch buffers. This can be one of the
+following:
+
+ t Inherits the major mode of the last buffer you had selected.
+ nil Uses `fundamental-mode'
+ MAJOR-MODE Any major mode symbol")
+
+(defvar doom-scratch-buffers nil
+ "A list of active scratch buffers.")
+
+(defvar-local doom-scratch-current-project nil
+ "The name of the project associated with the current scratch buffer.")
(defvar doom-scratch-buffer-hook ()
- "The hooks to run after a scratch buffer is made.")
+ "The hooks to run after a scratch buffer is created.")
-(defun doom--create-scratch-buffer (&optional project-p)
- (let ((text (and (region-active-p)
- (buffer-substring-no-properties
- (region-beginning) (region-end))))
- (mode major-mode)
- (derived-p (derived-mode-p 'prog-mode 'text-mode))
- (old-project (doom-project-root)))
- (unless (file-directory-p doom-scratch-files-dir)
- (mkdir doom-scratch-files-dir t))
- (with-current-buffer
- (if project-p
- (find-file-noselect
- (expand-file-name (replace-regexp-in-string
- "\\." "_" (projectile-project-name)
- t t)
- doom-scratch-files-dir)
- nil t)
- (get-buffer-create "*doom:scratch*"))
+(defun doom--load-persistent-scratch-buffer (name)
+ (let ((scratch-file (expand-file-name (or name doom-scratch-default-file)
+ doom-scratch-dir)))
+ (make-directory doom-scratch-dir t)
+ (if (not (file-readable-p scratch-file))
+ nil
+ (erase-buffer)
+ (insert-file-contents scratch-file)
+ (set-auto-mode)
+ t)))
+
+;;;###autoload
+(defun doom-scratch-buffer (&optional mode directory project-name)
+ "Return a scratchpad buffer in major MODE."
+ (let* ((buffer-name (if project-name
+ (format "*doom:scratch (%s)*" project-name)
+ "*doom:scratch*"))
+ (buffer (get-buffer buffer-name)))
+ (with-current-buffer (get-buffer-create buffer-name)
+ (unless buffer
+ (setq buffer (current-buffer)
+ default-directory directory
+ doom-scratch-current-project project-name)
+ (setq doom-scratch-buffers (cl-delete-if-not #'buffer-live-p doom-scratch-buffers))
+ (cl-pushnew buffer doom-scratch-buffers)
+ (doom--load-persistent-scratch-buffer project-name)
+ (when (and (eq major-mode 'fundamental-mode)
+ (functionp mode))
+ (funcall mode))
+ (add-hook 'kill-buffer-hook #'doom|persist-scratch-buffer nil 'local)
+ (run-hooks 'doom-scratch-buffer-created-hook))
+ buffer)))
+
+
+;;
+;;; Persistent scratch buffer
+
+;;;###autoload
+(defun doom|persist-scratch-buffer ()
+ "Save the current buffer to `doom-scratch-dir'."
+ (write-region
+ (point-min) (point-max)
+ (expand-file-name (or doom-scratch-current-project doom-scratch-default-file)
+ doom-scratch-dir)))
+
+;;;###autoload
+(defun doom|persist-scratch-buffers ()
+ "Save all scratch buffers to `doom-scratch-dir'."
+ (dolist (buffer (cl-delete-if-not #'buffer-live-p doom-scratch-buffers))
+ (with-current-buffer buffer
+ (doom|persist-scratch-buffer))))
+
+;;;###autoload
+(add-hook 'kill-emacs-hook #'doom|persist-scratch-buffers)
+
+
+;;
+;;; Commands
+
+;;;###autoload
+(defun doom/open-scratch-buffer (&optional arg project-p)
+ "Opens the (persistent) scratch buffer in a popup.
+
+If passed the prefix ARG, switch to it in the current window.
+If PROJECT-P is non-nil, open a persistent scratch buffer associated with the
+ current project."
+ (interactive "P")
+ (let (projectile-enable-caching)
+ (funcall
+ (if arg
+ #'switch-to-buffer
+ #'pop-to-buffer)
+ (doom-scratch-buffer
+ (cond ((eq doom-scratch-buffer-major-mode t)
+ (unless (or buffer-read-only
+ (derived-mode-p 'special-mode)
+ (string-match-p "^ ?\\*" (buffer-name)))
+ major-mode))
+ ((null doom-scratch-buffer-major-mode)
+ nil)
+ ((symbolp doom-scratch-buffer-major-mode)
+ doom-scratch-buffer-major-mode))
+ default-directory
(when project-p
- (rename-buffer (format "*doom:scratch (%s)*" (projectile-project-name))))
- (setq default-directory old-project)
- (when (and (not (eq major-mode mode))
- derived-p
- (functionp mode))
- (funcall mode))
- (if text (insert text))
- (run-hooks 'doom-scratch-buffer-hook)
- (current-buffer))))
+ (doom-project-name))))))
;;;###autoload
-(defun doom/open-scratch-buffer ()
- "Opens a temporary scratch buffer in a popup window. It is discarded once it
-is closed. If a region is active, copy it to the scratch buffer."
- (interactive)
- (doom-popup-buffer (doom--create-scratch-buffer)))
+(defun doom/open-project-scratch-buffer (&optional arg)
+ "Opens the (persistent) project scratch buffer in a popup.
+
+If passed the prefix ARG, switch to it in the current window."
+ (interactive "P")
+ (doom/open-scratch-buffer arg 'project))
;;;###autoload
-(defun doom/open-project-scratch-buffer ()
- "Opens a (persistent) scratch buffer associated with the current project in a
-popup window. Scratch buffers are stored in `doom-scratch-files-dir'. If a
-region is active, copy it to the scratch buffer."
+(defun doom/revert-scratch-buffer ()
+ "Revert scratch buffer to last persistent state."
(interactive)
- (doom-popup-buffer (doom--create-scratch-buffer t)))
+ (unless (string-match-p "^\\*doom:scratch" (buffer-name))
+ (user-error "Not in a scratch buffer"))
+ (when (doom--load-persistent-scratch-buffer doom-scratch-current-project)
+ (message "Reloaded scratch buffer")))
+;;;###autoload
+(defun doom/delete-persistent-scratch-file (&optional arg)
+ "Deletes a scratch buffer file in `doom-scratch-dir'.
+
+If prefix ARG, delete all persistent scratches."
+ (interactive)
+ (if arg
+ (progn
+ (delete-directory doom-scratch-dir t)
+ (message "Cleared %S" (abbreviate-file-name doom-scratch-dir)))
+ (make-directory doom-scratch-dir t)
+ (let ((file (read-file-name "Delete scratch file > " doom-scratch-dir "scratch")))
+ (if (not (file-exists-p file))
+ (message "%S does not exist" (abbreviate-file-name file))
+ (delete-file file)
+ (message "Successfully deleted %S" (abbreviate-file-name file))))))
diff --git a/core/autoload/sessions.el b/core/autoload/sessions.el
new file mode 100644
index 000000000..1ba92fd85
--- /dev/null
+++ b/core/autoload/sessions.el
@@ -0,0 +1,119 @@
+;;; core/autoload/sessions.el -*- lexical-binding: t; -*-
+
+;;
+;;; Helpers
+
+;;;###autoload
+(defun doom-save-session (&optional file)
+ "TODO"
+ (setq file (expand-file-name (or file (doom-session-file))))
+ (cond ((require 'persp-mode nil t)
+ (unless persp-mode (persp-mode +1))
+ (setq persp-auto-save-opt 0)
+ (persp-save-state-to-file file))
+ ((and (require 'frameset nil t)
+ (require 'restart-emacs nil t))
+ (let ((frameset-filter-alist (append '((client . restart-emacs--record-tty-file))
+ frameset-filter-alist))
+ (desktop-base-file-name (file-name-nondirectory file))
+ (desktop-dirname (file-name-directory file))
+ (desktop-restore-eager t)
+ desktop-file-modtime)
+ (make-directory desktop-dirname t)
+ (desktop-save desktop-dirname t)))
+ ((error "No session backend to save session with"))))
+
+;;;###autoload
+(defun doom-load-session (&optional file)
+ "TODO"
+ (setq file (expand-file-name (or file (doom-session-file))))
+ (message "Attempting to load %s" file)
+ (cond ((require 'persp-mode nil t)
+ (unless persp-mode (persp-mode +1))
+ (persp-load-state-from-file file))
+ ((and (require 'frameset nil t)
+ (require 'restart-emacs nil t))
+ (restart-emacs--restore-frames-using-desktop file))
+ ((error "No session backend to load session with"))))
+
+;;;###autoload
+(defun doom-session-file ()
+ "TODO"
+ (cond ((require 'persp-mode nil t)
+ (expand-file-name persp-auto-save-fname persp-save-dir))
+ ((require 'desktop nil t)
+ (desktop-full-file-name))
+ ((error "No session backend available"))))
+
+
+;;
+;;; Command line switch
+
+;;;###autoload
+(defun doom-restore-session-handler (&rest _)
+ "TODO"
+ (add-hook 'window-setup-hook #'doom-load-session 'append))
+
+;;;###autoload
+(add-to-list 'command-switch-alist (cons "--restore" #'doom-restore-session-handler))
+
+
+;;
+;;; Commands
+
+;;;###autoload
+(defun doom/quickload-session ()
+ "TODO"
+ (interactive)
+ (message "Restoring session...")
+ (doom-load-session)
+ (message "Session restored. Welcome back."))
+
+;;;###autoload
+(defun doom/quicksave-session ()
+ "TODO"
+ (interactive)
+ (message "Saving session")
+ (doom-save-session)
+ (message "Saving session...DONE"))
+
+;;;###autoload
+(defun doom/load-session (file)
+ "TODO"
+ (interactive
+ (let ((session-file (doom-session-file)))
+ (list (or (read-file-name "Session to restore: "
+ (file-name-directory session-file)
+ nil t
+ (file-name-nondirectory session-file))
+ (user-error "No session selected. Aborting")))))
+ (unless file
+ (error "No session file selected"))
+ (message "Loading '%s' session" file)
+ (doom-load-session file))
+
+;;;###autoload
+(defun doom/save-session (file)
+ "TODO"
+ (interactive
+ (let ((session-file (doom-session-file)))
+ (list (or (read-file-name "Save session to: "
+ (file-name-directory session-file)
+ nil nil
+ (file-name-nondirectory session-file))
+ (user-error "No session selected. Aborting")))))
+ (unless file
+ (error "No session file selected"))
+ (message "Saving '%s' session" file)
+ (doom-save-session file))
+
+;;;###autoload
+(defalias 'doom/restart #'restart-emacs)
+
+;;;###autoload
+(defun doom/restart-and-restore (&optional debug)
+ "TODO"
+ (interactive "P")
+ (doom/quicksave-session)
+ (restart-emacs
+ (delq nil (list (if debug "--debug-init") "--restore"))))
diff --git a/core/autoload/system.el b/core/autoload/system.el
deleted file mode 100644
index 97b44e2da..000000000
--- a/core/autoload/system.el
+++ /dev/null
@@ -1,68 +0,0 @@
-;;; core/autoload/system.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun doom-system-os (&optional os)
- "Returns the OS: arch, debian, macos, general linux, cygwin or windows. If OS
-is given, returns t if it matches the current system, and nil otherwise."
- (let* ((gnu-linux-p (eq system-type 'gnu/linux))
- (type (cond ((and gnu-linux-p (file-exists-p "/etc/arch-release"))
- 'arch)
- ((and gnu-linux-p (file-exists-p "/etc/debian_version"))
- 'debian)
- (gnu-linux-p
- 'linux)
- ((eq system-type 'darwin)
- 'macos)
- ((memq system-type '(windows-nt cygwin))
- 'windows)
- (t (error "Unknown OS: %s" system-type)))))
- (or (and os (eq os type))
- type)))
-
-;;;###autoload
-(defun doom-sh (command &rest args)
- "Runs a shell command and prints any output to the DOOM buffer."
- (let ((cmd-list (split-string command " ")))
- (cond ((equal (car cmd-list) "sudo")
- (apply #'doom-sudo (string-join (cdr cmd-list) " ") args))
- ((let ((bin (executable-find "npm")))
- (and (file-exists-p bin)
- (not (file-writable-p bin))))
- (apply #'doom-sudo (string-join cmd-list " ") args))
- (t
- (princ (shell-command-to-string (apply #'format command args)))))))
-
-(defvar tramp-verbose)
-;;;###autoload
-(defun doom-sudo (command &rest args)
- "Like `doom-sh', but runs as root (prompts for password)."
- (let ((tramp-verbose 2))
- (with-current-buffer (get-buffer-create "*doom-sudo*")
- (unless (string-prefix-p "/sudo::/" default-directory)
- (cd "/sudo::/"))
- (princ (shell-command-to-string (apply #'format command args))))))
-
-;;;###autoload
-(defun doom-fetch (fetcher location dest)
- "Clone a remote version-controlled repo at REPO-URL to PATH, if it exists.
-Requires the corresponding client, e.g. git for git repos, hg for mercurial,
-etc."
- (let* ((command (pcase fetcher
- (:github "git clone --recursive https://github.com/%s.git")
- (:git "git clone --recursive %s")
- (:gist "git clone https://gist.github.com/%s.git")
- ;; TODO Add hg
- (_ (error "%s is not a valid fetcher" fetcher))))
- (argv (split-string command " " t))
- (args (format (string-join (cdr argv) " ") location))
- (bin (executable-find (car argv)))
- (dest (expand-file-name dest)))
- (unless bin
- (error "%s couldn't be found" command))
- (unless (file-directory-p dest)
- (funcall (if noninteractive
- (lambda (c) (princ (shell-command-to-string c)))
- #'async-shell-command)
- (format "%s %s %s" bin args (shell-quote-argument dest)))
- (message! "Cloning %s -> %s" location (file-relative-name dest)))))
-
diff --git a/core/autoload/test.el b/core/autoload/test.el
deleted file mode 100644
index e41cd424c..000000000
--- a/core/autoload/test.el
+++ /dev/null
@@ -1,166 +0,0 @@
-;;; core/autoload/test.el -*- lexical-binding: t; no-byte-compile: t; -*-
-
-;;;###autoload
-(defun doom//run-tests (&optional modules)
- "Run all loaded tests, specified by MODULES (a list of module cons cells) or
-command line args following a double dash (each arg should be in the
-'module/submodule' format).
-
-If neither is available, run all tests in all enabled modules."
- (interactive)
- (condition-case-unless-debug ex
- (let (targets)
- ;; ensure DOOM is initialized
- (let (noninteractive)
- (load (expand-file-name "core/core.el" user-emacs-directory) nil t)
- (doom-initialize-modules nil))
- ;; collect targets
- (cond ((and argv (equal (car argv) "--"))
- (cl-loop for arg in (cdr argv)
- if (equal arg "core")
- do (push (expand-file-name "test/" doom-core-dir) targets)
- else
- collect
- (cl-destructuring-bind (car &optional cdr) (split-string arg "/" t)
- (cons (intern (concat ":" car))
- (and cdr (intern cdr))))
- into args
- finally do
- (setq modules args argv nil)))
-
- (modules
- (unless (cl-loop for module in modules
- unless (and (consp module)
- (keywordp (car module))
- (symbolp (cdr module)))
- return t)
- (error "Expected a list of cons, got: %s" modules)))
-
- (t
- (let ((noninteractive t)
- doom-modules)
- (load (expand-file-name "init.test.el" user-emacs-directory) nil t)
- (setq modules (doom-module-pairs)
- targets (list (expand-file-name "test/" doom-core-dir))))))
- ;; resolve targets to a list of test files and load them
- (cl-loop with targets =
- (append targets
- (cl-loop for (module . submodule) in modules
- if submodule
- collect (doom-module-path module submodule "test/")
- else
- nconc
- (cl-loop with module-name = (substring (symbol-name module) 1)
- with module-path = (expand-file-name module-name doom-modules-dir)
- for path in (directory-files module-path t "^\\w")
- collect (expand-file-name "test/" path))))
- for dir in targets
- if (file-directory-p dir)
- nconc (reverse (directory-files-recursively dir "\\.el$"))
- into items
- finally do (quiet! (mapc #'load-file items)))
- ;; run all loaded tests
- (if noninteractive
- (ert-run-tests-batch-and-exit)
- (call-interactively #'ert-run-tests-interactively)))
- ('error
- (lwarn 'doom-test :error
- "%s -> %s"
- (car ex) (error-message-string ex)))))
-
-
-;; --- Test helpers -----------------------
-
-(defmacro def-test! (name &rest body)
- "Define a namespaced ERT test."
- (declare (indent defun) (doc-string 2))
- (let (plist)
- (while (keywordp (car body))
- (push (pop body) plist))
- (setq plist (reverse plist))
- (when (plist-get plist :skip)
- (setq body `((ert-skip nil) ,@body)))
- (when-let* ((modes (doom-enlist (plist-get plist :minor-mode))))
- (dolist (mode modes)
- (setq body `((with-minor-mode!! ,mode ,@body)))))
- (when-let* ((before (plist-get plist :before)))
- (setq body `(,@before ,@body)))
- (when-let* ((after (plist-get plist :after)))
- (setq body `(,@body @after)))
- `(ert-deftest
- ,(cl-loop with path = (file-relative-name (file-name-sans-extension load-file-name)
- doom-emacs-dir)
- for (rep . with) in '(("/test/" . "/") ("/" . ":"))
- do (setq path (replace-regexp-in-string rep with path t t))
- finally return (intern (format "%s::%s" path name)))
- ()
- (with-temp-buffer
- (save-mark-and-excursion
- (save-window-excursion
- ,@body))))))
-
-(defmacro should-buffer!! (initial expected &rest body)
- "Test that a buffer with INITIAL text, run BODY, then test it against EXPECTED.
-
-INITIAL will recognize cursor markers in the form {[0-9]}. A {0} marker marks
-where the cursor should be after setup. Otherwise, the cursor will be placed at
-`point-min'.
-
-EXPECTED will recognize one (optional) cursor marker: {|}, this is the
-'expected' location of the cursor after BODY is finished, and will be tested
-against."
- (declare (indent 2))
- `(with-temp-buffer
- (cl-loop for line in ',initial
- do (insert line "\n"))
- (goto-char (point-min))
- (let (marker-list)
- (save-excursion
- (while (re-search-forward "{\\([0-9]\\)}" nil t)
- (push (cons (match-string 1)
- (set-marker (make-marker) (match-beginning 0)))
- marker-list)
- (replace-match "" t t))
- (if (not marker-list)
- (goto-char (point-min))
- (sort marker-list
- (lambda (m1 m2) (< (marker-position m1)
- (marker-position m2))))
- (when (equal (caar marker-list) "0")
- (goto-char!! 0)))
- ,@body
- (let ((result-text (buffer-substring-no-properties (point-min) (point-max)))
- (point (point))
- same-point
- expected-text)
- (with-temp-buffer
- (cl-loop for line in ',expected
- do (insert line "\n"))
- (save-excursion
- (goto-char 1)
- (when (re-search-forward "{|}" nil t)
- (setq same-point (= point (match-beginning 0)))
- (replace-match "" t t)))
- (setq expected-text (buffer-substring-no-properties (point-min) (point-max)))
- (should (equal expected-text result-text))
- (should same-point)))))))
-
-(defmacro goto-char!! (index)
- "Meant to be used with `should-buffer!!'. Will move the cursor to one of the
-cursor markers. e.g. Go to marker {2} with (goto-char!! 2)."
- `(goto-char (point!! ,index)))
-
-(defmacro point!! (index)
- "Meant to be used with `should-buffer!!'. Returns the position of a cursor
-marker. e.g. {2} can be retrieved with (point!! 2)."
- `(cdr (assoc ,(cond ((numberp index) (number-to-string index))
- ((symbolp index) (symbol-name index))
- ((stringp index) index))
- marker-list)))
-
-(defmacro with-minor-mode!! (mode &rest body)
- "TODO"
- (declare (indent defun))
- `(progn (,mode +1)
- ,@body
- (,mode -1)))
diff --git a/core/autoload/text.el b/core/autoload/text.el
new file mode 100644
index 000000000..2dd0184e1
--- /dev/null
+++ b/core/autoload/text.el
@@ -0,0 +1,181 @@
+;;; core/autoload/text.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun doom-surrounded-p (pair &optional inline balanced)
+ "Returns t if point is surrounded by a brace delimiter: {[(
+
+If INLINE is non-nil, only returns t if braces are on the same line, and
+whitespace is balanced on either side of the cursor.
+
+If INLINE is nil, returns t if the opening and closing braces are on adjacent
+lines, above and below, with only whitespace in between."
+ (when pair
+ (let ((beg (plist-get pair :beg))
+ (end (plist-get pair :end))
+ (pt (point)))
+ (when (and (> pt beg) (< pt end))
+ (when-let* ((cl (plist-get pair :cl))
+ (op (plist-get pair :op)))
+ (and (not (string= op ""))
+ (not (string= cl ""))
+ (let ((nbeg (+ (length op) beg))
+ (nend (- end (length cl))))
+ (let ((content (buffer-substring-no-properties nbeg nend)))
+ (and (string-match-p (format "[ %s]*" (if inline "" "\n")) content)
+ (or (not balanced)
+ (= (- pt nbeg) (- nend pt))))))))))))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun doom/backward-to-bol-or-indent ()
+ "Jump between the indentation column (first non-whitespace character) and the
+beginning of the line. The opposite of
+`doom/forward-to-last-non-comment-or-eol'."
+ (interactive)
+ (let ((pos (point))
+ (indent (save-excursion
+ (beginning-of-visual-line)
+ (skip-chars-forward " \t\r")
+ (point))))
+ (cond ((or (> pos indent) (= pos (line-beginning-position)))
+ (goto-char indent))
+ ((<= pos indent)
+ (beginning-of-visual-line)))))
+
+;;;###autoload
+(defun doom/forward-to-last-non-comment-or-eol ()
+ "Jumps between the last non-blank, non-comment character in the line and the
+true end of the line. The opposite of `doom/backward-to-bol-or-indent'."
+ (interactive)
+ (let ((eol (save-excursion (if visual-line-mode
+ (end-of-visual-line)
+ (end-of-line))
+ (point))))
+ (if (or (and (< (point) eol)
+ (sp-point-in-comment))
+ (not (sp-point-in-comment eol)))
+ (goto-char eol)
+ (let* ((bol (save-excursion (beginning-of-visual-line) (point)))
+ (boc (or (save-excursion
+ (if (not comment-use-syntax)
+ (progn
+ (goto-char bol)
+ (when (re-search-forward comment-start-skip eol t)
+ (or (match-end 1) (match-beginning 0))))
+ (goto-char eol)
+ (while (and (sp-point-in-comment)
+ (> (point) bol))
+ (backward-char))
+ (skip-chars-backward " " bol)
+ (point)))
+ eol)))
+ (cond ((= boc (point))
+ (goto-char eol))
+ ((/= bol boc)
+ (goto-char boc)))))))
+
+;;;###autoload
+(defun doom/dumb-indent ()
+ "Inserts a tab character (or spaces x tab-width)."
+ (interactive)
+ (if indent-tabs-mode
+ (insert "\t")
+ (let* ((movement (% (current-column) tab-width))
+ (spaces (if (= 0 movement) tab-width (- tab-width movement))))
+ (insert (make-string spaces ? )))))
+
+;;;###autoload
+(defun doom/dumb-dedent ()
+ "Dedents the current line."
+ (interactive)
+ (if indent-tabs-mode
+ (call-interactively #'backward-delete-char)
+ (unless (bolp)
+ (save-excursion
+ (when (> (current-column) (current-indentation))
+ (back-to-indentation))
+ (let ((movement (% (current-column) tab-width)))
+ (delete-char
+ (- (if (= 0 movement)
+ tab-width
+ (- tab-width movement)))))))))
+
+;;;###autoload
+(defun doom/backward-kill-to-bol-and-indent ()
+ "Kill line to the first non-blank character. If invoked again
+afterwards, kill line to beginning of line."
+ (interactive)
+ (let ((empty-line-p (save-excursion (beginning-of-line)
+ (looking-at-p "[ \t]*$"))))
+ (funcall (if (fboundp 'evil-delete)
+ #'evil-delete
+ #'delete-region)
+ (point-at-bol) (point))
+ (unless empty-line-p
+ (indent-according-to-mode))))
+
+;;;###autoload
+(defun doom/retab (arg &optional beg end)
+ "Converts tabs-to-spaces or spaces-to-tabs within BEG and END (defaults to
+buffer start and end, to make indentation consistent. Which it does depends on
+the value of `indent-tab-mode'.
+
+If ARG (universal argument) is non-nil, retab the current buffer using the
+opposite indentation style."
+ (interactive "Pr")
+ (unless (and beg end)
+ (setq beg (point-min)
+ end (point-max)))
+ (let ((indent-tabs-mode (if arg (not indent-tabs-mode) indent-tabs-mode)))
+ (if indent-tabs-mode
+ (tabify beg end)
+ (untabify beg end))))
+
+;;;###autoload
+(defun doom/delete-trailing-newlines ()
+ "Trim trailing newlines.
+
+Respects `require-final-newline'."
+ (interactive)
+ (goto-char (point-max))
+ (skip-chars-backward " \t\n\v")
+ (when (looking-at "\n\\(\n\\|\\'\\)")
+ (forward-char 1))
+ (when require-final-newline
+ (unless (bolp)
+ (insert "\n")))
+ (when (looking-at "\n+")
+ (replace-match "")))
+
+;;;###autoload
+(defun doom/dos2unix ()
+ "Convert the current buffer to a Unix file encoding."
+ (interactive)
+ (set-buffer-file-coding-system 'undecided-unix nil))
+
+;;;###autoload
+(defun doom/unix2dos ()
+ "Convert the current buffer to a DOS file encoding."
+ (interactive)
+ (set-buffer-file-coding-system 'undecided-dos nil))
+
+
+;;
+;; Hooks
+
+;;;###autoload
+(defun doom|enable-delete-trailing-whitespace ()
+ "Enables the automatic deletion of trailing whitespaces upon file save.
+
+i.e. enables `ws-butler-mode' in the current buffer."
+ (ws-butler-mode +1))
+
+;;;###autoload
+(defun doom|disable-delete-trailing-whitespace ()
+ "Disables the automatic deletion of trailing whitespaces upon file save.
+
+i.e. disables `ws-butler-mode' in the current buffer."
+ (ws-butler-mode -1))
diff --git a/core/autoload/ui.el b/core/autoload/ui.el
index 912a2bef6..dad427c58 100644
--- a/core/autoload/ui.el
+++ b/core/autoload/ui.el
@@ -1,29 +1,7 @@
;;; core/autoload/ui.el -*- lexical-binding: t; -*-
-;;;###autoload
-(defun doom/toggle-fullscreen ()
- "Toggle fullscreen Emacs (non-native on MacOS)."
- (interactive)
- (set-frame-parameter
- nil 'fullscreen
- (unless (frame-parameter nil 'fullscreen)
- 'fullboth)))
-
-;;;###autoload
-(defun doom/toggle-line-numbers (&optional arg)
- "Toggle `linum-mode'."
- (interactive "P")
- (cond ((boundp 'display-line-numbers)
- (setq display-line-numbers
- (pcase arg
- ('(4) 'relative)
- (1 t)
- (-1 nil)
- (_ (not display-line-numbers)))))
- ((featurep 'nlinum)
- (nlinum-mode (or arg (if nlinum-mode -1 +1))))
- (t
- (error "No line number plugin detected"))))
+;;
+;; Public library
;;;###autoload
(defun doom-resize-window (window new-size &optional horizontal force-p)
@@ -35,32 +13,73 @@ If FORCE-P is omitted when `window-size-fixed' is non-nil, resizing will fail."
horizontal))))
;;;###autoload
-(defun doom/window-zoom ()
- "Close other windows to focus on this one. Activate again to undo this. If the
-window changes before then, the undo expires.
+(defun doom-quit-p (&optional prompt)
+ "Prompt the user for confirmation when killing Emacs.
-Alternatively, use `doom/window-enlargen'."
- (interactive)
- (if (and (one-window-p)
- (assoc ?_ register-alist))
- (jump-to-register ?_)
- (window-configuration-to-register ?_)
- (delete-other-windows)))
+Returns t if it is safe to kill this session. Does not prompt if no real buffers
+are open."
+ (or (not (ignore-errors (doom-real-buffer-list)))
+ (yes-or-no-p (format "››› %s" (or prompt "Quit Emacs?")))
+ (ignore (message "Aborted"))))
+
+
+;;
+;; Advice
-(defvar doom--window-enlargened nil)
;;;###autoload
-(defun doom/window-enlargen ()
- "Enlargen the current window to focus on this one. Does not close other
-windows (unlike `doom/window-zoom') Activate again to undo."
+(defun doom*recenter (&rest _)
+ "Generic advisor for recentering window (typically :after other functions)."
+ (recenter))
+
+;;;###autoload
+(defun doom*shut-up (orig-fn &rest args)
+ "Generic advisor for silencing noisy functions."
+ (quiet! (apply orig-fn args)))
+
+
+;;
+;; Hooks
+
+;;;###autoload
+(defun doom|apply-ansi-color-to-compilation-buffer ()
+ "Applies ansi codes to the compilation buffers. Meant for
+`compilation-filter-hook'."
+ (with-silent-modifications
+ (ansi-color-apply-on-region compilation-filter-start (point))))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun doom/toggle-line-numbers ()
+ "Toggle line numbers.
+
+Cycles through regular, relative and no line numbers. The order depends on what
+`display-line-numbers-type' is set to. If you're using Emacs 26+, and
+visual-line-mode is on, this skips relative and uses visual instead.
+
+See `display-line-numbers' for what these values mean."
(interactive)
- (setq doom--window-enlargened
- (if (and doom--window-enlargened
- (assoc ?_ register-alist))
- (ignore (jump-to-register ?_))
- (window-configuration-to-register ?_)
- (doom-resize-window nil (truncate (/ (frame-width) 1.2)) t)
- (doom-resize-window nil (truncate (/ (frame-height) 1.2)))
- t)))
+ (defvar doom--line-number-style display-line-numbers-type)
+ (let* ((styles `(t ,(if (and EMACS26+ visual-line-mode) 'visual 'relative) nil))
+ (order (cons display-line-numbers-type (remq display-line-numbers-type styles)))
+ (queue (memq doom--line-number-style order))
+ (next (if (= (length queue) 1)
+ (car order)
+ (car (cdr queue)))))
+ (setq doom--line-number-style next)
+ (if EMACS26+
+ (setq display-line-numbers next)
+ (pcase next
+ (`t (nlinum-relative-off) (nlinum-mode +1))
+ (`relative (nlinum-relative-on))
+ (`nil (nlinum-mode -1))))
+ (message "Switched to %s line numbers"
+ (pcase next
+ (`t "normal")
+ (`nil "disabled")
+ (_ (symbol-name next))))))
;;;###autoload
(defun doom/delete-frame ()
@@ -71,26 +90,123 @@ windows (unlike `doom/window-zoom') Activate again to undo."
(delete-frame))
(save-buffers-kill-emacs)))
+;;;###autoload
+(defun doom/window-maximize-buffer ()
+ "Close other windows to focus on this one. Activate again to undo this. If the
+window changes before then, the undo expires.
+
+Alternatively, use `doom/window-enlargen'."
+ (interactive)
+ (if (and (one-window-p)
+ (assq ?_ register-alist))
+ (jump-to-register ?_)
+ (window-configuration-to-register ?_)
+ (delete-other-windows)))
+
+(defvar doom--window-enlargened nil)
+;;;###autoload
+(defun doom/window-enlargen ()
+ "Enlargen the current window to focus on this one. Does not close other
+windows (unlike `doom/window-maximize-buffer') Activate again to undo."
+ (interactive)
+ (setq doom--window-enlargened
+ (if (and doom--window-enlargened
+ (assq ?_ register-alist))
+ (ignore (ignore-errors (jump-to-register ?_)))
+ (window-configuration-to-register ?_)
+ (if (window-dedicated-p)
+ ;; `window-resize' and `window-max-delta' don't respect
+ ;; `ignore-window-parameters', so we gotta force it to.
+ (cl-letf* ((old-window-resize (symbol-function #'window-resize))
+ (old-window-max-delta (symbol-function #'window-max-delta))
+ ((symbol-function #'window-resize)
+ (lambda (window delta &optional horizontal _ignore pixelwise)
+ (funcall old-window-resize window delta horizontal
+ t pixelwise)))
+ ((symbol-function #'window-max-delta)
+ (lambda (&optional window horizontal _ignore trail noup nodown pixelwise)
+ (funcall old-window-max-delta window horizontal t
+ trail noup nodown pixelwise))))
+ (maximize-window))
+ (maximize-window))
+ t)))
+
+;;;###autoload
+(defun doom/window-maximize-horizontally ()
+ "Delete all windows to the left and right of the current window."
+ (interactive)
+ (require 'windmove)
+ (save-excursion
+ (while (ignore-errors (windmove-left)) (delete-window))
+ (while (ignore-errors (windmove-right)) (delete-window))))
+
+;;;###autoload
+(defun doom/window-maximize-vertically ()
+ "Delete all windows above and below the current window."
+ (interactive)
+ (require 'windmove)
+ (save-excursion
+ (while (ignore-errors (windmove-up)) (delete-window))
+ (while (ignore-errors (windmove-down)) (delete-window))))
+
+;;;###autoload
+(defun doom/set-frame-opacity (opacity)
+ "Interactively change the current frame's opacity.
+
+OPACITY is an integer between 0 to 100, inclusive."
+ (interactive
+ (list (read-number "Opacity (0-100): "
+ (or (frame-parameter nil 'alpha)
+ 100))))
+ (set-frame-parameter nil 'alpha opacity))
+
+(defvar-local doom--buffer-narrowed-origin nil)
+;;;###autoload
+(defun doom/clone-and-narrow-buffer (beg end &optional clone-p)
+ "Restrict editing in this buffer to the current region, indirectly. With CLONE-P,
+clone the buffer and hard-narrow the selection. If mark isn't active, then widen
+the buffer (if narrowed).
+
+Inspired from http://demonastery.org/2013/04/emacs-evil-narrow-region/"
+ (interactive "rP")
+ (cond ((or (region-active-p)
+ (and beg end))
+ (deactivate-mark)
+ (when clone-p
+ (let ((old-buf (current-buffer)))
+ (switch-to-buffer (clone-indirect-buffer nil nil))
+ (setq doom--buffer-narrowed-origin old-buf)))
+ (narrow-to-region beg end))
+ (doom--buffer-narrowed-origin
+ (kill-this-buffer)
+ (switch-to-buffer doom--buffer-narrowed-origin)
+ (setq doom--buffer-narrowed-origin nil))
+ (t
+ (widen))))
+
+
+;;
+;; Modes
+
;;;###autoload
(define-minor-mode doom-big-font-mode
"A global mode that resizes the font, for streams, screen-sharing and
-presentations."
+presentations.
+
+Uses `doom-big-font' when enabled."
:init-value nil
:lighter " BIG"
:global t
- (unless (fontp doom-big-font)
- (user-error "`doom-big-font' isn't set to a valid font"))
- (if doom-big-font-mode
- (set-frame-font doom-big-font t t)
+ (unless doom-big-font
+ (user-error "`doom-big-font' must be set to a valid font"))
+ (unless doom-font
+ (user-error "`doom-font' must be set to a valid font"))
+ (let ((doom-font (if doom-big-font-mode
+ doom-big-font
+ doom-font)))
+ (setf (alist-get 'font default-frame-alist)
+ (cond ((null doom-font))
+ ((stringp doom-font) doom-font)
+ ((fontp doom-font) (font-xlfd-name doom-font))
+ ((signal 'wrong-type-argument (list '(fontp stringp) doom-font)))))
(set-frame-font doom-font t t)))
-
-;;;###autoload
-(defun doom//reload-theme ()
- "Reset the color theme currently in use."
- (interactive)
- (let ((theme (or (car-safe custom-enabled-themes) doom-theme)))
- (when theme
- (mapc #'disable-theme custom-enabled-themes))
- (run-hooks 'doom-pre-reload-theme-hook)
- (doom|init-ui)
- (run-hooks 'doom-post-reload-theme-hook)))
diff --git a/core/cli/autoloads.el b/core/cli/autoloads.el
new file mode 100644
index 000000000..84678d50e
--- /dev/null
+++ b/core/cli/autoloads.el
@@ -0,0 +1,368 @@
+;;; core/cli/autoloads.el -*- lexical-binding: t; -*-
+
+(dispatcher! (autoloads a) (doom-reload-autoloads nil 'force)
+ "Regenerates Doom's autoloads file.
+
+This file tells Emacs where to find your module's autoloaded functions and
+plugins.")
+
+;; external variables
+(defvar autoload-timestamps)
+(defvar generated-autoload-load-name)
+(defvar generated-autoload-file)
+
+
+;;
+;; Helpers
+
+(defvar doom-autoload-excluded-packages '(marshal gh)
+ "Packages that have silly or destructive autoload files that try to load
+everyone in the universe and their dog, causing errors that make babies cry. No
+one wants that.")
+
+(defun doom-delete-autoloads-file (file)
+ "Delete FILE (an autoloads file), and delete the accompanying *.elc file, if
+it exists."
+ (cl-check-type file string)
+ (when (file-exists-p file)
+ (when-let* ((buf (find-buffer-visiting doom-autoload-file)))
+ (with-current-buffer buf
+ (set-buffer-modified-p nil))
+ (kill-buffer buf))
+ (delete-file file)
+ (ignore-errors (delete-file (byte-compile-dest-file file)))
+ (message "Deleted old %s" (file-name-nondirectory file))))
+
+(defun doom--warn-refresh-session ()
+ (print! (bold (green "\nFinished!")))
+ (message "If you have a running Emacs Session, you will need to restart it or")
+ (message "reload Doom for changes to take effect:\n")
+ (message " M-x doom/restart-and-restore")
+ (message " M-x doom/restart")
+ (message " M-x doom/reload"))
+
+(defun doom--do-load (&rest files)
+ (if (and noninteractive (not (daemonp)))
+ (add-hook 'kill-emacs-hook #'doom--warn-refresh-session)
+ (dolist (file files)
+ (load-file (byte-compile-dest-file file)))))
+
+(defun doom--byte-compile-file (file)
+ (let ((short-name (file-name-nondirectory file))
+ (byte-compile-dynamic-docstrings t))
+ (condition-case e
+ (when (byte-compile-file file)
+ ;; Give autoloads file a chance to report error
+ (load (if doom-debug-mode
+ file
+ (byte-compile-dest-file file))
+ nil t)
+ (unless noninteractive
+ (message "Finished compiling %s" short-name)))
+ ((debug error)
+ (let ((backup-file (concat file ".bk")))
+ (message "Copied backup to %s" backup-file)
+ (copy-file file backup-file 'overwrite))
+ (doom-delete-autoloads-file file)
+ (signal 'doom-autoload-error (list short-name e))))))
+
+(defun doom-reload-autoloads (&optional file force-p)
+ "Reloads FILE (an autoload file), if it needs reloading.
+
+FILE should be one of `doom-autoload-file' or `doom-package-autoload-file'. If
+it is nil, it will try to reload both. If FORCE-P (universal argument) do it
+even if it doesn't need reloading!"
+ (or (null file)
+ (stringp file)
+ (signal 'wrong-type-argument (list 'stringp file)))
+ (if (stringp file)
+ (cond ((file-equal-p file doom-autoload-file)
+ (doom-reload-doom-autoloads force-p))
+ ((file-equal-p file doom-package-autoload-file)
+ (doom-reload-package-autoloads force-p))
+ ((error "Invalid autoloads file: %s" file)))
+ (doom-reload-doom-autoloads force-p)
+ (doom-reload-package-autoloads force-p)))
+
+
+;;
+;; Doom autoloads
+
+(defun doom--file-cookie-p (file)
+ "Returns the return value of the ;;;###if predicate form in FILE."
+ (with-temp-buffer
+ (insert-file-contents-literally file nil 0 256)
+ (if (and (re-search-forward "^;;;###if " nil t)
+ (<= (line-number-at-pos) 3))
+ (let ((load-file-name file))
+ (eval (sexp-at-point)))
+ t)))
+
+(defun doom--generate-header (func)
+ (goto-char (point-min))
+ (insert ";; -*- lexical-binding:t -*-\n"
+ ";; This file is autogenerated by `" (symbol-name func) "', DO NOT EDIT !!\n\n"))
+
+(defun doom--generate-autoloads (targets)
+ (require 'autoload)
+ (dolist (file targets)
+ (let* ((file (file-truename file))
+ (generated-autoload-file doom-autoload-file)
+ (generated-autoload-load-name (file-name-sans-extension file))
+ (noninteractive (not doom-debug-mode))
+ autoload-timestamps)
+ (print!
+ (cond ((not (doom--file-cookie-p file))
+ "⚠ Ignoring %s")
+ ((autoload-generate-file-autoloads file (current-buffer))
+ (yellow "✕ Nothing in %s"))
+ ((green "✓ Scanned %s")))
+ (if (file-in-directory-p file default-directory)
+ (file-relative-name file)
+ (abbreviate-file-name file))))))
+
+(defun doom--expand-autoloads ()
+ (let ((load-path
+ ;; NOTE With `doom-private-dir' in `load-path', Doom autoloads files
+ ;; will be unable to declare autoloads for the built-in autoload.el
+ ;; Emacs package, should $DOOMDIR/autoload.el exist. Not sure why
+ ;; they'd want to though, so it's an acceptable compromise.
+ (append (list doom-private-dir)
+ doom-modules-dirs
+ load-path))
+ cache)
+ (while (re-search-forward "^\\s-*(autoload\\s-+'[^ ]+\\s-+\"\\([^\"]*\\)\"" nil t)
+ (let ((path (match-string 1)))
+ (replace-match
+ (or (cdr (assoc path cache))
+ (when-let* ((libpath (locate-library path))
+ (libpath (file-name-sans-extension libpath)))
+ (push (cons path (abbreviate-file-name libpath)) cache)
+ libpath)
+ path)
+ t t nil 1)))))
+
+(defun doom--generate-autodefs (targets enabled-targets)
+ (goto-char (point-max))
+ (search-backward ";;;***" nil t)
+ (save-excursion (insert "\n"))
+ (dolist (path targets)
+ (insert
+ (with-temp-buffer
+ (insert-file-contents path)
+ (let ((member-p (or (member path enabled-targets)
+ (file-in-directory-p path doom-core-dir)))
+ forms)
+ (while (re-search-forward "^;;;###autodef *\\([^\n]+\\)?\n" nil t)
+ (let* ((sexp (sexp-at-point))
+ (alt-sexp (match-string 1))
+ (type (car sexp))
+ (name (doom-unquote (cadr sexp)))
+ (origin (cond ((doom-module-from-path path))
+ ((file-in-directory-p path doom-private-dir)
+ `(:private . ,(intern (file-name-base path))))
+ ((file-in-directory-p path doom-emacs-dir)
+ `(:core . ,(intern (file-name-base path))))))
+ (doom-file-form
+ `(put ',name 'doom-file ,(abbreviate-file-name path))))
+ (cond ((and (not member-p) alt-sexp)
+ (push (read alt-sexp) forms))
+
+ ((memq type '(defun defmacro cl-defun cl-defmacro))
+ (cl-destructuring-bind (_ name arglist &rest body) sexp
+ (let ((docstring (if (stringp (car body))
+ (pop body)
+ "No documentation.")))
+ (push (if member-p
+ (make-autoload sexp (abbreviate-file-name (file-name-sans-extension path)))
+ (push doom-file-form forms)
+ (setq docstring (format "THIS FUNCTION DOES NOTHING BECAUSE %s IS DISABLED\n\n%s"
+ origin docstring))
+ (condition-case-unless-debug e
+ (if alt-sexp
+ (read alt-sexp)
+ (append (list (pcase type
+ (`defun 'defmacro)
+ (`cl-defun `cl-defmacro)
+ (_ type))
+ name arglist docstring)
+ (cl-loop for arg in arglist
+ if (and (symbolp arg)
+ (not (keywordp arg))
+ (not (memq arg cl--lambda-list-keywords)))
+ collect arg into syms
+ else if (listp arg)
+ collect (car arg) into syms
+ finally return (if syms `((ignore ,@syms))))))
+ ('error
+ (message "Ignoring autodef %s (%s)"
+ name e)
+ nil)))
+ forms)
+ (push `(put ',name 'doom-module ',origin) forms))))
+
+ ((eq type 'defalias)
+ (cl-destructuring-bind (_type name target &optional docstring) sexp
+ (let ((name (doom-unquote name))
+ (target (doom-unquote target)))
+ (unless member-p
+ (setq docstring (format "THIS FUNCTION DOES NOTHING BECAUSE %s IS DISABLED\n\n%s"
+ origin docstring))
+ (setq target #'ignore))
+ (push doom-file-form forms)
+ (push `(put ',name 'doom-module ',origin) forms)
+ (push `(defalias ',name #',target ,docstring)
+ forms))))
+
+ (member-p
+ (push sexp forms)))))
+ (if forms
+ (concat (string-join (mapcar #'prin1-to-string (reverse forms)) "\n")
+ "\n")
+ ""))))))
+
+(defun doom--cleanup-autoloads ()
+ (goto-char (point-min))
+ (when (re-search-forward "^;;\\(;[^\n]*\\| no-byte-compile: t\\)\n" nil t)
+ (replace-match "" t t)))
+
+(defun doom-reload-doom-autoloads (&optional force-p)
+ "Refreshes the autoloads.el file, specified by `doom-autoload-file', if
+necessary (or if FORCE-P is non-nil).
+
+It scans and reads core/autoload/*.el, modules/*/*/autoload.el and
+modules/*/*/autoload/*.el, and generates `doom-autoload-file'. This file tells
+Emacs where to find lazy-loaded functions.
+
+This should be run whenever your `doom!' block, or a module autoload file, is
+modified."
+ (let* ((default-directory doom-emacs-dir)
+ (doom-modules (doom-modules))
+ (targets
+ (file-expand-wildcards
+ (expand-file-name "autoload/*.el" doom-core-dir)))
+ (enabled-targets (copy-sequence targets))
+ case-fold-search)
+ (dolist (path (doom-module-load-path t))
+ (let* ((auto-dir (expand-file-name "autoload" path))
+ (auto-file (expand-file-name "autoload.el" path))
+ (module (doom-module-from-path auto-file))
+ (module-p (or (doom-module-p (car module) (cdr module))
+ (file-equal-p path doom-private-dir))))
+ (when (file-exists-p auto-file)
+ (push auto-file targets)
+ (if module-p (push auto-file enabled-targets)))
+ (dolist (file (doom-files-in auto-dir :match "\\.el$" :full t))
+ (push file targets)
+ (if module-p (push file enabled-targets)))))
+ (if (and (not force-p)
+ (not doom-emacs-changed-p)
+ (file-exists-p doom-autoload-file)
+ (not (file-newer-than-file-p (expand-file-name "init.el" doom-private-dir)
+ doom-autoload-file))
+ (not (cl-loop for file in targets
+ if (file-newer-than-file-p file doom-autoload-file)
+ return t)))
+ (progn (print! (green "Doom core autoloads is up-to-date"))
+ (doom-initialize-autoloads doom-autoload-file)
+ nil)
+ (doom-delete-autoloads-file doom-autoload-file)
+ (message "Generating new autoloads.el")
+ (make-directory (file-name-directory doom-autoload-file) t)
+ (with-temp-file doom-autoload-file
+ (doom--generate-header 'doom-reload-doom-autoloads)
+ (save-excursion
+ (doom--generate-autoloads (reverse enabled-targets)))
+ ;; Replace autoload paths (only for module autoloads) with absolute
+ ;; paths for faster resolution during load and simpler `load-path'
+ (save-excursion
+ (doom--expand-autoloads)
+ (print! (green "✓ Expanded module autoload paths")))
+ ;; Generates stub definitions for functions/macros defined in disabled
+ ;; modules, so that you will never get a void-function when you use
+ ;; them.
+ (save-excursion
+ (doom--generate-autodefs (reverse targets) enabled-targets)
+ (print! (green "✓ Generated autodefs")))
+ ;; Remove byte-compile inhibiting file variables so we can byte-compile
+ ;; the file, and autoload comments.
+ (doom--cleanup-autoloads)
+ (print! (green "✓ Clean up autoloads")))
+ ;; Byte compile it to give the file a chance to reveal errors.
+ (doom--byte-compile-file doom-autoload-file)
+ (doom--do-load doom-autoload-file)
+ t)))
+
+
+;;
+;; Package autoloads
+
+(defun doom--generate-package-autoloads ()
+ (dolist (spec (doom-get-package-alist))
+ (if-let* ((pkg (car spec))
+ (desc (cdr spec)))
+ (unless (memq pkg doom-autoload-excluded-packages)
+ (let ((file (concat (package--autoloads-file-name desc) ".el")))
+ (when (file-exists-p file)
+ (insert "(let ((load-file-name " (prin1-to-string (abbreviate-file-name file)) "))\n")
+ (insert-file-contents file)
+ (while (re-search-forward "^\\(?:;;\\(.*\n\\)\\|\n\\|(provide '[^\n]+\\)" nil t)
+ (unless (nth 8 (syntax-ppss))
+ (replace-match "" t t)))
+ (unless (bolp) (insert "\n"))
+ (insert ")\n"))))
+ (message "Couldn't find package desc for %s" (car spec)))))
+
+(defun doom--generate-var-cache ()
+ (doom-initialize-packages)
+ (prin1 `(setq load-path ',load-path
+ auto-mode-alist ',auto-mode-alist
+ Info-directory-list ',Info-directory-list
+ doom-disabled-packages ',(mapcar #'car (doom-find-packages :disabled t))
+ package-activated-list ',package-activated-list)
+ (current-buffer)))
+
+(defun doom--cleanup-package-autoloads ()
+ (while (re-search-forward "^\\s-*\\((\\(?:add-to-list\\|\\(?:when\\|if\\) (boundp\\)\\s-+'\\(?:load-path\\|auto-mode-alist\\)\\)" nil t)
+ (goto-char (match-beginning 1))
+ (kill-sexp)))
+
+(defun doom-reload-package-autoloads (&optional force-p)
+ "Compiles `doom-package-autoload-file' from the autoloads files of all
+installed packages. It also caches `load-path', `Info-directory-list',
+`doom-disabled-packages', `package-activated-list' and `auto-mode-alist'.
+
+Will do nothing if none of your installed packages have been modified. If
+FORCE-P (universal argument) is non-nil, regenerate it anyway.
+
+This should be run whenever your `doom!' block or update your packages."
+ (if (and (not force-p)
+ (not doom-emacs-changed-p)
+ (file-exists-p doom-package-autoload-file)
+ (not (file-newer-than-file-p doom-packages-dir doom-package-autoload-file))
+ (not (ignore-errors
+ (cl-loop for key being the hash-keys of (doom-modules)
+ for path = (doom-module-path (car key) (cdr key) "packages.el")
+ if (file-newer-than-file-p path doom-package-autoload-file)
+ return t))))
+ (ignore (print! (green "Doom package autoloads is up-to-date"))
+ (doom-initialize-autoloads doom-package-autoload-file))
+ (let (case-fold-search)
+ (doom-delete-autoloads-file doom-package-autoload-file)
+ (with-temp-file doom-package-autoload-file
+ (doom--generate-header 'doom-reload-package-autoloads)
+ (save-excursion
+ ;; Cache important and expensive-to-initialize state here.
+ (doom--generate-var-cache)
+ (print! (green "✓ Cached package state"))
+ ;; Concatenate the autoloads of all installed packages.
+ (doom--generate-package-autoloads)
+ (print! (green "✓ Package autoloads included")))
+ ;; Remove `load-path' and `auto-mode-alist' modifications (most of them,
+ ;; at least); they are cached later, so all those membership checks are
+ ;; unnecessary overhead.
+ (doom--cleanup-package-autoloads)
+ (print! (green "✓ Removed load-path/auto-mode-alist entries"))))
+ (doom--byte-compile-file doom-package-autoload-file)
+ (doom--do-load doom-package-autoload-file)
+ t))
diff --git a/core/cli/byte-compile.el b/core/cli/byte-compile.el
new file mode 100644
index 000000000..d053ccdab
--- /dev/null
+++ b/core/cli/byte-compile.el
@@ -0,0 +1,190 @@
+;;; core/cli/byte-compile.el -*- lexical-binding: t; -*-
+
+(dispatcher! (compile c) (doom-byte-compile args)
+ "Byte-compiles your config or selected modules.
+
+ compile [TARGETS...]
+ compile :core :private lang/python
+ compile feature lang
+
+Accepts :core, :private and :plugins as special arguments, indicating you want
+to byte-compile Doom's core files, your private config or your ELPA plugins,
+respectively.")
+
+(dispatcher! (recompile rc) (doom-byte-compile args 'recompile)
+ "Re-byte-compiles outdated *.elc files.")
+
+(dispatcher! clean (doom-clean-byte-compiled-files)
+ "Delete all *.elc files.")
+
+
+;;
+;; Helpers
+
+(defun doom--byte-compile-ignore-file-p (path)
+ (let ((filename (file-name-nondirectory path)))
+ (or (string-prefix-p "." filename)
+ (string-prefix-p "test-" filename)
+ (not (equal (file-name-extension path) "el")))))
+
+(defun doom-byte-compile (&optional modules recompile-p)
+ "Byte compiles your emacs configuration.
+
+init.el is always byte-compiled by this.
+
+If MODULES is specified (a list of module strings, e.g. \"lang/php\"), those are
+byte-compiled. Otherwise, all enabled modules are byte-compiled, including Doom
+core. It always ignores unit tests and files with `no-byte-compile' enabled.
+
+WARNING: byte-compilation yields marginal gains and makes debugging new issues
+difficult. It is recommended you don't use it unless you understand the
+reprecussions.
+
+Use `doom-clean-byte-compiled-files' or `make clean' to reverse
+byte-compilation.
+
+If RECOMPILE-P is non-nil, only recompile out-of-date files."
+ (let ((default-directory doom-emacs-dir)
+ (total-ok 0)
+ (total-fail 0)
+ (total-noop 0)
+ compile-plugins-p
+ targets)
+ (dolist (module (delete-dups modules) (nreverse targets))
+ (pcase module
+ (":core" (push doom-core-dir targets))
+ (":private" (push doom-private-dir targets))
+ (":plugins"
+ (cl-loop for (_name . desc) in (doom-get-package-alist)
+ do (package--compile desc))
+ (setq compile-plugins-p t
+ modules (delete ":plugins" modules)))
+ ((pred file-directory-p)
+ (push module targets))
+ ((pred (string-match "^\\([^/]+\\)/\\([^/]+\\)$"))
+ (push (doom-module-locate-path
+ (doom-keyword-intern (match-string 1 module))
+ (intern (match-string 2 module)))
+ targets))))
+ (cl-block 'byte-compile
+ ;; If we're just here to byte-compile our plugins, we're done!
+ (and (not modules)
+ compile-plugins-p
+ (cl-return-from 'byte-compile t))
+ (unless (or (equal modules '(":core"))
+ recompile-p)
+ (unless (or doom-auto-accept
+ (y-or-n-p
+ (concat "Warning: byte compiling is for advanced users. It will interfere with your\n"
+ "efforts to debug issues. It is not recommended you do it if you frequently\n"
+ "tinker with your Emacs config.\n\n"
+ "Alternatively, use `bin/doom compile :core` instead to byte-compile only the\n"
+ "Doom core files, as these don't change often.\n\n"
+ "If you have issues, please make sure byte-compilation isn't the cause by using\n"
+ "`bin/doom clean` to clear out your *.elc files.\n\n"
+ "Byte-compile anyway?")))
+ (message "Aborting.")
+ (cl-return-from 'byte-compile)))
+ (when (and (not recompile-p)
+ (or (null modules)
+ (equal modules '(":core"))))
+ (doom-clean-byte-compiled-files))
+ (let (doom-emacs-changed-p
+ noninteractive)
+ ;; But first we must be sure that Doom and your private config have been
+ ;; fully loaded. Which usually aren't so in an noninteractive session.
+ (unless (and (doom-initialize-autoloads doom-autoload-file)
+ (doom-initialize-autoloads doom-package-autoload-file))
+ (doom-reload-autoloads))
+ (doom-initialize)
+ (doom-initialize-modules 'force))
+ ;; If no targets were supplied, then we use your module list.
+ (unless modules
+ (let ((doom-modules-dirs (delete (expand-file-name "modules/" doom-private-dir)
+ doom-modules-dirs)))
+ (setq targets
+ (append (list doom-core-dir)
+ (delete doom-private-dir (doom-module-load-path))))))
+ ;; Assemble el files we want to compile; taking into account that
+ ;; MODULES may be a list of MODULE/SUBMODULE strings from the command
+ ;; line.
+ (let ((target-files (doom-files-in targets :filter #'doom--byte-compile-ignore-file-p)))
+ (when (or (not modules)
+ (member ":core" modules))
+ (push (expand-file-name "init.el" doom-emacs-dir)
+ target-files))
+ (unless target-files
+ (if targets
+ (message "Couldn't find any valid targets")
+ (message "No targets to %scompile" (if recompile-p "re" "")))
+ (cl-return-from 'byte-compile))
+ (require 'use-package)
+ (condition-case e
+ (let ((use-package-defaults use-package-defaults)
+ (use-package-expand-minimally t)
+ (load-path load-path)
+ kill-emacs-hook kill-buffer-query-functions)
+ ;; Prevent packages from being loaded at compile time if they
+ ;; don't meet their own predicates.
+ (push (list :no-require t
+ (lambda (_name args)
+ (or (when-let* ((pred (or (plist-get args :if)
+ (plist-get args :when))))
+ (not (eval pred t)))
+ (when-let* ((pred (plist-get args :unless)))
+ (eval pred t)))))
+ use-package-defaults)
+ (dolist (target (cl-delete-duplicates (mapcar #'file-truename target-files) :test #'equal))
+ (if (or (not recompile-p)
+ (let ((elc-file (byte-compile-dest-file target)))
+ (and (file-exists-p elc-file)
+ (file-newer-than-file-p target elc-file))))
+ (let ((result (if (or (string-match-p "/\\(?:packages\\|doctor\\)\\.el$" target)
+ (not (doom--file-cookie-p target)))
+ 'no-byte-compile
+ (byte-compile-file target)))
+ (short-name (if (file-in-directory-p target doom-emacs-dir)
+ (file-relative-name target doom-emacs-dir)
+ (abbreviate-file-name target))))
+ (cl-incf
+ (cond ((eq result 'no-byte-compile)
+ (print! (dark (white "⚠ Ignored %s")) short-name)
+ total-noop)
+ ((null result)
+ (print! (red "✕ Failed to compile %s") short-name)
+ total-fail)
+ (t
+ (print! (green "✓ Compiled %s") short-name)
+ (load target t t)
+ total-ok))))
+ (cl-incf total-noop)))
+ (print! (bold (color (if (= total-fail 0) 'green 'red)
+ "%s %d/%d file(s) (%d ignored)"))
+ (if recompile-p "Recompiled" "Compiled")
+ total-ok (- (length target-files) total-noop)
+ total-noop)
+ (or (= total-fail 0)
+ (error "Failed to compile some files")))
+ ((debug error)
+ (print! (red "\nThere were breaking errors.\n\n%s")
+ "Reverting changes...")
+ (signal 'doom-error (list 'byte-compile e))))))))
+
+(defun doom-clean-byte-compiled-files ()
+ "Delete all the compiled elc files in your Emacs configuration and private
+module. This does not include your byte-compiled, third party packages.'"
+ (cl-loop with default-directory = doom-emacs-dir
+ for path
+ in (append (doom-files-in doom-emacs-dir :match "\\.elc$" :depth 0)
+ (doom-files-in doom-private-dir :match "\\.elc$" :depth 1)
+ (doom-files-in doom-core-dir :match "\\.elc$")
+ (doom-files-in doom-modules-dirs :match "\\.elc$" :depth 4))
+ for truepath = (file-truename path)
+ if (file-exists-p path)
+ do (delete-file path)
+ and do
+ (print! (green "✓ Deleted %s")
+ (if (file-in-directory-p truepath default-directory)
+ (file-relative-name truepath)
+ (abbreviate-file-name truepath)))
+ finally do (print! (bold (green "Everything is clean")))))
diff --git a/core/cli/debug.el b/core/cli/debug.el
new file mode 100644
index 000000000..a66a52507
--- /dev/null
+++ b/core/cli/debug.el
@@ -0,0 +1,7 @@
+;;; core/cli/debug.el -*- lexical-binding: t; -*-
+
+(dispatcher! info (doom/info)
+ "Output system info in markdown for bug reports.")
+
+(dispatcher! (version v) (doom/version)
+ "Reports the version of Doom and Emacs.")
diff --git a/core/cli/env.el b/core/cli/env.el
new file mode 100644
index 000000000..1fbc050f9
--- /dev/null
+++ b/core/cli/env.el
@@ -0,0 +1,123 @@
+;;; core/cli/env.el -*- lexical-binding: t; -*-
+
+(dispatcher! env
+ (let ((env-file (abbreviate-file-name doom-env-file)))
+ (pcase (car args)
+ ((or "refresh" "re")
+ (doom-reload-env-file 'force))
+ ("enable"
+ (setenv "DOOMENV" "1")
+ (print! (green "Enabling auto-reload of %S") env-file)
+ (doom-reload-env-file 'force)
+ (print! (green "Done! `doom reload' will now refresh your envvar file.")))
+ ("clear"
+ (setenv "DOOMENV" nil)
+ (unless (file-exists-p env-file)
+ (user-error "%S does not exist to be cleared" env-file))
+ (delete-file env-file)
+ (print! (green "Disabled envvar file by deleting %S") env-file))
+ (_
+ (message "No valid subcommand provided. See `doom help env`."))))
+ "Manages your envvars file.
+
+ env [SUBCOMMAND]
+
+Available subcommands:
+
+ refresh Create or regenerate your envvar file
+ enable enable auto-reloading of your envvars file (on `doom refresh`)
+ clear deletes your envvar file (if it exists) and disables auto-reloading
+
+An envvars file (its location is controlled by the `doom-env-file' variable)
+will contain a list of environment variables scraped from your shell environment
+and loaded when Doom starts (if it exists). This is necessary when Emacs can't
+be launched from your shell environment (e.g. on MacOS or certain app launchers
+on Linux).
+
+To generate a file, run `doom env refresh`. If you'd like this file to be
+auto-reloaded when running `doom refresh`, run `doom env enable` instead (only
+needs to be run once).")
+
+
+;;
+;; Helpers
+
+(defvar doom-env-ignored-vars
+ '("DBUS_SESSION_BUS_ADDRESS"
+ "GPG_AGENT_INFO"
+ "SSH_AGENT_PID"
+ "SSH_AUTH_SOCK"
+ ;; Doom envvars
+ "INSECURE"
+ "DEBUG"
+ "YES")
+ "Environment variables to not save in `doom-env-file'.")
+
+(defvar doom-env-executable
+ (if IS-WINDOWS
+ "set"
+ (executable-find "env"))
+ "The program to use to scrape your shell environment with.
+It is rare that you'll need to change this.")
+
+(defvar doom-env-switches
+ (if IS-WINDOWS
+ '("-c")
+ ;; Execute twice, once in a non-interactive login shell and once in an
+ ;; interactive shell in order to capture all the init files possible.
+ '("-lc" "-ic"))
+ "The `shell-command-switch'es to use on `doom-env-executable'.
+This is a list of strings. Each entry is run separately and in sequence with
+`doom-env-executable' to scrape envvars from your shell environment.")
+
+;; Borrows heavily from Spacemacs' `spacemacs//init-spacemacs-env'.
+(defun doom-reload-env-file (&optional force-p)
+ "Generates `doom-env-file', if it doesn't exist (or FORCE-P is non-nil).
+
+Runs `doom-env-executable' X times, where X = length of `doom-env-switches', to
+scrape the variables from your shell environment. Duplicates are removed. The
+order of `doom-env-switches' determines priority."
+ (when (or force-p (not (file-exists-p doom-env-file)))
+ (with-temp-file doom-env-file
+ (message "%s envvars file at %S"
+ (if (file-exists-p doom-env-file)
+ "Regenerating"
+ "Generating")
+ (abbreviate-file-name doom-env-file))
+ (let ((process-environment doom-site-process-environment))
+ (insert
+ (concat
+ "# -*- mode: dotenv -*-\n"
+ "# ---------------------------------------------------------------------------\n"
+ "# This file was auto-generated by Doom by running:\n"
+ "#\n"
+ (cl-loop for switch in doom-env-switches
+ concat (format "# %s %s %s\n"
+ shell-file-name
+ switch
+ doom-env-executable))
+ "#\n"
+ "# It contains all environment variables scraped from your default shell\n"
+ "# (excluding variables blacklisted in doom-env-ignored-vars).\n"
+ "#\n"
+ "# It is NOT safe to edit this file. Changes will be overwritten next time\n"
+ "# that `doom env refresh` is executed. Alternatively, create your own env file\n"
+ "# in your DOOMDIR and load that with `(load-env-vars FILE)`.\n"
+ "#\n"
+ "# To auto-regenerate this file when `doom reload` is run, use `doom env enable'\n"
+ "# or set DOOMENV=1 in your shell environment/config.\n"
+ "# ---------------------------------------------------------------------------\n\n"))
+ (let ((env-point (point)))
+ (dolist (shell-command-switch doom-env-switches)
+ (message "Scraping env from '%s %s %s'"
+ shell-file-name
+ shell-command-switch
+ doom-env-executable)
+ (insert (shell-command-to-string doom-env-executable)))
+ ;; sort the environment variables
+ (sort-lines nil env-point (point-max))
+ ;; remove adjacent duplicated lines
+ (delete-duplicate-lines env-point (point-max) nil t)
+ ;; remove ignored environment variables
+ (dolist (var doom-env-ignored-vars)
+ (flush-lines (concat "^" var "=") env-point (point-max))))))))
diff --git a/core/cli/packages.el b/core/cli/packages.el
new file mode 100644
index 000000000..2c1e630da
--- /dev/null
+++ b/core/cli/packages.el
@@ -0,0 +1,179 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/cli/packages.el
+
+(dispatcher! (install i) (doom--do #'doom-packages-install)
+ "Installs requested packages that aren't installed.")
+
+(dispatcher! (update u) (doom--do #'doom-packages-update)
+ "Updates packages.")
+
+(dispatcher! (autoremove r) (doom--do #'doom-packages-autoremove)
+ "Removes packages that are no longer needed.")
+
+
+;;
+;; Helpers
+
+(defmacro doom--condition-case! (&rest body)
+ `(condition-case-unless-debug e
+ (progn ,@body)
+ ('user-error
+ (print! (bold (red " NOTICE: %s")) e))
+ ('file-error
+ (print! (bold (red " FILE ERROR: %s")) (error-message-string e))
+ (print! " Trying again...")
+ (quiet! (doom-refresh-packages-maybe t))
+ ,@body)
+ ('error
+ (print! (bold (red " FATAL ERROR: %s\n Run again with the -d flag for details")) e))))
+
+(defsubst doom--do (fn)
+ (doom-reload-doom-autoloads)
+ (when (funcall fn doom-auto-accept)
+ (doom-reload-package-autoloads)))
+
+
+;;
+;; Library
+
+(defun doom-packages-install (&optional auto-accept-p)
+ "Interactive command for installing missing packages."
+ (print! "Looking for packages to install...")
+ (let ((packages (doom-get-missing-packages)))
+ (cond ((not packages)
+ (print! (green "No packages to install!"))
+ nil)
+
+ ((not (or auto-accept-p
+ (y-or-n-p
+ (format "%s packages will be installed:\n\n%s\n\nProceed?"
+ (length packages)
+ (mapconcat
+ (lambda (pkg)
+ (format "+ %s (%s)"
+ (car pkg)
+ (cond ((doom-package-different-recipe-p (car pkg))
+ "new recipe")
+ ((doom-package-different-backend-p (car pkg))
+ (if (plist-get (cdr pkg) :recipe)
+ "ELPA->QUELPA"
+ "QUELPA->ELPA"))
+ ((plist-get (cdr pkg) :recipe)
+ "QUELPA")
+ ("ELPA"))))
+ (cl-sort (cl-copy-list packages) #'string-lessp
+ :key #'car)
+ "\n")))))
+ (user-error "Aborted!"))
+
+ ((let (success)
+ (doom-refresh-packages-maybe doom-debug-mode)
+ (dolist (pkg packages)
+ (print! "Installing %s" (car pkg))
+ (doom--condition-case!
+ (let ((result
+ (or (and (doom-package-installed-p (car pkg))
+ (not (doom-package-different-backend-p (car pkg)))
+ (not (doom-package-different-recipe-p (car pkg)))
+ 'already-installed)
+ (and (doom-install-package (car pkg) (cdr pkg))
+ (setq success t)
+ 'success)
+ 'failure))
+ (pin-label
+ (and (plist-member (cdr pkg) :pin)
+ (format " [pinned: %s]" (plist-get (cdr pkg) :pin)))))
+ (print! "%s%s"
+ (pcase result
+ (`already-installed (dark (white "⚠ ALREADY INSTALLED")))
+ (`success (green "✓ DONE"))
+ (`failure (red "✕ FAILED")))
+ (or pin-label "")))))
+ (print! (bold (green "Finished!")))
+ (when success
+ (set-file-times doom-packages-dir)
+ (doom-delete-autoloads-file doom-package-autoload-file))
+ success)))))
+
+(defun doom-packages-update (&optional auto-accept-p)
+ "Interactive command for updating packages."
+ (print! "Looking for outdated packages...")
+ (let ((packages (cl-sort (cl-copy-list (doom-get-outdated-packages)) #'string-lessp
+ :key #'car)))
+ (cond ((not packages)
+ (print! (green "Everything is up-to-date"))
+ nil)
+
+ ((not (or auto-accept-p
+ (y-or-n-p
+ (format "%s packages will be updated:\n\n%s\n\nProceed?"
+ (length packages)
+ (let ((max-len
+ (or (car (sort (mapcar (lambda (it) (length (symbol-name (car it)))) packages)
+ #'>))
+ 10)))
+ (mapconcat
+ (lambda (pkg)
+ (format (format "+ %%-%ds %%-%ds -> %%s" (+ max-len 2) 14)
+ (symbol-name (car pkg))
+ (package-version-join (cadr pkg))
+ (package-version-join (cl-caddr pkg))))
+ packages
+ "\n"))))))
+ (user-error "Aborted!"))
+
+ ((let (success)
+ (dolist (pkg packages)
+ (print! "Updating %s" (car pkg))
+ (doom--condition-case!
+ (print!
+ (let ((result (doom-update-package (car pkg) t)))
+ (when result (setq success t))
+ (color (if result 'green 'red)
+ (if result "✓ DONE" "✕ FAILED"))))))
+ (print! (bold (green "Finished!")))
+ (when success
+ (set-file-times doom-packages-dir)
+ (doom-delete-autoloads-file doom-package-autoload-file))
+ success)))))
+
+(defun doom-packages-autoremove (&optional auto-accept-p)
+ "Interactive command for auto-removing orphaned packages."
+ (print! "Looking for orphaned packages...")
+ (let ((packages (doom-get-orphaned-packages)))
+ (cond ((not packages)
+ (print! (green "No unused packages to remove"))
+ nil)
+
+ ((not
+ (or auto-accept-p
+ (y-or-n-p
+ (format "%s packages will be deleted:\n\n%s\n\nProceed?"
+ (length packages)
+ (mapconcat
+ (lambda (sym)
+ (let ((backend (doom-package-backend sym)))
+ (format "+ %s (%s)" sym
+ (if (doom-package-different-backend-p sym)
+ (pcase backend
+ (`quelpa "QUELPA->ELPA")
+ (`elpa "ELPA->QUELPA")
+ (_ "removed"))
+ (upcase (symbol-name backend))))))
+ (sort (cl-copy-list packages) #'string-lessp)
+ "\n")))))
+ (user-error "Aborted!"))
+
+ ((let (success)
+ (dolist (pkg packages)
+ (doom--condition-case!
+ (let ((result (doom-delete-package pkg t)))
+ (if result (setq success t))
+ (print! (color (if result 'green 'red) "%s %s")
+ (if result "✓ Removed" "✕ Failed to remove")
+ pkg))))
+ (print! (bold (green "Finished!")))
+ (when success
+ (set-file-times doom-packages-dir)
+ (doom-delete-autoloads-file doom-package-autoload-file))
+ success)))))
diff --git a/core/cli/patch-macos.el b/core/cli/patch-macos.el
new file mode 100644
index 000000000..e680dcd90
--- /dev/null
+++ b/core/cli/patch-macos.el
@@ -0,0 +1,85 @@
+;;; core/cli/patch-macos.el -*- lexical-binding: t; -*-
+
+(dispatcher! (patch-macos)
+ (doom-patch-macos (or (member "--undo" args)
+ (member "-u" args))
+ (doom--find-emacsapp-path))
+ "Patches Emacs.app to respect your shell environment.
+
+A common issue with GUI Emacs on MacOS is that it launches in an environment
+independent of your shell configuration, including your PATH and any other
+utilities like rbenv, rvm or virtualenv.
+
+This patch fixes this by patching Emacs.app (in /Applications or
+~/Applications). It will:
+
+ 1. Move Contents/MacOS/Emacs to Contents/MacOS/RunEmacs
+ 2. And replace Contents/MacOS/Emacs with the following wrapper script:
+
+ #!/bin/bash
+ args=\"$@\"
+ pwd=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\"; pwd -P)\"
+ exec \"$SHELL\" -l -c \"$pwd/RunEmacs $args\"
+
+This ensures that Emacs is always aware of your shell environment, regardless of
+how it is launched.
+
+It can be undone with the --undo or -u options.
+
+Alternatively, you can install the exec-path-from-shell Emacs plugin, which will
+scrape your shell environment remotely, at startup. However, this can be slow
+depending on your shell configuration and isn't always reliable.")
+
+
+;;
+;; Library
+
+(defun doom--find-emacsapp-path ()
+ (or (getenv "EMACS_APP_PATH")
+ (cl-loop for dir in (list "/usr/local/opt/emacs"
+ "/usr/local/opt/emacs-plus"
+ "/Applications"
+ "~/Applications")
+ for appdir = (concat dir "/Emacs.app")
+ if (file-directory-p appdir)
+ return appdir)
+ (user-error "Couldn't find Emacs.app")))
+
+(defun doom-patch-macos (undo-p appdir)
+ "Patches Emacs.app to respect your shell environment."
+ (unless IS-MAC
+ (user-error "You don't seem to be running MacOS"))
+ (unless (file-directory-p appdir)
+ (user-error "Couldn't find '%s'" appdir))
+ (let ((oldbin (expand-file-name "Contents/MacOS/Emacs" appdir))
+ (newbin (expand-file-name "Contents/MacOS/RunEmacs" appdir)))
+ (cond (undo-p
+ (unless (file-exists-p newbin)
+ (user-error "Emacs.app is not patched"))
+ (copy-file newbin oldbin 'ok-if-already-exists nil nil 'preserve-permissions)
+ (unless (file-exists-p oldbin)
+ (error "Failed to copy %s to %s" newbin oldbin))
+ (delete-file newbin)
+ (message "%s successfully unpatched" appdir))
+
+ ((file-exists-p newbin)
+ (user-error "%s is already patched" appdir))
+
+ ((or doom-auto-accept
+ (y-or-n-p
+ (concat "Doom would like to patch your Emacs.app bundle so that it respects\n"
+ "your shell configuration. For more information on why and how, run\n\n"
+ " bin/doom help patch-macos\n\n"
+ "Patch Emacs.app?")))
+ (message "Patching '%s'" appdir)
+ (copy-file oldbin newbin nil nil nil 'preserve-permissions)
+ (unless (file-exists-p newbin)
+ (error "Failed to copy %s to %s" oldbin newbin))
+ (with-temp-buffer
+ (insert "#!/bin/bash\n"
+ "args=\"$@\"\n"
+ "pwd=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\"; pwd -P)\"\n"
+ "exec \"$SHELL\" -l -c \"$pwd/RunEmacs $args\"")
+ (write-file oldbin)
+ (chmod oldbin (file-modes newbin)))
+ (message "%s successfully patched" appdir)))))
diff --git a/core/cli/quickstart.el b/core/cli/quickstart.el
new file mode 100644
index 000000000..d67795fd4
--- /dev/null
+++ b/core/cli/quickstart.el
@@ -0,0 +1,92 @@
+;;; core/cli/quickstart.el -*- lexical-binding: t; -*-
+
+(dispatcher! (quickstart qs) (apply #'doom-quickstart args)
+ "Quickly deploy a private module and Doom.
+
+This deploys a barebones config to ~/.doom.d (if it doesn't already exist). The
+destination can be changed with the -p option, e.g.
+
+ doom -p ~/.config/doom quickstart
+
+Quickstart understands the following switches:
+
+ --no-config Don't deploy dummy config to ~/.doom.d
+ --no-install Don't auto-install packages
+ --no-env Don't generate an envvars file (see `doom help env`)
+ --no-fonts Don't install (or prompt to install) all-the-icons fonts
+
+This command is idempotent and is safe to reuse.")
+
+
+;;
+;; Library
+
+(defun doom-quickstart (&rest args)
+ "Quickly deploy a private module and Doom.
+
+This deploys a barebones config to `doom-private-dir', installs all missing
+packages and regenerates the autoloads file."
+ ;; Create `doom-private-dir'
+ (let ((short-private-dir (abbreviate-file-name doom-private-dir)))
+ (if (member "--no-config" args)
+ (print! (yellow "Not copying private config template, as requested"))
+ (if (file-directory-p doom-private-dir)
+ (print! (yellow "%s directory already exists. Skipping.") short-private-dir)
+ (print! "Creating %s" short-private-dir)
+ (make-directory doom-private-dir t)
+ (print! (green "Done!"))
+
+ ;; Create init.el, config.el & packages.el
+ (dolist (file (list (cons "init.el"
+ (lambda ()
+ (insert-file-contents (expand-file-name "init.example.el" doom-emacs-dir))))
+ (cons "config.el"
+ (lambda ()
+ (insert (format ";;; %sconfig.el -*- lexical-binding: t; -*-\n\n"
+ short-private-dir)
+ ";; Place your private configuration here\n")))
+ (cons "packages.el"
+ (lambda ()
+ (insert (format ";; -*- no-byte-compile: t; -*-\n;;; %spackages.el\n\n"
+ short-private-dir)
+ ";;; Examples:\n"
+ ";; (package! some-package)\n"
+ ";; (package! another-package :recipe (:fetcher github :repo \"username/repo\"))\n"
+ ";; (package! builtin-package :disable t)\n")))))
+ (cl-destructuring-bind (path . fn) file
+ (print! "Creating %s%s" short-private-dir path)
+ (with-temp-file (expand-file-name path doom-private-dir)
+ (funcall fn))
+ (print! (green "Done!")))))))
+
+ ;; In case no init.el was present the first time `doom-initialize-modules' was
+ ;; called in core.el (e.g. on first install)
+ (doom-initialize-modules)
+
+ ;; Ask if Emacs.app should be patched
+ (if (member "--no-env" args)
+ (print! (yellow "Not generating envvars file, as requested"))
+ (when (or doom-auto-accept
+ (y-or-n-p "Generate an env file? (see `doom help env` for details)"))
+ (doom-reload-env-file 'force-p)))
+
+ ;; Install Doom packages
+ (if (member "--no-install" args)
+ (print! (yellow "Not installing plugins, as requested"))
+ (print! "Installing plugins")
+ (doom-packages-install doom-auto-accept))
+
+ (print! "Regenerating autoloads files")
+ (doom-reload-autoloads nil 'force-p)
+
+ (if (member "--no-fonts" args)
+ (print! (yellow "Not installing fonts, as requested"))
+ (when (or doom-auto-accept
+ (y-or-n-p "Download and install all-the-icon's fonts?"))
+ (require 'all-the-icons)
+ (all-the-icons-install-fonts 'yes)))
+
+ (print! (bold (green "\nFinished! Doom is ready to go!\n")))
+ (with-temp-buffer
+ (doom-template-insert "QUICKSTART_INTRO")
+ (print! (buffer-string))))
diff --git a/core/cli/test.el b/core/cli/test.el
new file mode 100644
index 000000000..3d44c34dd
--- /dev/null
+++ b/core/cli/test.el
@@ -0,0 +1,69 @@
+;;; core/cli/test.el -*- lexical-binding: t; -*-
+
+(dispatcher! test (doom-run-tests args)
+ "Run Doom unit tests.")
+
+
+;;
+;; Library
+
+(defun doom-run-tests (&optional modules)
+ "Run all loaded tests, specified by MODULES (a list of module cons cells) or
+command line args following a double dash (each arg should be in the
+'module/submodule' format).
+
+If neither is available, run all tests in all enabled modules."
+ ;; Core libraries aren't fully loaded in a noninteractive session, so we
+ ;; reload it with `noninteractive' set to nil to force them to.
+ (quiet! (doom-reload-autoloads))
+ (let ((doom-modules (doom-modules))
+ noninteractive)
+ (let ((target-paths
+ ;; Convert targets into a list of string paths, pointing to the root
+ ;; directory of modules
+ (cond ((stringp (car modules)) ; command line
+ (save-match-data
+ (cl-loop for arg in modules
+ if (string= arg ":core") collect doom-core-dir
+ else if (string-match-p "/" arg)
+ nconc (mapcar (apply-partially #'expand-file-name arg)
+ doom-modules-dirs)
+ else
+ nconc (cl-loop for dir in doom-modules-dirs
+ for path = (expand-file-name arg dir)
+ if (file-directory-p path)
+ nconc (doom-files-in path :type 'dirs :depth 1 :full t))
+ finally do (setq argv nil))))
+
+ (modules ; cons-cells given to MODULES
+ (cl-loop for (module . submodule) in modules
+ if (doom-module-locate-path module submodule)
+ collect it))
+
+ ((append (list doom-core-dir)
+ (doom-module-load-path))))))
+ ;; Load all the unit test files...
+ (require 'buttercup)
+ (mapc (lambda (file) (load file :noerror (not doom-debug-mode)))
+ (doom-files-in (mapcar (apply-partially #'expand-file-name "test/")
+ target-paths)
+ :match "\\.el$" :full t))
+ ;; ... then run them
+ (when doom-debug-mode
+ (setq buttercup-stack-frame-style 'pretty))
+ (let ((split-width-threshold 0)
+ (split-height-threshold 0)
+ (window-min-width 0)
+ (window-min-height 0))
+ (buttercup-run)))))
+
+
+;;
+;; Test library
+
+(defmacro insert! (&rest text)
+ "Insert TEXT in buffer, then move cursor to last {0} marker."
+ `(progn
+ (insert ,@text)
+ (when (search-backward "{0}" nil t)
+ (replace-match "" t t))))
diff --git a/core/cli/upgrade.el b/core/cli/upgrade.el
new file mode 100644
index 000000000..4a35094bf
--- /dev/null
+++ b/core/cli/upgrade.el
@@ -0,0 +1,85 @@
+;;; core/cli/upgrade.el -*- lexical-binding: t; -*-
+
+(dispatcher! (upgrade up) (doom-upgrade)
+ "Checks out the latest Doom on this branch.
+
+Doing so is equivalent to:
+
+ cd ~/.emacs.d
+ git pull
+ bin/doom clean
+ bin/doom refresh
+ bin/doom update")
+
+
+;;
+;; Quality of Life Commands
+
+(defvar doom-repo-url "https://github.com/hlissner/doom-emacs"
+ "TODO")
+(defvar doom-repo-remote "_upgrade"
+ "TODO")
+
+(defun doom--working-tree-dirty-p (dir)
+ (with-temp-buffer
+ (let ((default-directory dir))
+ (if (zerop (process-file "git" nil (current-buffer) nil
+ "status" "--porcelain" "-uno"))
+ (string-match-p "[^ \t\n]" (buffer-string))
+ (error "Failed to check working tree in %s" dir)))))
+
+(defun doom-upgrade ()
+ "Upgrade Doom to the latest version non-destructively."
+ (require 'vc-git)
+ (let* ((gitdir (expand-file-name ".git" doom-emacs-dir))
+ (branch (vc-git--symbolic-ref doom-emacs-dir))
+ (default-directory doom-emacs-dir))
+ (unless (file-exists-p gitdir)
+ (error "Couldn't find %s. Was Doom cloned properly?"
+ (abbreviate-file-name gitdir)))
+ (unless branch
+ (error "Couldn't detect what branch you're using. Is Doom detached?"))
+ (when (doom--working-tree-dirty-p doom-emacs-dir)
+ (user-error "Refusing to upgrade because Doom has been modified. Stash or undo your changes"))
+ (with-temp-buffer
+ (let ((buf (current-buffer)))
+ (condition-case-unless-debug e
+ (progn
+ (process-file "git" nil buf nil "remote" "remove" doom-repo-remote)
+ (unless (zerop (process-file "git" nil buf nil "remote" "add"
+ doom-repo-remote doom-repo-url))
+ (error "Failed to add %s to remotes" doom-repo-remote))
+ (unless (zerop (process-file "git" nil buf nil "fetch" "--tags"
+ doom-repo-remote branch))
+ (error "Failed to fetch from upstream"))
+ (let ((current-rev (vc-git-working-revision doom-emacs-dir))
+ (rev (string-trim (shell-command-to-string (format "git rev-parse %s/%s" doom-repo-remote branch)))))
+ (unless rev
+ (error "Couldn't detect Doom's version. Is %s a repo?"
+ (abbreviate-file-name doom-emacs-dir)))
+ (when (equal current-rev rev)
+ (user-error "Doom is up to date!"))
+ (message "Updates for Doom are available!\n\n Old revision: %s\n New revision: %s\n"
+ current-rev rev)
+ (message "Comparision diff: https://github.com/hlissner/doom-emacs/compare/%s...%s\n"
+ (substring current-rev 0 10) (substring rev 0 10))
+ ;; TODO Display newsletter diff
+ (unless (or doom-auto-accept (y-or-n-p "Proceed?"))
+ (user-error "Aborted"))
+ (message "Removing byte-compiled files from your config (if any)")
+ (doom-clean-byte-compiled-files)
+ (unless (zerop (process-file "git" nil buf nil "reset" "--hard"
+ (format "%s/%s" doom-repo-remote branch)))
+ (error "An error occurred while checking out the latest commit\n\n%s"
+ (buffer-string)))
+ (unless (equal (vc-git-working-revision doom-emacs-dir) rev)
+ (error "Failed to checkout latest commit.\n\n%s" (buffer-string)))
+ (doom-refresh 'force)
+ (doom-packages-update doom-auto-accept)
+ (message "Done! Please restart Emacs for changes to take effect")))
+ (user-error
+ (message "%s Aborting." (error-message-string e)))
+ (error
+ (message "There was an unexpected error.\n\n%s\n\nOutput:\n%s"
+ (car e)
+ (buffer-string))))))))
diff --git a/core/core-cli.el b/core/core-cli.el
new file mode 100644
index 000000000..8885a7ea6
--- /dev/null
+++ b/core/core-cli.el
@@ -0,0 +1,164 @@
+;;; -*- lexical-binding: t; no-byte-compile: t; -*-
+
+;; Eagerly load these libraries because this module may be loaded in a session
+;; that hasn't been fully initialized (where autoloads files haven't been
+;; generated or `load-path' populated).
+(load! "autoload/debug")
+(load! "autoload/files")
+(load! "autoload/message")
+(load! "autoload/packages")
+
+
+;;
+;; Dispatcher API
+
+(defvar doom-auto-accept (getenv "YES")
+ "If non-nil, Doom will auto-accept any confirmation prompts during batch
+commands like `doom-packages-install', `doom-packages-update' and
+`doom-packages-autoremove'.")
+
+(defconst doom--dispatch-command-alist ())
+(defconst doom--dispatch-alias-alist ())
+
+(defun doom--dispatch-format (desc &optional short)
+ (with-temp-buffer
+ (let ((fill-column 72))
+ (insert desc)
+ (goto-char (point-min))
+ (while (re-search-forward "\n\n[^ \n]" nil t)
+ (fill-paragraph)))
+ (if (not short)
+ (buffer-string)
+ (goto-char (point-min))
+ (buffer-substring-no-properties
+ (line-beginning-position)
+ (line-end-position)))))
+
+(defun doom--dispatch-help (&optional command desc &rest args)
+ "Display help documentation for a dispatcher command. If COMMAND and DESC are
+omitted, show all available commands, their aliases and brief descriptions."
+ (if command
+ (princ (doom--dispatch-format desc))
+ (print! (bold "%-10s\t%s\t%s") "Command:" "Alias" "Description")
+ (dolist (spec (cl-sort doom--dispatch-command-alist #'string-lessp
+ :key #'car))
+ (cl-destructuring-bind (command &key desc _body) spec
+ (let ((aliases (cl-loop for (alias . cmd) in doom--dispatch-alias-alist
+ if (eq cmd command)
+ collect (symbol-name alias))))
+ (print! " %-10s\t%s\t%s"
+ command (if aliases (string-join aliases ",") "")
+ (doom--dispatch-format desc t)))))))
+
+(defun doom-dispatch (cmd args &optional show-help)
+ "Parses ARGS and invokes a dispatcher.
+
+If SHOW-HELP is non-nil, show the documentation for said dispatcher."
+ (when (equal cmd "help")
+ (setq show-help t)
+ (when args
+ (setq cmd (car args)
+ args (cdr args))))
+ (cl-destructuring-bind (command &key desc body)
+ (let ((sym (intern cmd)))
+ (or (assq sym doom--dispatch-command-alist)
+ (assq (cdr (assq sym doom--dispatch-alias-alist))
+ doom--dispatch-command-alist)
+ (user-error "Invalid command: %s" sym)))
+ (if show-help
+ (apply #'doom--dispatch-help command desc args)
+ (funcall body args))))
+
+(defmacro dispatcher! (command form &optional docstring)
+ "Define a dispatcher command. COMMAND is a symbol or a list of symbols
+representing the aliases for this command. DESC is a string description. The
+first line should be short (under 60 letters), as it will be displayed for
+bin/doom help.
+
+BODY will be run when this dispatcher is called."
+ (declare (indent defun) (doc-string 3))
+ (cl-destructuring-bind (cmd &rest aliases)
+ (doom-enlist command)
+ (macroexp-progn
+ (append
+ (when aliases
+ `((dolist (alias ',aliases)
+ (setf (alist-get alias doom--dispatch-alias-alist) ',cmd))))
+ `((setf (alist-get ',cmd doom--dispatch-command-alist)
+ (list :desc ,docstring
+ :body (lambda (args) (ignore args) ,form))))))))
+
+
+;;
+;; Dummy dispatch commands (no-op because they're handled especially)
+
+(dispatcher! run :noop
+ "Run Doom Emacs from bin/doom's parent directory.
+
+All arguments are passed on to Emacs (except for -p and -e).
+
+ doom run
+ doom run -nw init.el
+
+WARNING: this command exists for convenience and testing. Doom will suffer
+additional overhead by being started this way. For the best performance, it is
+best to run Doom out of ~/.emacs.d and ~/.doom.d.")
+
+(dispatcher! (doctor doc) :noop
+ "Checks for issues with your environment & Doom config.
+
+Use the doctor to diagnose common problems or list missing dependencies in
+enabled modules.")
+
+(dispatcher! (help h) :noop
+ "Look up additional information about a command.")
+
+
+;;
+;; Real dispatch commands
+
+(load! "cli/autoloads")
+(load! "cli/byte-compile")
+(load! "cli/debug")
+(load! "cli/env")
+(load! "cli/packages")
+(load! "cli/patch-macos")
+(load! "cli/quickstart")
+(load! "cli/upgrade")
+(load! "cli/test")
+
+
+;;
+(defun doom-refresh (&optional force-p)
+ "Ensure Doom is in a working state by checking autoloads and packages, and
+recompiling any changed compiled files. This is the shotgun solution to most
+problems with doom."
+ (when (getenv "DOOMENV")
+ (doom-reload-env-file 'force))
+ (doom-reload-doom-autoloads force-p)
+ (unwind-protect
+ (progn
+ (ignore-errors
+ (doom-packages-autoremove doom-auto-accept))
+ (ignore-errors
+ (doom-packages-install doom-auto-accept)))
+ (doom-reload-package-autoloads force-p)
+ (doom-byte-compile nil 'recompile)))
+
+(dispatcher! (refresh re) (doom-refresh 'force)
+ "Refresh Doom.
+
+This is the equivalent of running autoremove, install, autoloads, then
+recompile. Run this whenever you:
+
+ 1. Modify your `doom!' block,
+ 2. Add or remove `package!' blocks to your config,
+ 3. Add or remove autoloaded functions in module autoloaded files.
+ 4. Update Doom outside of Doom (e.g. with git)
+
+It will ensure that unneeded packages are removed, all needed packages are
+installed, autoloads files are up-to-date and no byte-compiled files have gone
+stale.")
+
+(provide 'core-cli)
+;;; core-cli.el ends here
diff --git a/core/core-editor.el b/core/core-editor.el
index 96de4e8ab..e877da0d3 100644
--- a/core/core-editor.el
+++ b/core/core-editor.el
@@ -1,16 +1,27 @@
;;; core-editor.el -*- lexical-binding: t; -*-
-(defvar doom-large-file-size 1
+(defvar doom-large-file-size 2
"Size (in MB) above which the user will be prompted to open the file literally
to avoid performance issues. Opening literally means that no major or minor
modes are active and the buffer is read-only.")
(defvar doom-large-file-modes-list
- '(archive-mode tar-mode jka-compr git-commit-mode image-mode
- doc-view-mode doc-view-mode-maybe ebrowse-tree-mode pdf-view-mode)
+ '(fundamental-mode special-mode archive-mode tar-mode jka-compr
+ git-commit-mode image-mode doc-view-mode doc-view-mode-maybe
+ ebrowse-tree-mode pdf-view-mode tags-table-mode)
"Major modes that `doom|check-large-file' will ignore.")
+(defvar-local doom-inhibit-indent-detection nil
+ "A buffer-local flag that indicates whether `dtrt-indent' should try to detect
+indentation settings or not. This should be set by editorconfig if it
+successfully sets indent_style/indent_size.")
+
+(defvar doom-detect-indentation-excluded-modes '(fundamental-mode)
+ "A list of major modes in which indentation should be automatically
+detected.")
+
(setq-default
+ large-file-warning-threshold 15000000
vc-follow-symlinks t
;; Save clipboard contents into kill-ring before replacing them
save-interprogram-paste-before-kill t
@@ -23,10 +34,10 @@ modes are active and the buffer is read-only.")
sentence-end-double-space nil
word-wrap t
;; Scrolling
- hscroll-margin 1
+ hscroll-margin 2
hscroll-step 1
scroll-conservatively 1001
- scroll-margin 0
+ scroll-margin 2
scroll-preserve-screen-position t
;; Whitespace (see `editorconfig')
indent-tabs-mode nil
@@ -36,208 +47,248 @@ modes are active and the buffer is read-only.")
tabify-regexp "^\t* [ \t]+" ; for :retab
;; Wrapping
truncate-lines t
- truncate-partial-width-windows 50
- ;; whitespace-mode
- whitespace-line-column fill-column
- whitespace-style
- '(face indentation tabs tab-mark spaces space-mark newline newline-mark
- trailing lines-tail)
- whitespace-display-mappings
- '((tab-mark ?\t [?› ?\t])
- (newline-mark ?\n [?¬ ?\n])
- (space-mark ?\ [?·] [?.])))
+ truncate-partial-width-windows 50)
-;; ediff
-(setq ediff-diff-options "-w"
- ediff-split-window-function #'split-window-horizontally
- ediff-window-setup-function #'ediff-setup-windows-plain)
+;; Remove hscroll-margin in shells, otherwise it causes jumpiness
+(setq-hook! '(eshell-mode-hook term-mode-hook) hscroll-margin 0)
-(defun doom|dont-kill-scratch-buffer ()
- "Don't kill the scratch buffer."
- (or (not (string= (buffer-name) "*scratch*"))
- (ignore (bury-buffer))))
-(add-hook 'kill-buffer-query-functions #'doom|dont-kill-scratch-buffer)
-
-;; temporary windows often have q bound to `quit-window', which only buries the
-;; contained buffer. I rarely don't want that buffer killed, so...
-(defun doom*quit-window (orig-fn &optional kill window)
- (funcall orig-fn (not kill) window))
-(advice-add #'quit-window :around #'doom*quit-window)
-
-(defun doom|check-large-file ()
- "Check if the buffer's file is large (see `doom-large-file-size'). If so, ask
-for confirmation to open it literally (read-only, disabled undo and in
-fundamental-mode) for performance sake."
- (let* ((filename (buffer-file-name))
- (size (nth 7 (file-attributes filename))))
- (when (and (not (memq major-mode doom-large-file-modes-list))
- size (> size (* 1024 1024 doom-large-file-size))
- (y-or-n-p
- (format (concat "%s is a large file, open literally to "
- "avoid performance issues?")
- (file-relative-name filename))))
+(defun doom*optimize-literal-mode-for-large-files (buffer)
+ (with-current-buffer buffer
+ (when find-file-literally
(setq buffer-read-only t)
- (buffer-disable-undo)
- (fundamental-mode))))
-(add-hook 'find-file-hook #'doom|check-large-file)
+ (buffer-disable-undo))
+ buffer))
+(advice-add #'find-file-noselect-1 :filter-return #'doom*optimize-literal-mode-for-large-files)
-(push '("/LICENSE$" . text-mode) auto-mode-alist)
+
+;;
+;; Extra file extensions to support
+
+(push '("/LICENSE\\'" . text-mode) auto-mode-alist)
;;
;; Built-in plugins
-;;
-;; revert buffers for changed files
-(global-auto-revert-mode 1)
-(setq auto-revert-verbose nil)
+(def-package! server
+ :when (display-graphic-p)
+ :after-call (pre-command-hook after-find-file)
+ :config
+ (when-let* ((name (getenv "EMACS_SERVER_NAME")))
+ (setq server-name name))
+ (unless (server-running-p)
+ (server-start)))
-;; enabled by default in Emacs 25+. No thanks.
-(electric-indent-mode -1)
+(def-package! autorevert
+ ;; revert buffers for changed files
+ :after-call after-find-file
+ :config
+ (setq auto-revert-verbose nil)
+ (global-auto-revert-mode +1))
-;; savehist / saveplace
-(setq savehist-file (concat doom-cache-dir "savehist")
- savehist-save-minibuffer-history t
- savehist-autosave-interval nil ; save on kill only
- savehist-additional-variables '(kill-ring search-ring regexp-search-ring)
- save-place-file (concat doom-cache-dir "saveplace"))
-(add-hook! 'doom-init-hook #'(savehist-mode save-place-mode))
+(def-package! savehist
+ ;; persist variables across sessions
+ :defer-incrementally (custom)
+ :after-call post-command-hook
+ :config
+ (setq savehist-file (concat doom-cache-dir "savehist")
+ savehist-save-minibuffer-history t
+ savehist-autosave-interval nil ; save on kill only
+ savehist-additional-variables '(kill-ring search-ring regexp-search-ring))
+ (savehist-mode +1)
+
+ (defun doom|unpropertize-kill-ring ()
+ "Remove text properties from `kill-ring' in the interest of shrinking the
+savehist file."
+ (setq kill-ring (cl-loop for item in kill-ring
+ if (stringp item)
+ collect (substring-no-properties item)
+ else if item collect it)))
+ (add-hook 'kill-emacs-hook #'doom|unpropertize-kill-ring))
+
+(def-package! saveplace
+ ;; persistent point location in buffers
+ :after-call (after-find-file dired-initial-position-hook)
+ :config
+ (setq save-place-file (concat doom-cache-dir "saveplace"))
+ (defun doom*recenter-on-load-saveplace (&rest _)
+ "Recenter on cursor when loading a saved place."
+ (if buffer-file-name (ignore-errors (recenter))))
+ (advice-add #'save-place-find-file-hook
+ :after-while #'doom*recenter-on-load-saveplace)
+ (save-place-mode +1))
-;; Keep track of recently opened files
(def-package! recentf
- :hook (doom-init . recentf-mode)
+ ;; Keep track of recently opened files
+ :defer-incrementally (easymenu tree-widget timer)
+ :after-call after-find-file
+ :commands recentf-open-files
:config
(setq recentf-save-file (concat doom-cache-dir "recentf")
+ recentf-auto-cleanup 'never
recentf-max-menu-items 0
recentf-max-saved-items 300
- recentf-filename-handlers '(file-truename)
+ recentf-filename-handlers '(file-truename abbreviate-file-name)
recentf-exclude
- (list "^/tmp/" "^/ssh:" "\\.?ido\\.last$" "\\.revive$" "/TAGS$"
+ (list #'file-remote-p "\\.\\(?:gz\\|gif\\|svg\\|png\\|jpe?g\\)$"
+ "^/tmp/" "^/ssh:" "\\.?ido\\.last$" "\\.revive$" "/TAGS$"
"^/var/folders/.+$"
;; ignore private DOOM temp files (but not all of them)
- (concat "^" (file-truename doom-local-dir)))))
+ (lambda (file) (file-in-directory-p file doom-local-dir))))
+ (unless noninteractive
+ (add-hook 'kill-emacs-hook #'recentf-cleanup)
+ (quiet! (recentf-mode +1))))
;;
-;; Core Plugins
-;;
+;; Packages
-;; Handles whitespace (tabs/spaces) settings externally. This way projects can
-;; specify their own formatting rules.
-(def-package! editorconfig
- :config
- (add-hook 'doom-init-hook #'editorconfig-mode)
-
- ;; editorconfig cannot procure the correct settings for extension-less files.
- ;; Executable scripts with a shebang line, for example. So why not use Emacs'
- ;; major mode to drop editorconfig a hint? This is accomplished by temporarily
- ;; appending an extension to `buffer-file-name' when we talk to editorconfig.
- (defvar doom-editorconfig-mode-alist
- '((sh-mode . "sh")
- (python-mode . "py")
- (ruby-mode . "rb")
- (perl-mode . "pl")
- (php-mode . "php"))
- "An alist mapping major modes to extensions. Used by
-`doom*editorconfig-smart-detection' to give editorconfig filetype hints.")
-
- (defun doom*editorconfig-smart-detection (orig-fn &rest args)
- "Retrieve the properties for the current file. If it doesn't have an
-extension, try to guess one."
- (let ((buffer-file-name
- (if (file-name-extension buffer-file-name)
- buffer-file-name
- (format "%s%s" buffer-file-name
- (let ((ext (cdr (assq major-mode doom-editorconfig-mode-alist))))
- (or (and ext (concat "." ext))
- ""))))))
- (apply orig-fn args)))
- (advice-add #'editorconfig-call-editorconfig-exec :around #'doom*editorconfig-smart-detection)
-
- ;; Editorconfig makes indentation too rigid in Lisp modes, so tell
- ;; editorconfig to ignore indentation. I prefer dynamic indentation support
- ;; built into Emacs.
- (dolist (mode '(emacs-lisp-mode lisp-mode))
- (setq editorconfig-indentation-alist
- (assq-delete-all mode editorconfig-indentation-alist)))
-
- (defvar whitespace-style)
- (defun doom|editorconfig-whitespace-mode-maybe (&rest _)
- "Show whitespace-mode when file uses TABS (ew)."
- (when indent-tabs-mode
- (let ((whitespace-style '(face tabs tab-mark trailing-lines tail)))
- (whitespace-mode +1))))
- (add-hook 'editorconfig-custom-hooks #'doom|editorconfig-whitespace-mode-maybe))
-
-(def-package! editorconfig-conf-mode
- :mode "\\.?editorconfig$")
-
-;; Auto-close delimiters and blocks as you type
(def-package! smartparens
- :hook (doom-init . smartparens-global-mode)
+ ;; Auto-close delimiters and blocks as you type. It's more powerful than that,
+ ;; but that is all Doom uses it for.
+ :after-call (doom-switch-buffer-hook after-find-file)
+ :commands (sp-pair sp-local-pair sp-with-modes sp-point-in-comment sp-point-in-string)
:config
(require 'smartparens-config)
-
- (setq sp-autowrap-region nil ; let evil-surround handle this
- sp-highlight-pair-overlay nil
+ (setq sp-highlight-pair-overlay nil
+ sp-highlight-wrap-overlay nil
+ sp-highlight-wrap-tag-overlay nil
+ sp-show-pair-from-inside t
sp-cancel-autoskip-on-backward-movement nil
- sp-show-pair-delay 0
- sp-max-pair-length 3)
+ sp-show-pair-delay 0.1
+ sp-max-pair-length 4
+ sp-max-prefix-length 50
+ sp-escape-quotes-after-insert nil) ; not smart enough
- ;; disable smartparens in evil-mode's replace state (they conflict)
+ ;; Smartparens' navigation feature is neat, but does not justify how expensive
+ ;; it is. It's also less useful for evil users. This may need to be
+ ;; reactivated for non-evil users though. Needs more testing!
+ (defun doom|disable-smartparens-navigate-skip-match ()
+ (setq sp-navigate-skip-match nil
+ sp-navigate-consider-sgml-tags nil))
+ (add-hook 'after-change-major-mode-hook #'doom|disable-smartparens-navigate-skip-match)
+
+ ;; autopairing in `eval-expression' and `evil-ex'
+ (defun doom|init-smartparens-in-eval-expression ()
+ "Enable `smartparens-mode' in the minibuffer, during `eval-expression' or
+`evil-ex'."
+ (when (memq this-command '(eval-expression evil-ex))
+ (smartparens-mode)))
+ (add-hook 'minibuffer-setup-hook #'doom|init-smartparens-in-eval-expression)
+ (sp-local-pair 'minibuffer-inactive-mode "'" nil :actions nil)
+
+ ;; smartparens breaks evil-mode's replace state
(add-hook 'evil-replace-state-entry-hook #'turn-off-smartparens-mode)
(add-hook 'evil-replace-state-exit-hook #'turn-on-smartparens-mode)
- (sp-local-pair '(xml-mode nxml-mode php-mode) ""
- :post-handlers '(("| " "SPC"))))
+ (smartparens-global-mode +1))
-;; Branching undo
-(def-package! undo-tree
+
+(def-package! dtrt-indent
+ ;; Automatic detection of indent settings
+ :unless noninteractive
+ :defer t
+ :init
+ (defun doom|detect-indentation ()
+ (unless (or (not after-init-time)
+ doom-inhibit-indent-detection
+ (member (substring (buffer-name) 0 1) '(" " "*"))
+ (memq major-mode doom-detect-indentation-excluded-modes))
+ (dtrt-indent-mode +1)))
+ (add-hook! '(change-major-mode-after-body-hook read-only-mode-hook)
+ #'doom|detect-indentation)
:config
- (add-hook 'doom-init-hook #'global-undo-tree-mode)
- ;; persistent undo history is known to cause undo history corruption, which
- ;; can be very destructive! So disable it!
- (setq undo-tree-auto-save-history nil
+ (setq dtrt-indent-verbosity (if doom-debug-mode 2 0))
+ ;; always keep tab-width up-to-date
+ (push '(t tab-width) dtrt-indent-hook-generic-mapping-list)
+
+ (defvar dtrt-indent-run-after-smie)
+ (defun doom*fix-broken-smie-modes (orig-fn arg)
+ "Some smie modes throw errors when trying to guess their indentation, like
+`nim-mode'. This prevents them from leaving Emacs in a broken state."
+ (let ((dtrt-indent-run-after-smie dtrt-indent-run-after-smie))
+ (cl-letf* ((old-smie-config-guess (symbol-function 'smie-config-guess))
+ ((symbol-function 'smie-config-guess)
+ (lambda ()
+ (condition-case e (funcall old-smie-config-guess)
+ (error (setq dtrt-indent-run-after-smie t)
+ (message "[WARNING] Indent detection: %s"
+ (error-message-string e))
+ (message "")))))) ; warn silently
+ (funcall orig-fn arg))))
+ (advice-add #'dtrt-indent-mode :around #'doom*fix-broken-smie-modes))
+
+
+(def-package! undo-tree
+ ;; Branching & persistent undo
+ :after-call (doom-switch-buffer-hook after-find-file)
+ :config
+ (setq undo-tree-auto-save-history t
+ ;; undo-in-region is known to cause undo history corruption, which can
+ ;; be very destructive! Disabling it deters the error, but does not fix
+ ;; it entirely!
+ undo-tree-enable-undo-in-region nil
undo-tree-history-directory-alist
- (list (cons "." (concat doom-cache-dir "undo-tree-hist/")))))
+ `(("." . ,(concat doom-cache-dir "undo-tree-hist/"))))
+ (global-undo-tree-mode +1)
+ ;; compress undo history with xz/gzip
+ (and (fset 'doom*undo-tree-make-history-save-file-name
+ (cond ((executable-find "zstd") (lambda (file) (concat file ".zst")))
+ ((executable-find "gzip") (lambda (file) (concat file ".gz")))))
+ (advice-add #'undo-tree-make-history-save-file-name :filter-return
+ #'doom*undo-tree-make-history-save-file-name))
-;;
-;; Autoloaded Plugins
-;;
+ (defun doom*strip-text-properties-from-undo-history (&rest _)
+ (dolist (item buffer-undo-list)
+ (and (consp item)
+ (stringp (car item))
+ (setcar item (substring-no-properties (car item))))))
+ (advice-add #'undo-list-transfer-to-tree :before #'doom*strip-text-properties-from-undo-history))
-(def-package! ace-link
- :commands (ace-link-help ace-link-org))
-
-(def-package! avy
- :commands (avy-goto-char-2 avy-goto-line)
- :config
- (setq avy-all-windows nil
- avy-background t))
(def-package! command-log-mode
- :commands (command-log-mode global-command-log-mode)
+ :commands global-command-log-mode
:config
- (set! :popup "*command-log*" :size 40 :align 'right :noselect t)
(setq command-log-mode-auto-show t
- command-log-mode-open-log-turns-on-mode t))
+ command-log-mode-open-log-turns-on-mode nil
+ command-log-mode-is-global t
+ command-log-mode-window-size 50))
-(def-package! expand-region
- :commands (er/expand-region er/contract-region er/mark-symbol er/mark-word))
-(def-package! help-fns+ ; Improved help commands
- :commands (describe-buffer describe-command describe-file
- describe-keymap describe-option describe-option-of-type))
+;; `helpful' --- a better *help* buffer
+(def-package! helpful
+ :commands helpful--read-symbol
+ :init
+ (define-key!
+ [remap describe-function] #'helpful-callable
+ [remap describe-command] #'helpful-command
+ [remap describe-variable] #'helpful-variable
+ [remap describe-key] #'helpful-key
+ [remap describe-symbol] #'doom/describe-symbol)
-(def-package! pcre2el
- :commands rxt-quote-pcre)
+ (after! apropos
+ ;; patch apropos buttons to call helpful instead of help
+ (dolist (fun-bt '(apropos-function apropos-macro apropos-command))
+ (button-type-put
+ fun-bt 'action
+ (lambda (button)
+ (helpful-callable (button-get button 'apropos-symbol)))))
+ (dolist (var-bt '(apropos-variable apropos-user-option))
+ (button-type-put
+ var-bt 'action
+ (lambda (button)
+ (helpful-variable (button-get button 'apropos-symbol)))))))
-(def-package! smart-forward
- :commands (smart-up smart-down smart-backward smart-forward))
-(def-package! wgrep
- :commands (wgrep-setup wgrep-change-to-wgrep-mode)
- :config (setq wgrep-auto-save-buffer t))
+(def-package! ws-butler
+ ;; a less intrusive `delete-trailing-whitespaces' on save
+ :after-call (after-find-file)
+ :config
+ (setq ws-butler-global-exempt-modes
+ (append ws-butler-global-exempt-modes
+ '(special-mode comint-mode term-mode eshell-mode)))
+ (ws-butler-global-mode))
(provide 'core-editor)
;;; core-editor.el ends here
diff --git a/core/core-keybinds.el b/core/core-keybinds.el
index 0b1310017..dee061219 100644
--- a/core/core-keybinds.el
+++ b/core/core-keybinds.el
@@ -2,13 +2,210 @@
;; A centralized keybinds system, integrated with `which-key' to preview
;; available keybindings. All built into one powerful macro: `map!'. If evil is
-;; never loaded, then evil bindings set with `map!' will be ignored.
+;; never loaded, then evil bindings set with `map!' are ignored (i.e. omitted
+;; entirely for performance reasons).
(defvar doom-leader-key "SPC"
- "The leader prefix key, for global commands.")
+ "The leader prefix key for Evil users.
+
+This needs to be changed from $DOOMDIR/init.el.")
+
+(defvar doom-leader-alt-key "M-SPC"
+ "An alternative leader prefix key, used for Insert and Emacs states, and for
+non-evil users.
+
+This needs to be changed from $DOOMDIR/init.el.")
(defvar doom-localleader-key "SPC m"
- "The localleader prefix key, for major-mode specific commands.")
+ "The localleader prefix key, for major-mode specific commands.
+
+This needs to be changed from $DOOMDIR/init.el.")
+
+(defvar doom-localleader-alt-key "M-SPC m"
+ "The localleader prefix key, for major-mode specific commands. Used for Insert
+and Emacs states, and for non-evil users.
+
+This needs to be changed from $DOOMDIR/init.el.")
+
+(defvar doom-leader-map (make-sparse-keymap)
+ "An overriding keymap for keys.")
+
+(defvar doom-which-key-leader-prefix-regexp nil)
+
+
+;;
+;;; Universal, non-nuclear escape
+
+;; `keyboard-quit' is too much of a nuclear option. I wanted an ESC/C-g to
+;; do-what-I-mean. It serves four purposes (in order):
+;;
+;; 1. Quit active states; e.g. highlights, searches, snippets, iedit,
+;; multiple-cursors, recording macros, etc.
+;; 2. Close popup windows remotely (if it is allowed to)
+;; 3. Refresh buffer indicators, like git-gutter and flycheck
+;; 4. Or fall back to `keyboard-quit'
+;;
+;; And it should do these things incrementally, rather than all at once. And it
+;; shouldn't interfere with recording macros or the minibuffer. This may require
+;; you press ESC/C-g two or three times on some occasions to reach
+;; `keyboard-quit', but this is much more intuitive.
+
+(defvar doom-escape-hook nil
+ "A hook run after C-g is pressed (or ESC in normal mode, for evil users). Both
+trigger `doom/escape'.
+
+If any hook returns non-nil, all hooks after it are ignored.")
+
+(defun doom/escape ()
+ "Run `doom-escape-hook'."
+ (interactive)
+ (cond ((minibuffer-window-active-p (minibuffer-window))
+ ;; quit the minibuffer if open.
+ (abort-recursive-edit))
+ ;; Run all escape hooks. If any returns non-nil, then stop there.
+ ((run-hook-with-args-until-success 'doom-escape-hook))
+ ;; don't abort macros
+ ((or defining-kbd-macro executing-kbd-macro) nil)
+ ;; Back to the default
+ ((keyboard-quit))))
+
+(global-set-key [remap keyboard-quit] #'doom/escape)
+
+
+;;
+;;; General + leader/localleader keys
+
+(require 'general)
+;; Convenience aliases
+(defalias 'define-key! #'general-def)
+(defalias 'unmap! #'general-unbind)
+
+;; `map!' uses this instead of `define-leader-key!' because it consumes 20-30%
+;; more startup time, so we reimplement it ourselves.
+(defmacro doom--define-leader-key (&rest keys)
+ (let (prefix forms wkforms)
+ (while keys
+ (let ((key (pop keys))
+ (def (pop keys)))
+ (if (keywordp key)
+ (when (memq key '(:prefix :infix))
+ (setq prefix def))
+ (when prefix
+ (setq key `(general--concat t ,prefix ,key)))
+ (let* ((udef (cdr-safe (doom-unquote def)))
+ (bdef (if (general--extended-def-p udef)
+ (general--extract-def (general--normalize-extended-def udef))
+ def)))
+ (unless (eq bdef :ignore)
+ (push `(define-key doom-leader-map (general--kbd ,key)
+ ,bdef)
+ forms))
+ (when-let* ((desc (plist-get udef :which-key)))
+ (push `(which-key-add-key-based-replacements
+ (general--concat t doom-leader-alt-key ,key)
+ ,desc)
+ wkforms)
+ (push `(which-key-add-key-based-replacements
+ (general--concat t doom-leader-key ,key)
+ ,desc)
+ wkforms))))))
+ (macroexp-progn
+ (append (nreverse forms)
+ (when wkforms
+ `((after! which-key
+ ,@(nreverse wkforms))))))))
+
+(defmacro define-leader-key! (&rest args)
+ "Define keys.
+
+Uses `general-define-key' under the hood, but does not support :states,
+:wk-full-keys or :keymaps. Use `map!' for a more convenient interface.
+
+See `doom-leader-key' and `doom-leader-alt-key' to change the leader prefix."
+ `(general-define-key
+ :states nil
+ :wk-full-keys nil
+ :keymaps 'doom-leader-map
+ ,@args))
+
+(defmacro define-localleader-key! (&rest args)
+ "Define key.
+
+Uses `general-define-key' under the hood, but does not support :major-modes,
+:states, :prefix or :non-normal-prefix. Use `map!' for a more convenient
+interface.
+
+See `doom-localleader-key' and `doom-localleader-alt-key' to change the
+localleader prefix."
+ (if (featurep 'evil)
+ ;; :non-normal-prefix doesn't apply to non-evil sessions (only evil's
+ ;; emacs state)
+ `(general-define-key
+ :states '(normal visual motion emacs)
+ :major-modes t
+ :prefix doom-localleader-key
+ :non-normal-prefix doom-localleader-alt-key
+ ,@args)
+ `(general-define-key
+ :major-modes t
+ :prefix doom-localleader-alt-key
+ ,@args)))
+
+;; We use a prefix commands instead of general's :prefix/:non-normal-prefix
+;; properties because general is incredibly slow binding keys en mass with them
+;; in conjunction with :states -- an effective doubling of Doom's startup time!
+(define-prefix-command 'doom/leader 'doom-leader-map)
+(define-key doom-leader-map [override-state] 'all)
+
+;; Bind `doom-leader-key' and `doom-leader-alt-key' as late as possible to give
+;; the user a chance to modify them.
+(defun doom|init-leader-keys ()
+ "Bind `doom-leader-key' and `doom-leader-alt-key'."
+ (let ((map general-override-mode-map))
+ (if (not (featurep 'evil))
+ (define-key map (kbd doom-leader-alt-key) 'doom/leader)
+ (evil-define-key* '(normal visual motion) map (kbd doom-leader-key) 'doom/leader)
+ (evil-define-key* '(emacs insert) map (kbd doom-leader-alt-key) 'doom/leader))
+ (general-override-mode +1))
+ (unless (stringp doom-which-key-leader-prefix-regexp)
+ (setq doom-which-key-leader-prefix-regexp
+ (concat "\\(?:"
+ (cl-loop for key in (append (list doom-leader-key doom-leader-alt-key)
+ (where-is-internal 'doom/leader))
+ for keystr = (if (stringp key) key (key-description key))
+ collect (regexp-quote keystr) into keys
+ finally return (string-join keys "\\|"))
+ "\\)"))))
+(add-hook 'doom-after-init-modules-hook #'doom|init-leader-keys)
+
+
+;;
+;;; Packages
+
+(def-package! which-key
+ :defer 1
+ :after-call pre-command-hook
+ :init
+ (setq which-key-sort-order #'which-key-prefix-then-key-order
+ which-key-sort-uppercase-first nil
+ which-key-add-column-padding 1
+ which-key-max-display-columns nil
+ which-key-min-display-lines 6
+ which-key-side-window-slot -10)
+ :config
+ ;; general improvements to which-key readability
+ (set-face-attribute 'which-key-local-map-description-face nil :weight 'bold)
+ (which-key-setup-side-window-bottom)
+ (setq-hook! 'which-key-init-buffer-hook line-spacing 3)
+ (which-key-mode +1))
+
+
+;;;###package hydra
+(setq lv-use-seperator t)
+
+
+;;
+;;; `map!' macro
(defvar doom-evil-state-alist
'((?n . normal)
@@ -21,87 +218,6 @@
(?g . global))
"A list of cons cells that map a letter to a evil state symbol.")
-
-;;
-(def-package! which-key
- :config
- (setq which-key-sort-order #'which-key-prefix-then-key-order
- which-key-sort-uppercase-first nil
- which-key-add-column-padding 1
- which-key-max-display-columns nil
- which-key-min-display-lines 5)
- ;; embolden local bindings
- (set-face-attribute 'which-key-local-map-description-face nil :weight 'bold)
- (which-key-setup-side-window-bottom)
- (add-hook 'doom-init-hook #'which-key-mode))
-
-
-(def-package! hydra
- :init
- ;; In case I later need to wrap defhydra in any special functionality.
- (defalias 'def-hydra! 'defhydra)
- (defalias 'def-hydra-radio! 'defhydradio)
- :config
- (setq lv-use-seperator t)
-
- (def-hydra! doom@text-zoom (:hint t :color red)
- "
- Text zoom: _j_:zoom in, _k_:zoom out, _0_:reset
-"
- ("j" text-scale-increase "in")
- ("k" text-scale-decrease "out")
- ("0" (text-scale-set 0) "reset"))
-
- (def-hydra! doom@window-nav (:hint nil)
- "
- Split: _v_ert _s_:horz
- Delete: _c_lose _o_nly
- Switch Window: _h_:left _j_:down _k_:up _l_:right
- Buffers: _p_revious _n_ext _b_:select _f_ind-file
- Resize: _H_:splitter left _J_:splitter down _K_:splitter up _L_:splitter right
- Move: _a_:up _z_:down _i_menu
-"
- ("z" scroll-up-line)
- ("a" scroll-down-line)
- ("i" idomenu)
-
- ("h" windmove-left)
- ("j" windmove-down)
- ("k" windmove-up)
- ("l" windmove-right)
-
- ("p" doom/previous-buffer)
- ("n" doom/next-buffer)
- ("b" switch-to-buffer)
- ("f" find-file)
-
- ("s" split-window-below)
- ("v" split-window-right)
-
- ("c" delete-window)
- ("o" delete-other-windows)
-
- ("H" hydra-move-splitter-left)
- ("J" hydra-move-splitter-down)
- ("K" hydra-move-splitter-up)
- ("L" hydra-move-splitter-right)
-
- ("q" nil)))
-
-
-;;
-(defun doom--keybind-register (key desc &optional modes)
- "Register a description for KEY with `which-key' in MODES.
-
- KEYS should be a string in kbd format.
- DESC should be a string describing what KEY does.
- MODES should be a list of major mode symbols."
- (if modes
- (dolist (mode modes)
- (which-key-add-major-mode-key-based-replacements mode key desc))
- (which-key-add-key-based-replacements key desc)))
-
-
(defun doom--keyword-to-states (keyword)
"Convert a KEYWORD into a list of evil state symbols.
@@ -113,188 +229,203 @@ For example, :nvi will map to (list 'normal 'visual 'insert). See
;; Register keywords for proper indentation (see `map!')
-(put ':after 'lisp-indent-function 'defun)
-(put ':desc 'lisp-indent-function 'defun)
-(put ':leader 'lisp-indent-function 'defun)
-(put ':local 'lisp-indent-function 'defun)
-(put ':localleader 'lisp-indent-function 'defun)
-(put ':map 'lisp-indent-function 'defun)
-(put ':map* 'lisp-indent-function 'defun)
-(put ':mode 'lisp-indent-function 'defun)
-(put ':prefix 'lisp-indent-function 'defun)
-(put ':textobj 'lisp-indent-function 'defun)
-(put ':unless 'lisp-indent-function 'defun)
-(put ':when 'lisp-indent-function 'defun)
+(put :after 'lisp-indent-function 'defun)
+(put :desc 'lisp-indent-function 'defun)
+(put :leader 'lisp-indent-function 'defun)
+(put :localleader 'lisp-indent-function 'defun)
+(put :map 'lisp-indent-function 'defun)
+(put :keymap 'lisp-indent-function 'defun)
+(put :mode 'lisp-indent-function 'defun)
+(put :prefix 'lisp-indent-function 'defun)
+(put :unless 'lisp-indent-function 'defun)
+(put :when 'lisp-indent-function 'defun)
;; specials
-(defvar doom--keymaps nil)
-(defvar doom--prefix nil)
-(defvar doom--defer nil)
-(defvar doom--local nil)
+(defvar doom--map-forms nil)
+(defvar doom--map-fn nil)
+(defvar doom--map-batch-forms nil)
+(defvar doom--map-state '(:dummy t))
+(defvar doom--map-parent-state nil)
+(defvar doom--map-evil-p nil)
+(after! evil (setq doom--map-evil-p t))
+(defun doom--map-process (rest)
+ (let ((doom--map-fn doom--map-fn)
+ doom--map-state
+ doom--map-forms
+ desc)
+ (while rest
+ (let ((key (pop rest)))
+ (cond ((listp key)
+ (doom--map-nested nil key))
+
+ ((keywordp key)
+ (pcase key
+ (:leader
+ (doom--map-commit)
+ (setq doom--map-fn 'doom--define-leader-key))
+ (:localleader
+ (doom--map-commit)
+ (setq doom--map-fn 'define-localleader-key!))
+ (:after
+ (doom--map-nested (list 'after! (pop rest)) rest)
+ (setq rest nil))
+ (:desc
+ (setq desc (pop rest)))
+ ((or :map :map* :keymap)
+ (doom--map-set :keymaps `(quote ,(doom-enlist (pop rest)))))
+ (:mode
+ (push (cl-loop for m in (doom-enlist (pop rest))
+ collect (intern (concat (symbol-name m) "-map")))
+ rest)
+ (push :map rest))
+ ((or :when :unless)
+ (doom--map-nested (list (intern (doom-keyword-name key)) (pop rest)) rest)
+ (setq rest nil))
+ (:prefix
+ (cl-destructuring-bind (prefix . desc) (doom-enlist (pop rest))
+ (doom--map-set (if doom--map-fn :infix :prefix)
+ prefix)
+ (when (stringp desc)
+ (setq rest (append (list :desc desc "" nil) rest)))))
+ (:textobj
+ (let* ((key (pop rest))
+ (inner (pop rest))
+ (outer (pop rest)))
+ (push `(map! (:map evil-inner-text-objects-map ,key ,inner)
+ (:map evil-outer-text-objects-map ,key ,outer))
+ doom--map-forms)))
+ (_
+ (condition-case _
+ (doom--map-def (pop rest) (pop rest) (doom--keyword-to-states key) desc)
+ (error
+ (error "Not a valid `map!' property: %s" key)))
+ (setq desc nil))))
+
+ ((doom--map-def key (pop rest) nil desc)
+ (setq desc nil)))))
+
+ (doom--map-commit)
+ (macroexp-progn (nreverse (delq nil doom--map-forms)))))
+
+(defun doom--map-append-keys (prop)
+ (let ((a (plist-get doom--map-parent-state prop))
+ (b (plist-get doom--map-state prop)))
+ (if (and a b)
+ `(general--concat nil ,a ,b)
+ (or a b))))
+
+(defun doom--map-nested (wrapper rest)
+ (doom--map-commit)
+ (let ((doom--map-parent-state (doom--map-state)))
+ (push (if wrapper
+ (append wrapper (list (doom--map-process rest)))
+ (doom--map-process rest))
+ doom--map-forms)))
+
+(defun doom--map-set (prop &optional value)
+ (unless (equal (plist-get doom--map-state prop) value)
+ (doom--map-commit))
+ (setq doom--map-state (plist-put doom--map-state prop value)))
+
+(defun doom--map-def (key def &optional states desc)
+ (when (or (memq 'global states)
+ (null states))
+ (setq states (cons 'nil (delq 'global states))))
+ (when desc
+ (let (unquoted)
+ (cond ((and (listp def)
+ (keywordp (car-safe (setq unquoted (doom-unquote def)))))
+ (setq def (list 'quote (plist-put unquoted :which-key desc))))
+ ((setq def (cons 'list
+ (if (and (equal key "")
+ (null def))
+ `(:ignore t :which-key ,desc)
+ (plist-put (general--normalize-extended-def def)
+ :which-key desc))))))))
+ (dolist (state states)
+ (push (list key def)
+ (alist-get state doom--map-batch-forms)))
+ t)
+
+(defun doom--map-commit ()
+ (when doom--map-batch-forms
+ (cl-loop with attrs = (doom--map-state)
+ for (state . defs) in doom--map-batch-forms
+ if (or doom--map-evil-p (not state))
+ collect `(,(or doom--map-fn 'general-define-key)
+ ,@(if state `(:states ',state)) ,@attrs
+ ,@(mapcan #'identity (nreverse defs)))
+ into forms
+ finally do (push (macroexp-progn forms) doom--map-forms))
+ (setq doom--map-batch-forms nil)))
+
+(defun doom--map-state ()
+ (let ((plist
+ (append (list :prefix (doom--map-append-keys :prefix)
+ :infix (doom--map-append-keys :infix)
+ :keymaps
+ (append (plist-get doom--map-parent-state :keymaps)
+ (plist-get doom--map-state :keymaps)))
+ doom--map-state
+ nil))
+ newplist)
+ (while plist
+ (let ((key (pop plist))
+ (val (pop plist)))
+ (when (and val (not (plist-member newplist key)))
+ (push val newplist)
+ (push key newplist))))
+ newplist))
+
+;;
(defmacro map! (&rest rest)
- "A nightmare of a key-binding macro that will use `evil-define-key*',
-`define-key', `local-set-key' and `global-set-key' depending on context and
-plist key flags (and whether evil is loaded or not). It was designed to make
-binding multiple keys more concise, like in vim.
+ "A convenience macro for defining keybinds, powered by `general'.
-If evil isn't loaded, it will ignore evil-specific bindings.
+If evil isn't loaded, evil-specific bindings are ignored.
States
- :n normal
- :v visual
- :i insert
- :e emacs
- :o operator
- :m motion
- :r replace
+ :n normal
+ :v visual
+ :i insert
+ :e emacs
+ :o operator
+ :m motion
+ :r replace
+ :g global (binds the key without evil `current-global-map')
- These can be combined (order doesn't matter), e.g. :nvi will apply to
- normal, visual and insert mode. The state resets after the following
- key=>def pair.
+ These can be combined in any order, e.g. :nvi will apply to normal, visual and
+ insert mode. The state resets after the following key=>def pair. If states are
+ omitted the keybind will be global (no emacs state; this is different from
+ evil's Emacs state and will work in the absence of `evil-mode').
- If states are omitted the keybind will be global.
+Properties
+ :leader [...] an alias for (:prefix doom-leader-key ...)
+ :localleader [...] bind to localleader; requires a keymap
+ :mode [MODE(s)] [...] inner keybinds are applied to major MODE(s)
+ :map [KEYMAP(s)] [...] inner keybinds are applied to KEYMAP(S)
+ :keymap [KEYMAP(s)] [...] same as :map
+ :prefix [PREFIX] [...] set keybind prefix for following keys
+ :after [FEATURE] [...] apply keybinds when [FEATURE] loads
+ :textobj KEY INNER-FN OUTER-FN define a text object keybind pair
+ :if [CONDITION] [...]
+ :when [CONDITION] [...]
+ :unless [CONDITION] [...]
- This can be customized with `doom-evil-state-alist'.
-
- :textobj is a special state that takes a key and two commands, one for the
- inner binding, another for the outer.
-
-Flags
- (:mode [MODE(s)] [...]) inner keybinds are applied to major MODE(s)
- (:map [KEYMAP(s)] [...]) inner keybinds are applied to KEYMAP(S)
- (:map* [KEYMAP(s)] [...]) same as :map, but deferred
- (:prefix [PREFIX] [...]) assign prefix to all inner keybindings
- (:after [FEATURE] [...]) apply keybinds when [FEATURE] loads
- (:local [...]) make bindings buffer local; incompatible with keymaps!
-
-Conditional keybinds
- (:when [CONDITION] [...])
- (:unless [CONDITION] [...])
+ Any of the above properties may be nested, so that they only apply to a
+ certain group of keybinds.
Example
- (map! :map magit-mode-map
- :m \"C-r\" 'do-something ; assign C-r in motion state
- :nv \"q\" 'magit-mode-quit-window ; assign to 'q' in normal and visual states
- \"C-x C-r\" 'a-global-keybind
+ (map! :map magit-mode-map
+ :m \"C-r\" 'do-something ; C-r in motion state
+ :nv \"q\" 'magit-mode-quit-window ; q in normal+visual states
+ \"C-x C-r\" 'a-global-keybind
+ :g \"C-x C-r\" 'another-global-keybind ; same as above
- (:when IS-MAC
- :n \"M-s\" 'some-fn
- :i \"M-o\" (lambda (interactive) (message \"Hi\"))))"
- (let ((doom--keymaps doom--keymaps)
- (doom--prefix doom--prefix)
- (doom--defer doom--defer)
- (doom--local doom--local)
- key def states forms desc modes)
- (while rest
- (setq key (pop rest))
- (cond
- ;; it's a sub expr
- ((listp key)
- (push (macroexpand `(map! ,@key)) forms))
-
- ;; it's a flag
- ((keywordp key)
- (cond ((eq key :leader)
- (push 'doom-leader-key rest)
- (setq key :prefix
- desc ""))
- ((eq key :localleader)
- (push 'doom-localleader-key rest)
- (setq key :prefix
- desc "")))
- (pcase key
- (:when (push `(if ,(pop rest) ,(macroexpand `(map! ,@rest))) forms) (setq rest '()))
- (:unless (push `(if (not ,(pop rest)) ,(macroexpand `(map! ,@rest))) forms) (setq rest '()))
- (:after (push `(after! ,(pop rest) ,(macroexpand `(map! ,@rest))) forms) (setq rest '()))
- (:desc (setq desc (pop rest)))
- (:map* (setq doom--defer t) (push :map rest))
- (:map
- (setq doom--keymaps (doom-enlist (pop rest))))
- (:mode
- (setq modes (doom-enlist (pop rest)))
- (unless doom--keymaps
- (setq doom--keymaps
- (cl-loop for m in modes
- collect (intern (format "%s-map" (symbol-name m)))))))
- (:textobj
- (let* ((key (pop rest))
- (inner (pop rest))
- (outer (pop rest)))
- (push (macroexpand `(map! (:map evil-inner-text-objects-map ,key ,inner)
- (:map evil-outer-text-objects-map ,key ,outer)))
- forms)))
- (:prefix
- (let ((def (pop rest)))
- (setq doom--prefix `(vconcat ,doom--prefix (kbd ,def)))
- (when desc
- (push `(doom--keybind-register ,(key-description (eval doom--prefix))
- ,desc ',modes)
- forms)
- (setq desc nil))))
- (:local
- (setq doom--local t))
- (_ ; might be a state doom--prefix
- (setq states (doom--keyword-to-states key)))))
-
- ;; It's a key-def pair
- ((or (stringp key)
- (characterp key)
- (vectorp key)
- (symbolp key))
- (unwind-protect
- (catch 'skip
- (when (symbolp key)
- (setq key `(kbd ,key)))
- (when (stringp key)
- (setq key (kbd key)))
- (when doom--prefix
- (setq key (append doom--prefix (list key))))
- (unless (> (length rest) 0)
- (user-error "map! has no definition for %s key" key))
- (setq def (pop rest))
- (when desc
- (push `(doom--keybind-register ,(key-description (eval key))
- ,desc ',modes)
- forms))
- (cond ((and doom--local doom--keymaps)
- (push `(lwarn 'doom-map :warning
- "Can't local bind '%s' key to a keymap; skipped"
- ,key)
- forms)
- (throw 'skip 'local))
- ((and doom--keymaps states)
- (unless (featurep 'evil)
- (throw 'skip 'evil))
- (dolist (keymap doom--keymaps)
- (when (memq 'global states)
- (push `(define-key ,keymap ,key ,def) forms))
- (when-let* ((states (delq 'global states)))
- (push `(,(if doom--defer 'evil-define-key 'evil-define-key*)
- ',states ,keymap ,key ,def)
- forms))))
- (states
- (unless (featurep 'evil)
- (throw 'skip 'evil))
- (dolist (state states)
- (push `(define-key
- ,(if (eq state 'global)
- '(current-global-map)
- (intern (format "evil-%s-state-%smap" state (if doom--local "local-" ""))))
- ,key ,def)
- forms)))
- (doom--keymaps
- (dolist (keymap doom--keymaps)
- (push `(define-key ,keymap ,key ,def) forms)))
- (t
- (push `(,(if doom--local 'local-set-key 'global-set-key) ,key ,def)
- forms))))
- (setq states '()
- doom--local nil
- desc nil)))
-
- (t (user-error "Invalid key %s" key))))
- `(progn ,@(nreverse forms))))
+ (:when IS-MAC
+ :n \"M-s\" 'some-fn
+ :i \"M-o\" (lambda (interactive) (message \"Hi\"))))"
+ (doom--map-process rest))
(provide 'core-keybinds)
;;; core-keybinds.el ends here
diff --git a/core/core-lib.el b/core/core-lib.el
index d812d2b3e..e02c31abf 100644
--- a/core/core-lib.el
+++ b/core/core-lib.el
@@ -1,35 +1,54 @@
;;; core-lib.el -*- lexical-binding: t; -*-
-(require 'subr-x)
-(load "async-autoloads" nil t)
-(load "persistent-soft-autoloads" nil t)
-(dolist (sym '(json-read json-read-file json-read-from-string json-encode))
- (autoload sym "json"))
-(eval-and-compile
- (when (version< emacs-version "26")
- (with-no-warnings
- (defalias 'if-let* #'if-let)
- (defalias 'when-let* #'when-let))))
-
-
-;;
-;; Helpers
;;
+;;; Helpers
-(defun doom--resolve-path-forms (paths &optional root)
- (cond ((stringp paths)
- `(file-exists-p
- (expand-file-name
- ,paths ,(if (or (string-prefix-p "./" paths)
- (string-prefix-p "../" paths))
- 'default-directory
- (or root `(doom-project-root))))))
- ((listp paths)
- (cl-loop for i in paths
- collect (doom--resolve-path-forms i root)))
- (t paths)))
+(defun doom--resolve-path-forms (spec &optional directory)
+ "Converts a simple nested series of or/and forms into a series of
+`file-exists-p' checks.
+
+For example
+
+ (doom--resolve-path-forms
+ '(or A (and B C))
+ \"~\")
+
+Returns (approximately):
+
+ '(let* ((_directory \"~\")
+ (A (expand-file-name A _directory))
+ (B (expand-file-name B _directory))
+ (C (expand-file-name C _directory)))
+ (or (and (file-exists-p A) A)
+ (and (if (file-exists-p B) B)
+ (if (file-exists-p C) C))))
+
+This is used by `associate!', `file-exists-p!' and `project-file-exists-p!'."
+ (declare (pure t) (side-effect-free t))
+ (cond ((stringp spec)
+ `(let ((--file-- ,(if (file-name-absolute-p spec)
+ spec
+ `(expand-file-name ,spec ,directory))))
+ (and (file-exists-p --file--)
+ --file--)))
+ ((and (listp spec)
+ (memq (car spec) '(or and)))
+ `(,(car spec)
+ ,@(cl-loop for i in (cdr spec)
+ collect (doom--resolve-path-forms i directory))))
+ ((or (symbolp spec)
+ (listp spec))
+ `(let ((--file-- ,(if (and directory
+ (or (not (stringp directory))
+ (file-name-absolute-p directory)))
+ `(expand-file-name ,spec ,directory)
+ spec)))
+ (and (file-exists-p --file--)
+ --file--)))
+ (spec)))
(defun doom--resolve-hook-forms (hooks)
+ (declare (pure t) (side-effect-free t))
(cl-loop with quoted-p = (eq (car-safe hooks) 'quote)
for hook in (doom-enlist (doom-unquote hooks))
if (eq (car-safe hook) 'quote)
@@ -38,151 +57,234 @@
collect hook
else collect (intern (format "%s-hook" (symbol-name hook)))))
+(defun doom--assert-stage-p (stage macro)
+ (cl-assert (eq stage doom--stage)
+ nil
+ "Found %s call in non-%s.el file (%s)"
+ macro (symbol-name stage)
+ (let ((path (FILE!)))
+ (if (file-in-directory-p path doom-emacs-dir)
+ (file-relative-name path doom-emacs-dir)
+ (abbreviate-file-name path)))))
+
+
+;;
+;;; Public library
+
(defun doom-unquote (exp)
"Return EXP unquoted."
+ (declare (pure t) (side-effect-free t))
(while (memq (car-safe exp) '(quote function))
(setq exp (cadr exp)))
exp)
(defun doom-enlist (exp)
"Return EXP wrapped in a list, or as-is if already a list."
+ (declare (pure t) (side-effect-free t))
(if (listp exp) exp (list exp)))
-(defun doom-resolve-vim-path (file-name)
- "Take a path and resolve any vim-like filename modifiers in it. On top of the
-classical vim modifiers, this adds support for:
+(defun doom-keyword-intern (str)
+ "Converts STR (a string) into a keyword (`keywordp')."
+ (declare (pure t) (side-effect-free t))
+ (cl-check-type str string)
+ (intern (concat ":" str)))
- %:P Resolves to `doom-project-root'.
+(defun doom-keyword-name (keyword)
+ "Returns the string name of KEYWORD (`keywordp') minus the leading colon."
+ (declare (pure t) (side-effect-free t))
+ (cl-check-type :test keyword)
+ (substring (symbol-name keyword) 1))
-See http://vimdoc.sourceforge.net/htmldoc/cmdline.html#filename-modifiers."
- (let* (case-fold-search
- (regexp (concat "\\(?:^\\|[^\\\\]\\)"
- "\\([#%]\\)"
- "\\(\\(?::\\(?:[PphtreS~.]\\|g?s[^:\t\n ]+\\)\\)*\\)"))
- (matches
- (cl-loop with i = 0
- while (and (< i (length file-name))
- (string-match regexp file-name i))
- do (setq i (1+ (match-beginning 0)))
- and collect
- (cl-loop for j to (/ (length (match-data)) 2)
- collect (match-string j file-name)))))
- (dolist (match matches)
- (let ((flags (split-string (car (cdr (cdr match))) ":" t))
- (path (and buffer-file-name
- (pcase (car (cdr match))
- ("%" (file-relative-name buffer-file-name))
- ("#" (save-excursion (other-window 1) (file-relative-name buffer-file-name))))))
- flag global)
- (if (not path)
- (setq path "")
- (while flags
- (setq flag (pop flags))
- (when (string-suffix-p "\\" flag)
- (setq flag (concat flag (pop flags))))
- (when (string-prefix-p "gs" flag)
- (setq global t
- flag (substring flag 1)))
- (setq path
- (or (pcase (substring flag 0 1)
- ("p" (expand-file-name path))
- ("~" (concat "~/" (file-relative-name path "~")))
- ("." (file-relative-name path default-directory))
- ("t" (file-name-nondirectory (directory-file-name path)))
- ("r" (file-name-sans-extension path))
- ("e" (file-name-extension path))
- ("S" (shell-quote-argument path))
- ("h"
- (let ((parent (file-name-directory (expand-file-name path))))
- (unless (equal (file-truename path)
- (file-truename parent))
- (if (file-name-absolute-p path)
- (directory-file-name parent)
- (file-relative-name parent)))))
- ("s"
- (if (featurep 'evil)
- (when-let* ((args (evil-delimited-arguments (substring flag 1) 2)))
- (let ((pattern (evil-transform-vim-style-regexp (car args)))
- (replace (cadr args)))
- (replace-regexp-in-string
- (if global pattern (concat "\\(" pattern "\\).*\\'"))
- (evil-transform-vim-style-regexp replace) path t t
- (unless global 1))))
- path))
- ("P"
- (let ((default-directory (file-name-directory (expand-file-name path))))
- (abbreviate-file-name (doom-project-root))))
- (_ path))
- "")))
- ;; strip trailing slash, if applicable
- (when (and (not (string= path "")) (equal (substring path -1) "/"))
- (setq path (substring path 0 -1))))
- (setq file-name
- (replace-regexp-in-string (format "\\(?:^\\|[^\\\\]\\)\\(%s\\)"
- (regexp-quote (string-trim-left (car match))))
- path file-name t t 1))))
- (replace-regexp-in-string regexp "\\1" file-name t)))
+(defmacro doom-log (format-string &rest args)
+ "Log to *Messages* if `doom-debug-mode' is on.
+Does not interrupt the minibuffer if it is in use, but still logs to *Messages*.
+Accepts the same arguments as `message'."
+ `(when doom-debug-mode
+ (let ((inhibit-message (active-minibuffer-window)))
+ (message
+ ,(concat (propertize "DOOM " 'face 'font-lock-comment-face)
+ format-string
+ (when doom--current-module
+ (propertize
+ (format " [%s/%s]"
+ (doom-keyword-name (car doom--current-module))
+ (cdr doom--current-module))
+ 'face 'warning)))
+ ,@args))))
+
+(defun FILE! ()
+ "Return the emacs lisp file this macro is called from."
+ (cond ((bound-and-true-p byte-compile-current-file))
+ (load-file-name)
+ (buffer-file-name)
+ ((stringp (car-safe current-load-list)) (car current-load-list))))
+
+(defun DIR! ()
+ "Returns the directory of the emacs lisp file this macro is called from."
+ (let ((file (FILE!)))
+ (and file (file-name-directory file))))
;;
-;; Library
-;;
+;; Macros
(defmacro λ! (&rest body)
"A shortcut for inline interactive lambdas."
(declare (doc-string 1))
`(lambda () (interactive) ,@body))
-(defmacro after! (feature &rest forms)
- "A smart wrapper around `with-eval-after-load'. Supresses warnings during
-compilation."
+(defalias 'lambda! 'λ!)
+
+(defmacro pushnew! (place &rest values)
+ "Like `cl-pushnew', but will prepend VALUES to PLACE.
+The order VALUES is preserved."
+ `(dolist (--value-- (nreverse (list ,@values)))
+ (cl-pushnew --value-- ,place)))
+
+(defmacro delq! (elt list &optional fetcher)
+ "Delete ELT from LIST in-place."
+ `(setq ,list
+ (delq ,(if fetcher
+ `(funcall ,fetcher ,elt ,list)
+ elt)
+ ,list)))
+
+(defmacro defer-until! (condition &rest body)
+ "Run BODY when CONDITION is true (checks on `after-load-functions'). Meant to
+serve as a predicated alternative to `after!'."
(declare (indent defun) (debug t))
- `(,(if (or (not (bound-and-true-p byte-compile-current-file))
- (if (symbolp feature)
- (require feature nil :no-error)
- (load feature :no-message :no-error)))
- #'progn
- #'with-no-warnings)
- (with-eval-after-load ',feature ,@forms)))
+ `(if ,condition
+ (progn ,@body)
+ ,(let ((fun (make-symbol "doom|delay-form-")))
+ `(progn
+ (fset ',fun (lambda (&rest args)
+ (when ,(or condition t)
+ (remove-hook 'after-load-functions #',fun)
+ (unintern ',fun nil)
+ (ignore args)
+ ,@body)))
+ (put ',fun 'permanent-local-hook t)
+ (add-hook 'after-load-functions #',fun)))))
+
+(defmacro defer-feature! (feature &optional mode)
+ "Pretend FEATURE hasn't been loaded yet, until FEATURE-hook is triggered.
+
+Some packages (like `elisp-mode' and `lisp-mode') are loaded immediately at
+startup, which will prematurely trigger `after!' (and `with-eval-after-load')
+blocks. To get around this we make Emacs believe FEATURE hasn't been loaded yet,
+then wait until FEATURE-hook (or MODE-hook, if MODE is provided) is triggered to
+reverse this and trigger `after!' blocks at a more reasonable time."
+ (let ((advice-fn (intern (format "doom|defer-feature-%s" feature)))
+ (mode (or mode feature)))
+ `(progn
+ (setq features (delq ',feature features))
+ (advice-add #',mode :before #',advice-fn)
+ (defun ,advice-fn (&rest _)
+ ;; Some plugins (like yasnippet) will invoke a mode early, e.g. to
+ ;; parse some code. This would prematurely trigger this function. This
+ ;; checks for that:
+ (when (and ,(intern (format "%s-hook" mode))
+ (not delay-mode-hooks))
+ ;; Otherwise, announce to the world this package has been loaded, so
+ ;; `after!' handlers can respond and configure elisp-mode as
+ ;; expected.
+ (provide ',feature)
+ (advice-remove #',mode #',advice-fn))))))
+
+(defmacro after! (targets &rest body)
+ "A smart wrapper around `with-eval-after-load' that:
+
+1. Suppresses warnings at compile-time
+2. No-ops for TARGETS that are disabled by the user (via `package!')
+3. Supports compound TARGETS statements (see below)
+
+BODY is evaluated once TARGETS are loaded. TARGETS can either be:
+
+- An unquoted package symbol (the name of a package)
+
+ (after! helm ...)
+
+- An unquoted list of package symbols
+
+ (after! (magit git-gutter) ...)
+
+- An unquoted, nested list of compound package lists, using :or/:any and/or :and/:all
+
+ (after! (:or package-a package-b ...) ...)
+ (after! (:and package-a package-b ...) ...)
+ (after! (:and package-a (:or package-b package-c) ...) ...)
+
+ Note that:
+ - :or and :any are equivalent
+ - :and and :all are equivalent
+ - If these are omitted, :and is assumed."
+ (declare (indent defun) (debug t))
+ (unless (and (symbolp targets)
+ (memq targets (bound-and-true-p doom-disabled-packages)))
+ (list (if (or (not (bound-and-true-p byte-compile-current-file))
+ (dolist (next (doom-enlist targets))
+ (unless (keywordp next)
+ (if (symbolp next)
+ (require next nil :no-error)
+ (load next :no-message :no-error)))))
+ #'progn
+ #'with-no-warnings)
+ (if (symbolp targets)
+ `(with-eval-after-load ',targets ,@body)
+ (pcase (car-safe targets)
+ ((or :or :any)
+ (macroexp-progn
+ (cl-loop for next in (cdr targets)
+ collect `(after! ,next ,@body))))
+ ((or :and :all)
+ (dolist (next (cdr targets))
+ (setq body `((after! ,next ,@body))))
+ (car body))
+ (_ `(after! (:and ,@targets) ,@body)))))))
(defmacro quiet! (&rest forms)
- "Run FORMS without making any noise."
- `(if doom-debug-mode
- (progn ,@forms)
- (let ((old-fn (symbol-function 'write-region)))
- (cl-letf* ((standard-output (lambda (&rest _)))
- ((symbol-function 'load-file) (lambda (file) (load file nil t)))
- ((symbol-function 'message) (lambda (&rest _)))
- ((symbol-function 'write-region)
- (lambda (start end filename &optional append visit lockname mustbenew)
- (unless visit (setq visit 'no-message))
- (funcall old-fn start end filename append visit lockname mustbenew)))
- (inhibit-message t)
- (save-silently t))
- ,@forms))))
+ "Run FORMS without making any output."
+ `(cond (noninteractive
+ (let ((old-fn (symbol-function 'write-region)))
+ (cl-letf ((standard-output (lambda (&rest _)))
+ ((symbol-function 'load-file) (lambda (file) (load file nil t)))
+ ((symbol-function 'message) (lambda (&rest _)))
+ ((symbol-function 'write-region)
+ (lambda (start end filename &optional append visit lockname mustbenew)
+ (unless visit (setq visit 'no-message))
+ (funcall old-fn start end filename append visit lockname mustbenew))))
+ ,@forms)))
+ ((or doom-debug-mode debug-on-error debug-on-quit)
+ ,@forms)
+ ((let ((inhibit-message t)
+ (save-silently t))
+ (prog1 ,@forms (message ""))))))
-(defvar doom--transient-counter 0)
-(defmacro add-transient-hook! (hook &rest forms)
- "Attaches transient forms to a HOOK.
+(defmacro add-transient-hook! (hook-or-function &rest forms)
+ "Attaches a self-removing function to HOOK-OR-FUNCTION.
-HOOK can be a quoted hook or a sharp-quoted function (which will be advised).
+FORMS are evaluated once when that function/hook is first invoked, then never
+again.
-These forms will be evaluated once when that function/hook is first invoked,
-then it detaches itself."
+HOOK-OR-FUNCTION can be a quoted hook or a sharp-quoted function (which will be
+advised)."
(declare (indent 1))
- (let ((append (eq (car forms) :after))
- (fn (intern (format "doom-transient-hook-%s" (cl-incf doom--transient-counter)))))
- `(when ,hook
+ (let ((append (if (eq (car forms) :after) (pop forms)))
+ (fn (if (symbolp (car forms))
+ (intern (format "doom|transient-hook-%s" (pop forms)))
+ (make-symbol "doom|transient-hook-"))))
+ `(progn
(fset ',fn
(lambda (&rest _)
,@forms
- (cond ((functionp ,hook) (advice-remove ,hook #',fn))
- ((symbolp ,hook) (remove-hook ,hook #',fn)))
+ (cond ((functionp ,hook-or-function) (advice-remove ,hook-or-function #',fn))
+ ((symbolp ,hook-or-function) (remove-hook ,hook-or-function #',fn)))
(unintern ',fn nil)))
- (cond ((functionp ,hook)
- (advice-add ,hook ,(if append :after :before) #',fn))
- ((symbolp ,hook)
- (add-hook ,hook #',fn ,append))))))
+ (cond ((functionp ,hook-or-function)
+ (advice-add ,hook-or-function ,(if append :after :before) #',fn))
+ ((symbolp ,hook-or-function)
+ (put ',fn 'permanent-local-hook t)
+ (add-hook ,hook-or-function #',fn ,append))))))
(defmacro add-hook! (&rest args)
"A convenience macro for `add-hook'. Takes, in order:
@@ -195,7 +297,7 @@ then it detaches itself."
3. A function, list of functions, or body forms to be wrapped in a lambda.
Examples:
- (add-hook! 'some-mode-hook 'enable-something)
+ (add-hook! 'some-mode-hook 'enable-something) (same as `add-hook')
(add-hook! some-mode '(enable-something and-another))
(add-hook! '(one-mode-hook second-mode-hook) 'enable-something)
(add-hook! (one-mode second-mode) 'enable-something)
@@ -205,7 +307,9 @@ Examples:
(add-hook! :append :local (one-mode second-mode) (setq v 5) (setq a 2))
Body forms can access the hook's arguments through the let-bound variable
-`args'."
+`args'.
+
+\(fn [:append :local] HOOKS FUNCTIONS)"
(declare (indent defun) (debug t))
(let ((hook-fn 'add-hook)
append-p local-p)
@@ -232,81 +336,150 @@ Body forms can access the hook's arguments through the let-bound variable
`(remove-hook ',hook ,fn ,local-p)
`(add-hook ',hook ,fn ,append-p ,local-p))
forms)))
- `(progn ,@(nreverse forms)))))
+ `(progn ,@(if append-p (nreverse forms) forms)))))
(defmacro remove-hook! (&rest args)
"Convenience macro for `remove-hook'. Takes the same arguments as
-`add-hook!'."
+`add-hook!'.
+
+\(fn [:append :local] HOOKS FUNCTIONS)"
+ (declare (indent defun) (debug t))
`(add-hook! :remove ,@args))
-(defmacro associate! (mode &rest plist)
- "Associate a minor mode to certain patterns and project files."
+(defmacro setq-hook! (hooks &rest rest)
+ "Convenience macro for setting buffer-local variables in a hook.
+
+ (setq-hook! 'markdown-mode-hook
+ line-spacing 2
+ fill-column 80)"
+ (declare (indent 1))
+ (unless (= 0 (% (length rest) 2))
+ (signal 'wrong-number-of-arguments (list #'evenp (length rest))))
+ `(add-hook! :append ,hooks
+ ,@(let (forms)
+ (while rest
+ (let ((var (pop rest))
+ (val (pop rest)))
+ (push `(setq-local ,var ,val) forms)))
+ (nreverse forms))))
+
+(defun advice-add! (symbols where functions)
+ "Variadic version of `advice-add'.
+
+SYMBOLS and FUNCTIONS can be lists of functions."
+ (let ((functions (if (functionp functions)
+ (list functions)
+ functions)))
+ (dolist (s (doom-enlist symbols))
+ (dolist (f (doom-enlist functions))
+ (advice-add s where f)))))
+
+(defun advice-remove! (symbols where-or-fns &optional functions)
+ "Variadic version of `advice-remove'.
+
+WHERE-OR-FNS is ignored if FUNCTIONS is provided. This lets you substitute
+advice-add with advice-remove and evaluate them without having to modify every
+statement."
+ (unless functions
+ (setq functions where-or-fns
+ where-or-fns nil))
+ (let ((functions (if (functionp functions)
+ (list functions)
+ functions)))
+ (dolist (s (doom-enlist symbols))
+ (dolist (f (doom-enlist functions))
+ (advice-remove s f)))))
+
+(cl-defmacro associate! (mode &key modes match files when)
+ "Enables a minor mode if certain conditions are met.
+
+The available conditions are:
+
+ :modes SYMBOL_LIST
+ A list of major/minor modes in which this minor mode may apply.
+ :match REGEXP
+ A regexp to be tested against the current file path.
+ :files SPEC
+ Accepts what `project-file-exists-p!' accepts. Checks if certain files exist
+ relative to the project root.
+ :when FORM
+ Whenever FORM returns non-nil."
(declare (indent 1))
(unless noninteractive
- (let ((modes (plist-get plist :modes))
- (match (plist-get plist :match))
- (files (plist-get plist :files))
- (pred-form (plist-get plist :when)))
- (cond ((or files modes pred-form)
- (when (and files
- (not (or (listp files)
- (stringp files))))
- (user-error "associate! :files expects a string or list of strings"))
- (let ((hook-name (intern (format "doom--init-mode-%s" mode))))
- `(progn
- (defun ,hook-name ()
- (when (and (boundp ',mode)
- (not ,mode)
- (and buffer-file-name (not (file-remote-p buffer-file-name)))
- ,(if match `(if buffer-file-name (string-match-p ,match buffer-file-name)) t)
- ,(if files (doom--resolve-path-forms files) t)
- ,(or pred-form t))
- (,mode 1)))
- ,@(if (and modes (listp modes))
- (cl-loop for hook in (doom--resolve-hook-forms modes)
- collect `(add-hook ',hook ',hook-name))
- `((add-hook 'after-change-major-mode-hook ',hook-name))))))
- (match
- `(push (cons ,match ',mode) doom-auto-minor-mode-alist))
- (t (user-error "associate! invalid rules for mode [%s] (modes %s) (match %s) (files %s)"
- mode modes match files))))))
+ (cond ((or files modes when)
+ (when (and files
+ (not (or (listp files)
+ (stringp files))))
+ (user-error "associate! :files expects a string or list of strings"))
+ (let ((hook-name (intern (format "doom--init-mode-%s" mode))))
+ `(progn
+ (fset ',hook-name
+ (lambda ()
+ (and (fboundp ',mode)
+ (not (bound-and-true-p ,mode))
+ (and buffer-file-name (not (file-remote-p buffer-file-name)))
+ ,(or (not match)
+ `(if buffer-file-name (string-match-p ,match buffer-file-name)))
+ ,(or (not files)
+ (doom--resolve-path-forms
+ (if (stringp (car files)) (cons 'and files) files)
+ '(doom-project-root)))
+ ,(or when t)
+ (,mode 1))))
+ ,@(if (and modes (listp modes))
+ (cl-loop for hook in (doom--resolve-hook-forms modes)
+ collect `(add-hook ',hook #',hook-name))
+ `((add-hook 'after-change-major-mode-hook #',hook-name))))))
+ (match
+ `(add-to-list 'doom-auto-minor-mode-alist '(,match . ,mode)))
+ ((user-error "Invalid `associate!' rules for mode [%s] (:modes %s :match %s :files %s :when %s)"
+ mode modes match files when)))))
+(defmacro file-exists-p! (spec &optional directory)
+ "Returns non-nil if the files in SPEC all exist.
-;; I needed a way to reliably cross-configure modules without worrying about
-;; whether they were enabled or not, so I wrote `set!'. If a setting doesn't
-;; exist at runtime, the `set!' call is ignored (and omitted when
-;; byte-compiled).
-(defvar doom-settings nil)
+Returns the last file found to meet the rules set by SPEC. SPEC can be a single
+file or a list of forms/files. It understands nested (and ...) and (or ...), as
+well.
-(defmacro def-setting! (keyword arglist &optional docstring &rest forms)
- "Define a setting. Like `defmacro', this should return a form to be executed
-when called with `set!'. FORMS are not evaluated until `set!' calls it.
+DIRECTORY is where to look for the files in SPEC if they aren't absolute.
-See `doom/describe-setting' for a list of available settings.
+For example:
+ (file-exists-p! (or doom-core-dir \"~/.config\" \"some-file\") \"~\")"
+ (if directory
+ `(let ((--directory-- ,directory))
+ ,(doom--resolve-path-forms spec '--directory--))
+ (doom--resolve-path-forms spec)))
-Do not use this for configuring Doom core."
- (declare (indent defun) (doc-string 3))
- (unless (keywordp keyword)
- (error "Not a valid property name: %s" keyword))
- (let ((fn (intern (format "doom--set%s" keyword))))
- `(progn
- (defun ,fn ,arglist
- ,docstring
- ,@forms)
- (cl-pushnew ',(cons keyword fn) doom-settings :test #'eq :key #'car))))
+(defmacro load! (filename &optional path noerror)
+ "Load a file relative to the current executing file (`load-file-name').
-(defmacro set! (keyword &rest values)
- "Set an option defined by `def-setting!'. Skip if doesn't exist. See
-`doom/describe-setting' for a list of available settings."
- (declare (indent defun))
- (unless values
- (error "Empty set! for %s" keyword))
- (let ((fn (cdr (assq keyword doom-settings))))
- (if fn
- (apply fn values)
- (when doom-debug-mode
- (message "No setting found for %s" keyword)
- nil))))
+FILENAME is either a file path string or a form that should evaluate to such a
+string at run time. PATH is where to look for the file (a string representing a
+directory path). If omitted, the lookup is relative to either `load-file-name',
+`byte-compile-current-file' or `buffer-file-name' (checked in that order).
+
+If NOERROR is non-nil, don't throw an error if the file doesn't exist."
+ (unless path
+ (setq path (or (DIR!)
+ (error "Could not detect path to look for '%s' in"
+ filename))))
+ (let ((file (if path `(expand-file-name ,filename ,path) filename)))
+ `(condition-case e
+ (load ,file ,noerror ,(not doom-debug-mode))
+ ((debug doom-error) (signal (car e) (cdr e)))
+ ((debug error)
+ (let* ((source (file-name-sans-extension ,file))
+ (err (cond ((file-in-directory-p source doom-core-dir)
+ (cons 'doom-error doom-core-dir))
+ ((file-in-directory-p source doom-private-dir)
+ (cons 'doom-private-error doom-private-dir))
+ ((cons 'doom-module-error doom-emacs-dir)))))
+ (signal (car err)
+ (list (file-relative-name
+ (concat source ".el")
+ (cdr err))
+ e)))))))
(provide 'core-lib)
;;; core-lib.el ends here
diff --git a/core/core-modules.el b/core/core-modules.el
new file mode 100644
index 000000000..8fb3f5c7d
--- /dev/null
+++ b/core/core-modules.el
@@ -0,0 +1,472 @@
+;;; core-modules.el --- module & package management system -*- lexical-binding: t; -*-
+
+(defvar doom-init-modules-p nil
+ "Non-nil if `doom-initialize-modules' has run.")
+
+(defvar doom-modules ()
+ "A hash table of enabled modules. Set by `doom-initialize-modules'.")
+
+(defvar doom-modules-dirs
+ (list (expand-file-name "modules/" doom-private-dir)
+ doom-modules-dir)
+ "A list of module root directories. Order determines priority.")
+
+(defvar doom-inhibit-module-warnings (not noninteractive)
+ "If non-nil, don't emit deprecated or missing module warnings at startup.")
+
+(defconst doom-obsolete-modules
+ '((:feature (version-control (:emacs vc) (:ui vc-gutter))
+ (spellcheck (:tools flyspell))
+ (syntax-checker (:tools flycheck)))
+ (:tools (rotate-text (:editor rotate-text)))
+ (:emacs (electric-indent (:emacs electric))
+ (hideshow (:editor fold)))
+ (:ui (doom-modeline (:ui modeline)))
+ (:ui (fci (:ui fill-column)))
+ (:ui (evil-goggles (:ui ophints))))
+ "An alist of deprecated modules, mapping deprecated modules to an optional new
+location (which will create an alias). Each CAR and CDR is a (CATEGORY .
+MODULES). E.g.
+
+ ((:emacs . electric-indent) . (:emacs electric))
+ ((:feature . version-control) (:emacs vc) (:ui . vc-gutter))
+
+A warning will be put out if these deprecated modules are used.")
+
+(defvar doom--current-module nil)
+(defvar doom--current-flags nil)
+
+
+;;
+;;; Custom hooks
+
+(defvar doom-before-init-modules-hook nil
+ "A list of hooks to run before Doom's modules' config.el files are loaded, but
+after their init.el files are loaded.")
+
+(defvar doom-init-modules-hook nil
+ "A list of hooks to run after Doom's modules' config.el files have loaded, but
+before the user's private module.")
+
+(defvaralias 'doom-after-init-modules-hook 'after-init-hook)
+
+(define-obsolete-variable-alias 'doom-post-init-hook 'doom-init-modules-hook "2.1.0")
+(define-obsolete-variable-alias 'doom-init-hook 'doom-before-init-modules-hook "2.1.0")
+
+
+;;
+;;; Bootstrap API
+
+(defun doom-initialize-modules (&optional force-p)
+ "Loads the init.el in `doom-private-dir' and sets up hooks for a healthy
+session of Dooming. Will noop if used more than once, unless FORCE-P is
+non-nil."
+ (when (and (or force-p
+ (not doom-init-modules-p))
+ (not (setq doom-modules nil))
+ (load! "init" doom-private-dir t))
+ (setq doom-init-modules-p t)
+ (maphash (lambda (key plist)
+ (let ((doom--current-module key)
+ (doom--current-flags (plist-get plist :flags)))
+ (load! "init" (plist-get plist :path) t)))
+ doom-modules)
+ (run-hook-wrapped 'doom-before-init-modules-hook #'doom-try-run-hook)
+ (unless noninteractive
+ (maphash (lambda (key plist)
+ (let ((doom--current-module key)
+ (doom--current-flags (plist-get plist :flags)))
+ (load! "config" (plist-get plist :path) t)))
+ doom-modules)
+ (run-hook-wrapped 'doom-init-modules-hook #'doom-try-run-hook)
+ (load! "config" doom-private-dir t)
+ (unless custom-file
+ (setq custom-file (concat doom-local-dir "custom.el")))
+ (when (stringp custom-file)
+ (load custom-file t t t)))))
+
+
+;;
+;;; Module API
+
+(defun doom-module-p (category module)
+ "Returns t if CATEGORY MODULE is enabled (ie. present in `doom-modules')."
+ (declare (pure t) (side-effect-free t))
+ (and (hash-table-p doom-modules)
+ (gethash (cons category module) doom-modules)
+ t))
+
+(defun doom-module-get (category module &optional property)
+ "Returns the plist for CATEGORY MODULE. Gets PROPERTY, specifically, if set."
+ (declare (pure t) (side-effect-free t))
+ (when-let* ((plist (gethash (cons category module) doom-modules)))
+ (if property
+ (plist-get plist property)
+ plist)))
+
+(defun doom-module-put (category module &rest plist)
+ "Set a PROPERTY for CATEGORY MODULE to VALUE. PLIST should be additional pairs
+of PROPERTY and VALUEs.
+
+\(fn CATEGORY MODULE PROPERTY VALUE &rest [PROPERTY VALUE [...]])"
+ (if-let* ((old-plist (doom-module-get category module)))
+ (progn
+ (when plist
+ (when (cl-oddp (length plist))
+ (signal 'wrong-number-of-arguments (list (length plist))))
+ (while plist
+ (plist-put old-plist (pop plist) (pop plist))))
+ (puthash (cons category module) old-plist doom-modules))
+ (puthash (cons category module) plist doom-modules)))
+
+(defun doom-module-set (category module &rest plist)
+ "Enables a module by adding it to `doom-modules'.
+
+CATEGORY is a keyword, module is a symbol, PLIST is a plist that accepts the
+following properties:
+
+ :flags [SYMBOL LIST] list of enabled category flags
+ :path [STRING] path to category root directory
+
+Example:
+ (doom-module-set :lang 'haskell :flags '(+intero))"
+ (puthash (cons category module)
+ plist
+ doom-modules))
+
+(defun doom-module-path (category module &optional file)
+ "Like `expand-file-name', but expands FILE relative to CATEGORY (keywordp) and
+MODULE (symbol).
+
+If the category isn't enabled this will always return nil. For finding disabled
+modules use `doom-module-locate-path'."
+ (let ((path (doom-module-get category module :path))
+ file-name-handler-alist)
+ (if file (expand-file-name file path)
+ path)))
+
+(defun doom-module-locate-path (category &optional module file)
+ "Searches `doom-modules-dirs' to find the path to a module.
+
+CATEGORY is a keyword (e.g. :lang) and MODULE is a symbol (e.g. 'python). FILE
+is a string that will be appended to the resulting path. If no path exists, this
+returns nil, otherwise an absolute path.
+
+This doesn't require modules to be enabled. For enabled modules us
+`doom-module-path'."
+ (when (keywordp category)
+ (setq category (doom-keyword-name category)))
+ (when (and module (symbolp module))
+ (setq module (symbol-name module)))
+ (cl-loop with file-name-handler-alist = nil
+ for default-directory in doom-modules-dirs
+ for path = (concat category "/" module "/" file)
+ if (file-exists-p path)
+ return (expand-file-name path)))
+
+(defun doom-module-from-path (&optional path)
+ "Returns a cons cell (CATEGORY . MODULE) derived from PATH (a file path)."
+ (or doom--current-module
+ (let* (file-name-handler-alist
+ (path (or path (FILE!))))
+ (save-match-data
+ (setq path (file-truename path))
+ (when (string-match "/modules/\\([^/]+\\)/\\([^/]+\\)\\(?:/.*\\)?$" path)
+ (when-let* ((category (match-string 1 path))
+ (module (match-string 2 path)))
+ (cons (doom-keyword-intern category)
+ (intern module))))))))
+
+(defun doom-module-load-path (&optional all-p)
+ "Return a list of absolute file paths to activated modules. If ALL-P is
+non-nil, return paths of possible modules, activated or otherwise."
+ (declare (pure t) (side-effect-free t))
+ (append (if all-p
+ (doom-files-in doom-modules-dirs
+ :type 'dirs
+ :mindepth 1
+ :depth 1
+ :full t)
+ (cl-loop for plist being the hash-values of (doom-modules)
+ collect (plist-get plist :path)))
+ (list doom-private-dir)))
+
+(defun doom-modules (&optional refresh-p)
+ "Minimally initialize `doom-modules' (a hash table) and return it."
+ (or (unless refresh-p doom-modules)
+ (let ((noninteractive t)
+ doom-modules
+ doom-init-modules-p)
+ (load! "init" doom-private-dir t)
+ (or doom-modules
+ (make-hash-table :test 'equal
+ :size 20
+ :rehash-threshold 1.0)))))
+
+
+;;
+;;; Use-package modifications
+
+(autoload 'use-package "use-package-core" nil nil t)
+
+(setq use-package-compute-statistics doom-debug-mode
+ use-package-verbose doom-debug-mode
+ use-package-minimum-reported-time (if doom-debug-mode 0 0.1)
+ use-package-expand-minimally (not noninteractive))
+
+;; Adds two new keywords to `use-package' (and consequently, `def-package!') to
+;; expand its lazy-loading capabilities. They are:
+;;
+;; :after-call SYMBOL|LIST
+;; :defer-incrementally SYMBOL|LIST|t
+;;
+;; Check out `def-package!'s documentation for more about these two.
+(defvar doom--deferred-packages-alist '(t))
+(after! use-package-core
+ ;; :ensure and :pin don't work well with Doom, so we forcibly remove them.
+ (dolist (keyword '(:ensure :pin))
+ (setq use-package-keywords (delq keyword use-package-keywords)))
+
+ ;; Insert new deferring keywords
+ (dolist (keyword '(:defer-incrementally :after-call))
+ (add-to-list 'use-package-deferring-keywords keyword nil #'eq)
+ (setq use-package-keywords
+ (use-package-list-insert keyword use-package-keywords :after)))
+
+ (defalias 'use-package-normalize/:defer-incrementally 'use-package-normalize-symlist)
+ (defun use-package-handler/:defer-incrementally (name _keyword targets rest state)
+ (use-package-concat
+ `((doom-load-packages-incrementally
+ ',(if (equal targets '(t))
+ (list name)
+ (append targets (list name)))))
+ (use-package-process-keywords name rest state)))
+
+ (defalias 'use-package-normalize/:after-call 'use-package-normalize-symlist)
+ (defun use-package-handler/:after-call (name _keyword hooks rest state)
+ (if (plist-get state :demand)
+ (use-package-process-keywords name rest state)
+ (let ((fn (intern (format "doom|transient-hook--load-%s" name))))
+ (use-package-concat
+ `((fset ',fn
+ (lambda (&rest _)
+ (doom-log "Loading deferred package %s from %s" ',name ',fn)
+ (condition-case e
+ (require ',name)
+ ((debug error)
+ (message "Failed to load deferred package %s: %s" ',name e)))
+ (when-let* ((deferral-list (assq ',name doom--deferred-packages-alist)))
+ (dolist (hook (cdr deferral-list))
+ (if (functionp hook)
+ (advice-remove hook #',fn)
+ (remove-hook hook #',fn)))
+ (setq doom--deferred-packages-alist
+ (delq deferral-list doom--deferred-packages-alist))))))
+ (let (forms)
+ (dolist (hook hooks forms)
+ (push (if (functionp hook)
+ `(advice-add #',hook :before #',fn)
+ `(add-hook ',hook #',fn))
+ forms)))
+ `((unless (assq ',name doom--deferred-packages-alist)
+ (push '(,name) doom--deferred-packages-alist))
+ (nconc (assq ',name doom--deferred-packages-alist)
+ '(,@hooks)))
+ (use-package-process-keywords name rest state))))))
+
+
+;;
+;;; Module config macros
+
+(defmacro doom! (&rest modules)
+ "Bootstraps DOOM Emacs and its modules.
+
+The bootstrap process involves making sure the essential directories exist, core
+packages are installed, `doom-autoload-file' is loaded, `doom-packages-file'
+cache exists (and is loaded) and, finally, loads your private init.el (which
+should contain your `doom!' block).
+
+If the cache exists, much of this function isn't run, which substantially
+reduces startup time.
+
+The overall load order of Doom is as follows:
+
+ ~/.emacs.d/init.el
+ ~/.emacs.d/core/core.el
+ $DOOMDIR/init.el
+ {$DOOMDIR,~/.emacs.d}/modules/*/*/init.el
+ `doom-before-init-modules-hook'
+ {$DOOMDIR,~/.emacs.d}/modules/*/*/config.el
+ `doom-init-modules-hook'
+ $DOOMDIR/config.el
+ `doom-after-init-modules-hook'
+ `after-init-hook'
+ `emacs-startup-hook'
+ `window-setup-hook'
+
+Module load order is determined by your `doom!' block. See `doom-modules-dirs'
+for a list of all recognized module trees. Order defines precedence (from most
+to least)."
+ (unless doom-modules
+ (setq doom-modules
+ (make-hash-table :test 'equal
+ :size (if modules (length modules) 150)
+ :rehash-threshold 1.0)))
+ (let ((inhibit-message doom-inhibit-module-warnings)
+ category m)
+ (while modules
+ (setq m (pop modules))
+ (cond ((keywordp m) (setq category m))
+ ((not category) (error "No module category specified for %s" m))
+ ((catch 'doom-modules
+ (let* ((module (if (listp m) (car m) m))
+ (flags (if (listp m) (cdr m))))
+ (when-let* ((obsolete (assq category doom-obsolete-modules))
+ (new (assq module obsolete)))
+ (let ((newkeys (cdr new)))
+ (if (null newkeys)
+ (message "WARNING %s is deprecated" key)
+ (message "WARNING %s is deprecated, enabling %s instead"
+ (list category module) newkeys)
+ (push category modules)
+ (dolist (key newkeys)
+ (push (if flags
+ (nconc (cdr key) flags)
+ (cdr key))
+ modules)
+ (push (car key) modules))
+ (throw 'doom-modules t))))
+ (if-let* ((path (doom-module-locate-path category module)))
+ (doom-module-set category module :flags flags :path path)
+ (message "WARNING Couldn't find the %s %s module" category module))))))))
+ (when noninteractive
+ (setq doom-inhibit-module-warnings t))
+ `(setq doom-modules ',doom-modules))
+
+(defvar doom-disabled-packages)
+(defmacro def-package! (name &rest plist)
+ "This is a thin wrapper around `use-package'.
+
+It is ignored if the NAME package is disabled.
+
+Supports two special properties over `use-package':
+
+:after-call SYMBOL|LIST
+ Takes a symbol or list of symbols representing functions or hook variables.
+ The first time any of these functions or hooks are executed, the package is
+ loaded. e.g.
+
+ (def-package! projectile
+ :after-call (pre-command-hook after-find-file dired-before-readin-hook)
+ ...)
+
+:defer-incrementally SYMBOL|LIST|t
+ Takes a symbol or list of symbols representing packages that will be loaded
+ incrementally at startup before this one. This is helpful for large packages
+ like magit or org, which load a lot of dependencies on first load. This lets
+ you load them piece-meal during idle periods, so that when you finally do need
+ the package, it'll load quicker. e.g.
+
+ NAME is implicitly added if this property is present and non-nil. No need to
+ specify it. A value of `t' implies NAME, e.g.
+
+ (def-package! x
+ ;; This is equivalent to :defer-incrementally (x)
+ :defer-incrementally t
+ ...)"
+ (unless (or (memq name doom-disabled-packages)
+ ;; At compile-time, use-package will forcibly load its package to
+ ;; prevent compile-time errors. However, Doom users can
+ ;; intentionally disable packages, resulting if file-missing
+ ;; package errors, so we preform this check at compile time:
+ (and (bound-and-true-p byte-compile-current-file)
+ (not (locate-library (symbol-name name)))))
+ `(use-package ,name ,@plist)))
+
+(defmacro def-package-hook! (package when &rest body)
+ "Reconfigures a package's `def-package!' block.
+
+Only use this macro in a module's init.el file.
+
+Under the hood, this uses use-package's `use-package-inject-hooks'.
+
+PACKAGE is a symbol; the package's name.
+WHEN should be one of the following:
+ :pre-init :post-init :pre-config :post-config
+
+WARNING: If :pre-init or :pre-config hooks return nil, the original
+`def-package!''s :init/:config block (respectively) is overwritten, so remember
+to have them return non-nil (or exploit that to overwrite Doom's config)."
+ (declare (indent defun))
+ (doom--assert-stage-p 'init #'package!)
+ (unless (memq when '(:pre-init :post-init :pre-config :post-config))
+ (error "'%s' isn't a valid hook for def-package-hook!" when))
+ `(progn
+ (setq use-package-inject-hooks t)
+ (add-hook!
+ ',(intern (format "use-package--%s--%s-hook"
+ package
+ (substring (symbol-name when) 1)))
+ ,@body)))
+
+(defmacro require! (category module &rest flags)
+ "Loads the CATEGORY MODULE module with FLAGS.
+
+CATEGORY is a keyword, MODULE is a symbol and FLAGS are symbols.
+
+ (require! :lang php +lsp)
+
+This is for testing and internal use. This is not the correct way to enable a
+module."
+ `(let ((doom-modules ,doom-modules)
+ (module-path (doom-module-locate-path ,category ',module)))
+ (doom-module-set
+ ,category ',module
+ ,@(let ((plist (doom-module-get category module)))
+ (when flags
+ (plist-put plist :flags flags))
+ (unless (plist-member plist :path)
+ (plist-put plist :path (doom-module-locate-path category module)))
+ plist))
+ (if (directory-name-p module-path)
+ (condition-case-unless-debug ex
+ (let ((doom--current-module ',(cons category module))
+ (doom--current-flags ',flags))
+ (load! "init" module-path :noerror)
+ (let ((doom--stage 'config))
+ (load! "config" module-path :noerror)))
+ ('error
+ (lwarn 'doom-modules :error
+ "%s in '%s %s' -> %s"
+ (car ex) ,category ',module
+ (error-message-string ex))))
+ (warn 'doom-modules :warning "Couldn't find module '%s %s'"
+ ,category ',module))))
+
+(defmacro featurep! (category &optional module flag)
+ "Returns t if CATEGORY MODULE is enabled.
+
+If FLAG is provided, returns t if CATEGORY MODULE has FLAG enabled.
+
+ (featurep! :config default)
+
+Module FLAGs are set in your config's `doom!' block, typically in
+~/.emacs.d/init.el. Like so:
+
+ :config (default +flag1 -flag2)
+
+CATEGORY and MODULE can be omitted When this macro is used from inside a module
+(except your DOOMDIR, which is a special moduel). e.g. (featurep! +flag)"
+ (and (cond (flag (memq flag (doom-module-get category module :flags)))
+ (module (doom-module-p category module))
+ (doom--current-flags (memq category doom--current-flags))
+ ((let ((module-pair
+ (or doom--current-module
+ (doom-module-from-path (FILE!)))))
+ (unless module-pair
+ (error "featurep! call couldn't auto-detect what module its in (from %s)" (FILE!)))
+ (memq category (doom-module-get (car module-pair) (cdr module-pair) :flags)))))
+ t))
+
+(provide 'core-modules)
+;;; core-modules.el ends here
diff --git a/core/core-os.el b/core/core-os.el
index 08f356fb9..c84861c1d 100644
--- a/core/core-os.el
+++ b/core/core-os.el
@@ -1,24 +1,33 @@
;;; core-os.el -*- lexical-binding: t; -*-
-(defconst IS-MAC (eq system-type 'darwin))
-(defconst IS-LINUX (eq system-type 'gnu/linux))
+;; TODO Remove me later (deprecated)
+(defmacro set-env! (&rest _))
;; clipboard
-(setq x-select-request-type '(UTF8_STRING COMPOUND_TEXT TEXT STRING)
- ;; Use a shared clipboard
- select-enable-clipboard t
- select-enable-primary t)
+(setq x-select-request-type '(UTF8_STRING COMPOUND_TEXT TEXT STRING))
-(after! evil
- ;; stop copying each visual state move to the clipboard:
- ;; https://bitbucket.org/lyro/evil/issue/336/osx-visual-state-copies-the-region-on
- ;; Most of this code grokked from:
- ;; http://stackoverflow.com/questions/15873346/elisp-rename-macro
- (advice-add #'evil-visual-update-x-selection :override #'ignore))
+;; fewer opts to process for systems that don't need them
+(unless IS-MAC (setq command-line-ns-option-alist nil))
+(unless IS-LINUX (setq command-line-x-option-alist nil))
+
+;; Fix the clipboard in terminal or daemon Emacs (non-GUI)
+(defun doom|init-clipboard-in-tty-emacs ()
+ (if IS-MAC
+ (if (require 'osx-clipboard nil t) (osx-clipboard-mode))
+ (if (require 'xclip nil t) (xclip-mode))))
+(add-hook 'tty-setup-hook #'doom|init-clipboard-in-tty-emacs)
+
+;; Enable mouse in terminal Emacs
+(add-hook 'tty-setup-hook #'xterm-mouse-mode)
+
+;; stop copying each visual state move to the clipboard:
+;; https://bitbucket.org/lyro/evil/issue/336/osx-visual-state-copies-the-region-on
+;; grokked from: http://stackoverflow.com/questions/15873346/elisp-rename-macro
+(advice-add #'evil-visual-update-x-selection :override #'ignore)
(cond (IS-MAC
- (setq mac-command-modifier 'meta
- mac-option-modifier 'alt
+ (setq mac-command-modifier 'super
+ mac-option-modifier 'meta
;; sane trackpad/mouse scroll settings
mac-redisplay-dont-reset-vscroll t
mac-mouse-wheel-smooth-scroll nil
@@ -27,31 +36,25 @@
;; Curse Lion and its sudden but inevitable fullscreen mode!
;; NOTE Meaningless to railwaycat's emacs-mac build
ns-use-native-fullscreen nil
- ;; Don't open files from the workspace in a new frame
+ ;; Visit files opened outside of Emacs in existing frame, rather
+ ;; than a new one
ns-pop-up-frames nil)
- (cond ((display-graphic-p)
- ;; A known problem with GUI Emacs on MacOS: it runs in an isolated
- ;; environment, so envvars will be wrong. That includes the PATH
- ;; Emacs picks up. `exec-path-from-shell' fixes this. This is slow
- ;; and benefits greatly from compilation.
- (setq exec-path
- (or (eval-when-compile
- (when (require 'exec-path-from-shell nil t)
- (setq exec-path-from-shell-check-startup-files nil
- exec-path-from-shell-arguments (delete "-i" exec-path-from-shell-arguments))
- (nconc exec-path-from-shell-variables '("GOPATH" "GOROOT" "PYTHONPATH"))
- (exec-path-from-shell-initialize)
- exec-path))
- exec-path)))
- (t
- (when (require 'osx-clipboard nil t)
- (osx-clipboard-mode +1)))))
+ ;; Syncs ns frame parameters with theme (and fixes mismatching text color
+ ;; in the frame title)
+ (when (and (or (daemonp)
+ (display-graphic-p))
+ (require 'ns-auto-titlebar nil t))
+ (add-hook 'doom-load-theme-hook #'ns-auto-titlebar-mode)))
(IS-LINUX
- ;; native tooltips are ugly!
- (setq x-gtk-use-system-tooltips nil)
- ))
+ (setq x-gtk-use-system-tooltips nil ; native tooltips are ugly!
+ x-underline-at-descent-line t)) ; draw underline lower
+
+ (IS-WINDOWS
+ (setq w32-get-true-file-attributes nil) ; fix file io slowdowns
+ (when (display-graphic-p)
+ (setenv "GIT_ASKPASS" "git-gui--askpass"))))
(provide 'core-os)
;;; core-os.el ends here
diff --git a/core/core-packages.el b/core/core-packages.el
index c40ef27b5..d237c0e9d 100644
--- a/core/core-packages.el
+++ b/core/core-packages.el
@@ -1,17 +1,14 @@
-;;; core-packages.el --- package management system -*- lexical-binding: t; -*-
+;;; core/core-packages.el -*- lexical-binding: t; -*-
-;; Emacs package management is opinionated. Unfortunately, so am I. I've bound
-;; together `use-package', `quelpa' and package.el to create my own,
-;; rolling-release, lazily-loaded package management system for Emacs.
+;; Emacs package management is opinionated, and so am I. I've bound together
+;; `use-package', `quelpa' and package.el to create my own, rolling-release,
+;; lazily-loaded package management system for Emacs.
;;
;; The three key commands are:
;;
-;; + `make install` or `doom//packages-install': Installs packages that are
-;; wanted, but not installed.
-;; + `make update` or `doom//packages-update': Updates packages that are
-;; out-of-date.
-;; + `make autoremove` or `doom//packages-autoremove': Uninstalls packages that
-;; are no longer needed.
+;; + `bin/doom install`: Installs packages that are wanted, but not installed.
+;; + `bin/doom update`: Updates packages that are out-of-date.
+;; + `bin/doom autoremove`: Uninstalls packages that are no longer needed.
;;
;; This system reads packages.el files located in each activated module (and one
;; in `doom-core-dir'). These contain `package!' blocks that tell DOOM what
@@ -35,399 +32,120 @@
;; just Emacs. Arguably, my config is still over-complicated, but shhh, it's
;; fine. Everything is fine.
;;
-;; You should be able to use package.el commands without any conflicts, but to
-;; be absolutely certain use the doom alternatives:
-;;
-;; + `package-install': `doom/install-package'
-;; + `package-reinstall': `doom/reinstall-package'
-;; + `package-delete': `doom/delete-package'
-;; + `package-update': `doom/update-package'
-;; + `package-autoremove': `doom//packages-autoremove'
-;; + `package-refresh-contents': `doom/refresh-packages'
+;; You should be able to use package.el commands without any conflicts.
;;
;; See core/autoload/packages.el for more functions.
-(defvar doom-init-p nil
- "Non-nil if doom is done initializing (once `doom-post-init-hook' is done). If
-this is nil after Emacs has started something is wrong.")
-
-(defvar doom-init-time nil
- "The time it took, in seconds, for DOOM Emacs to initialize.")
-
-(defvar doom-modules ()
- "A hash table of enabled modules. Set by `doom-initialize-modules'.")
-
(defvar doom-packages ()
"A list of enabled packages. Each element is a sublist, whose CAR is the
package's name as a symbol, and whose CDR is the plist supplied to its
`package!' declaration. Set by `doom-initialize-packages'.")
(defvar doom-core-packages
- '(persistent-soft use-package quelpa async)
+ '(persistent-soft use-package quelpa async load-env-vars)
"A list of packages that must be installed (and will be auto-installed if
missing) and shouldn't be deleted.")
(defvar doom-disabled-packages ()
"A list of packages that should be ignored by `def-package!'.")
-(defvar doom-reload-hook nil
- "A list of hooks to run when `doom/reload-load-path' is called.")
-
-(defvar doom--site-load-path load-path
- "The load path of built in Emacs libraries.")
-
-(defvar doom--package-load-path ()
- "The load path of package libraries installed via ELPA and QUELPA.")
-
-(defvar doom--base-load-path
- (append (list doom-core-dir doom-modules-dir)
- doom--site-load-path)
- "A backup of `load-path' before it was altered by `doom-initialize'. Used as a
-base by `doom!' and for calculating how many packages exist.")
-
-(defvar doom--refreshed-p nil)
-
(setq package--init-file-ensured t
package-user-dir (expand-file-name "elpa" doom-packages-dir)
+ package-gnupghome-dir (expand-file-name "gpg" doom-packages-dir)
package-enable-at-startup nil
package-archives
'(("gnu" . "https://elpa.gnu.org/packages/")
- ("melpa" . "https://melpa.org/packages/"))
+ ("melpa" . "https://melpa.org/packages/")
+ ("org" . "https://orgmode.org/elpa/"))
;; I omit Marmalade because its packages are manually submitted rather
;; than pulled, so packages are often out of date with upstream.
- ;; security settings
- gnutls-verify-error (not (getenv "INSECURE")) ; you shouldn't use this
- tls-checktrust gnutls-verify-error
- tls-program (list "gnutls-cli --x509cafile %t -p %p %h"
- ;; compatibility fallbacks
- "gnutls-cli -p %p %h"
- "openssl s_client -connect %h:%p -no_ssl2 -no_ssl3 -ign_eof")
-
- use-package-verbose doom-debug-mode
- use-package-minimum-reported-time (if doom-debug-mode 0 0.1)
-
;; Don't track MELPA, we'll use package.el for that
quelpa-checkout-melpa-p nil
quelpa-update-melpa-p nil
quelpa-melpa-recipe-stores nil
quelpa-self-upgrade-p nil
quelpa-verbose doom-debug-mode
- quelpa-dir (expand-file-name "quelpa" doom-packages-dir)
+ quelpa-dir (expand-file-name "quelpa" doom-packages-dir))
- byte-compile-dynamic nil
- byte-compile-verbose doom-debug-mode
- byte-compile-warnings '(not free-vars unresolved noruntime lexical make-local))
+;; accommodate INSECURE setting
+(unless gnutls-verify-error
+ (dolist (archive package-archives)
+ (setcdr archive (replace-regexp-in-string "^https://" "http://" (cdr archive) t nil))))
;;
-;; Bootstrap function
+;; Bootstrapper
+
+(defun doom-initialize-packages (&optional force-p)
+ "Ensures that Doom's package management system, package.el and quelpa are
+initialized, and `doom-packages', `packages-alist' and `quelpa-cache' are
+populated, if they aren't already.
+
+If FORCE-P is non-nil, do it anyway.
+If FORCE-P is 'internal, only (re)populate `doom-packages'.
+
+Use this before any of package.el, quelpa or Doom's package management's API to
+ensure all the necessary package metadata is initialized and available for
+them."
+ (let ((load-prefer-newer t)) ; reduce stale code issues
+ ;; package.el and quelpa handle themselves if their state changes during the
+ ;; current session, but if you change an packages.el file in a module,
+ ;; there's no non-trivial way to detect that, so to reload only
+ ;; doom-packages pass 'internal as FORCE-P or use `doom/reload-packages'.
+ (unless (eq force-p 'internal)
+ ;; `package-alist'
+ (when (or force-p (not (bound-and-true-p package-alist)))
+ (doom-ensure-packages-initialized 'force)
+ (setq load-path (cl-delete-if-not #'file-directory-p load-path)))
+ ;; `quelpa-cache'
+ (when (or force-p (not (bound-and-true-p quelpa-cache)))
+ ;; ensure un-byte-compiled version of quelpa is loaded
+ (unless (featurep 'quelpa)
+ (load (locate-library "quelpa.el") nil t t))
+ (setq quelpa-initialized-p nil)
+ (or (quelpa-setup-p)
+ (error "Could not initialize quelpa"))))
+ ;; `doom-packages'
+ (when (or force-p (not doom-packages))
+ (setq doom-packages (doom-package-list)))))
+
+
;;
+;; Package API
-(defun doom-initialize (&optional force-p)
- "Initialize installed packages (using package.el) and ensure the core packages
-are installed.
-
-If you byte-compile core/core.el, this function will be avoided to speed up
-startup."
- ;; Called early during initialization; only use native (and cl-lib) functions!
- (when (or force-p (not doom-init-p))
- ;; Speed things up with a `load-path' for only the bare essentials
- (let ((load-path doom--base-load-path))
- ;; Ensure core folders exist, otherwise we get errors
- (dolist (dir (list doom-local-dir doom-etc-dir doom-cache-dir doom-packages-dir))
- (unless (file-directory-p dir)
- (make-directory dir t)))
- ;; Ensure package.el is initialized; we use its state
- (setq package-activated-list nil)
- (condition-case _ (package-initialize t)
+(defun doom-ensure-packages-initialized (&optional force-p)
+ "Make sure package.el is initialized."
+ (when (or force-p (not (bound-and-true-p package--initialized)))
+ (require 'package)
+ (setq package-activated-list nil
+ package--initialized nil)
+ (let (byte-compile-warnings)
+ (condition-case _
+ (package-initialize)
('error (package-refresh-contents)
(setq doom--refreshed-p t)
- (package-initialize t)))
- ;; Ensure core packages are installed
- (let ((core-packages (cl-remove-if #'package-installed-p doom-core-packages)))
- (when core-packages
- (message "Installing core packages")
- (unless doom--refreshed-p
- (package-refresh-contents))
- (dolist (package core-packages)
- (let ((inhibit-message t))
- (package-install package))
- (if (package-installed-p package)
- (message "✓ Installed %s" package)
- (error "✕ Couldn't install %s" package)))
- (message "Installing core packages...done")))
- (setq doom-init-p t))))
+ (package-initialize))))))
-(defun doom-initialize-load-path (&optional force-p)
- (when (or force-p (not doom--package-load-path))
- ;; We could let `package-initialize' fill `load-path', but it does more than
- ;; that alone (like load autoload files). If you want something prematurely
- ;; optimizated right, ya gotta do it yourself.
- ;;
- ;; Also, in some edge cases involving package initialization during a
- ;; non-interactive session, `package-initialize' fails to fill `load-path'.
- (setq doom--package-load-path (directory-files package-user-dir t "^[^.]" t)
- load-path (append doom--base-load-path doom--package-load-path))))
-
-(defun doom-initialize-autoloads ()
- "Ensures that `doom-autoload-file' exists and is loaded. Otherwise run
-`doom/reload-autoloads' to generate it."
- (unless (file-exists-p doom-autoload-file)
- (quiet! (doom//reload-autoloads))))
-
-(defun doom-initialize-packages (&optional force-p load-p)
- "Crawls across your emacs.d to fill `doom-modules' (from init.el) and
-`doom-packages' (from packages.el files), if they aren't set already.
-
-If FORCE-P is non-nil, do it even if they are.
-
-This aggressively reloads core autoload files."
- (doom-initialize-load-path force-p)
- (with-temp-buffer ; prevent buffer-local settings from propagating
- (cl-flet
- ((_load
- (file &optional noerror interactive)
- (condition-case-unless-debug ex
- (let ((load-prefer-newer t)
- (noninteractive (not interactive)))
- (load file noerror :nomessage :nosuffix))
- ('error
- (lwarn 'doom-initialize-packages :warning
- "%s in %s: %s"
- (car ex)
- (file-relative-name file doom-emacs-dir)
- (error-message-string ex))))))
- (when (or force-p (not doom-modules))
- (setq doom-modules nil
- doom-packages nil)
- (_load (concat doom-core-dir "core.el") nil 'interactive)
- (_load (expand-file-name "init.el" doom-emacs-dir))
- (when load-p
- (mapc #'_load (file-expand-wildcards (expand-file-name "autoload/*.el" doom-core-dir)))
- (_load (expand-file-name "init.el" doom-emacs-dir) nil 'interactive)))
- (when (or force-p (not doom-packages))
- (setq doom-packages nil)
- (_load (expand-file-name "packages.el" doom-core-dir))
- (cl-loop for (module . submodule) in (doom-module-pairs)
- for path = (doom-module-path module submodule "packages.el")
- do (_load path 'noerror))))))
-
-(defun doom-initialize-modules (modules)
- "Adds MODULES to `doom-modules'. MODULES must be in mplist format.
-
- e.g '(:feature evil :lang emacs-lisp javascript java)"
- (unless doom-modules
- (setq doom-modules (make-hash-table :test #'equal
- :size (+ 5 (length modules))
- :rehash-threshold 1.0)))
- (let (mode)
- (dolist (m modules)
- (cond ((keywordp m) (setq mode m))
- ((not mode) (error "No namespace specified on `doom!' for %s" m))
- ((listp m) (doom-module-enable mode (car m) (cdr m)))
- (t (doom-module-enable mode m))))))
-
-(defun doom-module-path (module submodule &optional file)
- "Get the full path to a module: e.g. :lang emacs-lisp maps to
-~/.emacs.d/modules/lang/emacs-lisp/ and will append FILE if non-nil."
- (when (keywordp module)
- (setq module (substring (symbol-name module) 1)))
- (when (symbolp submodule)
- (setq submodule (symbol-name submodule)))
- (expand-file-name (concat module "/" submodule "/" file)
- doom-modules-dir))
-
-(defun doom-module-from-path (path)
- "Get module cons cell (MODULE . SUBMODULE) for PATH, if possible."
- (when-let* ((path (file-relative-name (file-truename path) (file-truename doom-modules-dir))))
- (let ((segments (split-string path "/")))
- (cons (intern (concat ":" (car segments)))
- (intern (cadr segments))))))
-
-(defun doom-module-paths (&optional append-file)
- "Returns a list of absolute file paths to activated modules, with APPEND-FILE
-added, if the file exists."
- (cl-loop for (module . submodule) in (doom-module-pairs)
- for path = (doom-module-path module submodule append-file)
- if (file-exists-p path)
- collect path))
-
-(defun doom-module-get (module submodule)
- "Returns a list of flags provided for MODULE SUBMODULE."
- (gethash (cons module submodule) doom-modules))
-
-(defun doom-module-enabled-p (module submodule)
- "Returns t if MODULE->SUBMODULE is present in `doom-modules'."
- (and (doom-module-get module submodule) t))
-
-(defun doom-module-enable (module submodule &optional flags)
- "Adds MODULE and SUBMODULE to `doom-modules', overwriting it if it exists.
-
-MODULE is a keyword, SUBMODULE is a symbol. e.g. :lang 'emacs-lisp.
-
-Used by `require!' and `depends-on!'."
- (let ((key (cons module submodule)))
- (puthash key
- (or (doom-enlist flags)
- (gethash key doom-modules)
- '(t))
- doom-modules)))
-
-(defun doom-module-pairs ()
- "Returns `doom-modules' as a list of (MODULE . SUBMODULE) cons cells. The list
-is sorted by order of insertion unless ALL-P is non-nil. If ALL-P is non-nil,
-include all modules, enabled or otherwise."
- (unless (hash-table-p doom-modules)
- (error "doom-modules is uninitialized"))
- (cl-loop for key being the hash-keys of doom-modules
- collect key))
-
-(defun doom-packages--display-benchmark ()
- (message "Doom loaded %s packages across %d modules in %.03fs"
- ;; Certainly imprecise, especially where custom additions to
- ;; load-path are concerned, but I don't mind a [small] margin of
- ;; error in the plugin count in exchange for faster startup.
- (length doom--package-load-path)
- (hash-table-size doom-modules)
- (setq doom-init-time (float-time (time-subtract after-init-time before-init-time)))))
+(defun doom-ensure-core-packages ()
+ "Make sure `doom-core-packages' are installed."
+ (when-let* ((core-packages (cl-remove-if #'package-installed-p doom-core-packages)))
+ (message "Installing core packages")
+ (unless doom--refreshed-p
+ (package-refresh-contents))
+ (dolist (package core-packages)
+ (let ((inhibit-message t))
+ (package-install package))
+ (if (package-installed-p package)
+ (message "✓ Installed %s" package)
+ (error "✕ Couldn't install %s" package)))
+ (message "Installing core packages...done")))
;;
-;; Macros
-;;
+;; Module package macros
-(autoload 'use-package "use-package" nil nil 'macro)
-
-(defmacro doom! (&rest modules)
- "Bootstrap DOOM Emacs.
-
-MODULES is an malformed plist of modules to load."
- (doom-initialize-modules modules)
- `(let (file-name-handler-alist)
- (setq doom-modules ',doom-modules)
- (unless noninteractive
- (message "Doom initialized")
- ,@(cl-loop for (module . submodule) in (doom-module-pairs)
- for module-path = (doom-module-path module submodule)
- collect `(load! init ,module-path t) into inits
- collect `(load! config ,module-path t) into configs
- finally return (append inits configs))
- (when (display-graphic-p)
- (require 'server)
- (unless (server-running-p)
- (server-start)))
- (add-hook 'doom-init-hook #'doom-packages--display-benchmark t)
- (message "Doom modules initialized"))))
-
-(defmacro def-package! (name &rest plist)
- "A thin wrapper around `use-package'."
- ;; Ignore package if NAME is in `doom-disabled-packages'
- (when (and (memq name doom-disabled-packages)
- (not (memq :disabled plist)))
- (setq plist `(:disabled t ,@plist)))
- ;; If byte-compiling, ignore this package if it doesn't meet the condition.
- ;; This avoids false-positive load errors.
- (unless (and (bound-and-true-p byte-compile-current-file)
- (or (and (plist-member plist :if) (not (eval (plist-get plist :if))))
- (and (plist-member plist :when) (not (eval (plist-get plist :when))))
- (and (plist-member plist :unless) (eval (plist-get plist :unless)))))
- `(use-package ,name ,@plist)))
-
-(defmacro def-package-hook! (package when &rest body)
- "Reconfigures a package's `def-package!' block.
-
-Under the hood, this uses use-package's `use-package-inject-hooks'.
-
-PACKAGE is a symbol; the package's name.
-WHEN should be one of the following:
- :pre-init :post-init :pre-config :post-config :disable
-
-If WHEN is :disable then BODY is ignored, and DOOM will be instructed to ignore
-all `def-package!' blocks for PACKAGE.
-
-WARNING: If :pre-init or :pre-config hooks return nil, the original
-`def-package!''s :init/:config block (respectively) is overwritten, so remember
-to have them return non-nil (or exploit that to overwrite Doom's config)."
- (declare (indent defun))
- (cond ((eq when :disable)
- (push package doom-disabled-packages)
- nil)
- ((memq when '(:pre-init :post-init :pre-config :post-config))
- `(progn
- (setq use-package-inject-hooks t)
- (add-hook!
- ',(intern (format "use-package--%s--%s-hook"
- package
- (substring (symbol-name when) 1)))
- ,@body)))
- (t
- (error "'%s' isn't a valid hook for def-package-hook!" when))))
-
-(defmacro load! (filesym &optional path noerror)
- "Load a file relative to the current executing file (`load-file-name').
-
-FILESYM is either a symbol or string representing the file to load. PATH is
-where to look for the file (a string representing a directory path). If omitted,
-the lookup is relative to `load-file-name', `byte-compile-current-file' or
-`buffer-file-name' (in that order).
-
-If NOERROR is non-nil, don't throw an error if the file doesn't exist."
- (cl-assert (symbolp filesym) t)
- (let ((path (or path
- (and load-file-name (file-name-directory load-file-name))
- (and (bound-and-true-p byte-compile-current-file)
- (file-name-directory byte-compile-current-file))
- (and buffer-file-name
- (file-name-directory buffer-file-name))
- (error "Could not detect path to look for '%s' in" filesym)))
- (filename (symbol-name filesym)))
- (let ((file (expand-file-name (concat filename ".el") path)))
- (if (file-exists-p file)
- `(load ,(file-name-sans-extension file) ,noerror
- ,(not doom-debug-mode))
- (unless noerror
- (error "Could not load file '%s' from '%s'" file path))))))
-
-(defmacro require! (module submodule &optional flags reload-p)
- "Loads the module specified by MODULE (a property) and SUBMODULE (a symbol).
-
-The module is only loaded once. If RELOAD-P is non-nil, load it again."
- (when (or reload-p (not (doom-module-enabled-p module submodule)))
- (let ((module-path (doom-module-path module submodule)))
- (if (not (file-directory-p module-path))
- (lwarn 'doom-modules :warning "Couldn't find module '%s %s'"
- module submodule)
- (doom-module-enable module submodule flags)
- `(condition-case-unless-debug ex
- (load! config ,module-path t)
- ('error
- (lwarn 'doom-modules :error
- "%s in '%s %s' -> %s"
- (car ex) ,module ',submodule
- (error-message-string ex))))))))
-
-(defmacro featurep! (module &optional submodule flag)
- "A convenience macro wrapper for `doom-module-enabled-p'. It is evaluated at
-compile-time/macro-expansion time."
- (unless submodule
- (let* ((path (or load-file-name byte-compile-current-file))
- (module-pair (doom-module-from-path path)))
- (unless module-pair
- (error "featurep! couldn't detect what module I'm in! (in %s)" path))
- (setq flag module
- module (car module-pair)
- submodule (cdr module-pair))))
- (if flag
- (and (memq flag (doom-module-get module submodule)) t)
- (doom-module-enabled-p module submodule)))
-
-
-;;
-;; Declarative macros
-;;
-
-(defmacro package! (name &rest plist)
+(cl-defmacro package! (name &rest plist &key built-in recipe pin disable _ignore _freeze)
"Declares a package and how to install it (if applicable).
This macro is declarative and does not load nor install packages. It is used to
@@ -438,285 +156,104 @@ Only use this macro in a module's packages.el file.
Accepts the following properties:
- :recipe RECIPE Takes a MELPA-style recipe (see `quelpa-recipe' in
- `quelpa' for an example); for packages to be installed
- from external sources.
- :pin ARCHIVE-NAME Instructs ELPA to only look for this package in
- ARCHIVE-NAME. e.g. \"org\". Ignored if RECIPE is present.
- :ignore FORM Do not install this package if FORM is non-nil.
- :freeze FORM Do not update this package if FORM is non-nil."
+ :recipe RECIPE
+ Takes a MELPA-style recipe (see `quelpa-recipe' in `quelpa' for an example);
+ for packages to be installed from external sources.
+ :pin ARCHIVE-NAME
+ Instructs ELPA to only look for this package in ARCHIVE-NAME. e.g. \"org\".
+ Ignored if RECIPE is present.
+ :disable BOOL
+ Do not install or update this package AND disable all of its `def-package!'
+ blocks.
+ :ignore FORM
+ Do not install this package.
+ :freeze FORM
+ Do not update this package if FORM is non-nil.
+ :built-in BOOL
+ Same as :ignore if the package is a built-in Emacs package.
+
+Returns t if package is successfully registered, and nil if it was disabled
+elsewhere."
(declare (indent defun))
- (let* ((old-plist (assq name doom-packages))
- (pkg-recipe (or (plist-get plist :recipe)
- (and old-plist (plist-get old-plist :recipe))))
- (pkg-pin (or (plist-get plist :pin)
- (and old-plist (plist-get old-plist :pin)))))
- (when pkg-recipe
- (when (= 0 (% (length pkg-recipe) 2))
- (plist-put plist :recipe (cons name pkg-recipe)))
- (when pkg-pin
- (plist-put plist :pin nil)))
- (dolist (prop '(:ignore :freeze))
- (when-let* ((val (plist-get plist prop)))
- (plist-put plist prop (eval val))))
- `(progn
- (when ,(and pkg-pin t)
- (cl-pushnew (cons ',name ,pkg-pin) package-pinned-packages
- :test #'eq :key #'car))
- (when ,(and old-plist t)
- (assq-delete-all ',name doom-packages))
- (push ',(cons name plist) doom-packages))))
+ (doom--assert-stage-p 'packages #'package!)
+ (let ((old-plist (cdr (assq name doom-packages))))
+ (when recipe
+ (when (cl-evenp (length recipe))
+ (setq plist (plist-put plist :recipe (cons name recipe))))
+ (setq pin nil
+ plist (plist-put plist :pin nil)))
+ (let ((module-list (plist-get old-plist :modules))
+ (module (or doom--current-module
+ (let ((file (FILE!)))
+ (cond ((file-in-directory-p file doom-private-dir)
+ (list :private))
+ ((file-in-directory-p file doom-core-dir)
+ (list :core))
+ ((doom-module-from-path file)))))))
+ (doom-log "Registered package '%s'%s"
+ name (if recipe (format " with recipe %s" recipe) ""))
+ (unless (member module module-list)
+ (setq module-list (append module-list (list module) nil)
+ plist (plist-put plist :modules module-list))))
+ (when (and built-in (locate-library (symbol-name name) nil doom-site-load-path))
+ (doom-log "Ignoring built-in package '%s'" name)
+ (setq plist (plist-put plist :ignore t)))
+ (while plist
+ (unless (null (cadr plist))
+ (setq old-plist (plist-put old-plist (car plist) (cadr plist))))
+ (pop plist)
+ (pop plist))
+ (setq plist old-plist)
+ (macroexp-progn
+ (append (when disable
+ (doom-log "Disabling package '%s'" name)
+ `((add-to-list 'doom-disabled-packages ',name nil 'eq)))
+ (when pin
+ (doom-log "Pinning package '%s' to '%s'" name pin)
+ `((setf (alist-get ',name package-pinned-packages) ,pin)))
+ `((setf (alist-get ',name doom-packages) ',plist)
+ (not (memq ',name doom-disabled-packages)))))))
-(defmacro depends-on! (module submodule)
- "Declares that this module depends on another.
+(defmacro packages! (&rest packages)
+ "A convenience macro for `package!' for declaring multiple packages at once.
-Only use this macro in a module's packages.el file.
+Only use this macro in a module's packages.el file."
+ (doom--assert-stage-p 'packages #'packages!)
+ (macroexp-progn
+ (cl-loop for desc in packages
+ collect (macroexpand `(package! ,@(doom-enlist desc))))))
-MODULE is a keyword, and SUBMODULE is a symbol. Under the hood, this simply
-loads MODULE SUBMODULE's packages.el file."
- (doom-module-enable module submodule)
- `(load! packages ,(doom-module-path module submodule) t))
+(defmacro disable-packages! (&rest packages)
+ "A convenience macro like `package!', but allows you to disable multiple
+packages at once.
+Only use this macro in a module's packages.el file."
+ (doom--assert-stage-p 'packages #'disable-packages!)
+ (macroexp-progn
+ (cl-loop for pkg in packages
+ collect (macroexpand `(package! ,pkg :disable t)))))
-;;
-;; Commands
-;;
+(defmacro depends-on! (category module &rest flags)
+ "Declares that this CATEGORY depends on another.
-(defun doom-packages--read-if-cookies (file)
- "Returns the value of the ;;;###if predicate form in FILE."
- (with-temp-buffer
- (insert-file-contents-literally file nil 0 256)
- (if (and (re-search-forward "^;;;###if " nil t)
- (<= (line-number-at-pos) 3))
- (let ((load-file-name file))
- (eval (sexp-at-point)))
- t)))
+Emits a warning if CATEGORY MODULE isn't enabled, or is enabled without FLAGS.
-(defun doom-packages--async-run (fn)
- (let* ((default-directory doom-emacs-dir)
- (compilation-filter-hook
- (list (lambda () (ansi-color-apply-on-region compilation-filter-start (point))))))
- (compile (format "%s --quick --batch -l core/core.el -f %s"
- (executable-find "emacs")
- (symbol-name fn)))
- (while compilation-in-progress
- (sit-for 1))))
-
-(defun doom//reload-load-path ()
- "Reload `load-path' and recompile files (if necessary).
-
-Use this when `load-path' is out of sync with your plugins. This should only
-happen if you manually modify/update/install packages from outside Emacs, while
-an Emacs session is running.
-
-This isn't necessary if you use Doom's package management commands because they
-call `doom/reload-load-path' remotely (through emacsclient)."
- (interactive)
- (byte-recompile-file (expand-file-name "core.el" doom-core-dir) t)
- (cond (noninteractive
- (require 'server)
- (when (server-running-p)
- (message "Reloading active Emacs session...")
- (server-eval-at server-name '(doom//reload-load-path))))
- ((let ((noninteractive t))
- (doom-initialize-load-path t)
- (message "%d packages reloaded" (length doom--package-load-path))
- (run-hooks 'doom-reload-hook)))))
-
-(defun doom//reload-autoloads ()
- "Refreshes the autoloads.el file, specified by `doom-autoload-file'.
-
-It scans and reads core/autoload/*.el, modules/*/*/autoload.el and
-modules/*/*/autoload/*.el, and generates an autoloads file at the path specified
-by `doom-autoload-file'. This file tells Emacs where to find lazy-loaded
-functions.
-
-This should be run whenever init.el or an autoload file is modified. Running
-'make autoloads' from the commandline executes this command."
- (interactive)
- ;; This function must not use autoloaded functions or external dependencies.
- ;; It must assume nothing is set up!
- (if (not noninteractive)
- ;; This is done in another instance to protect the current session's
- ;; state. `doom-initialize-packages' will have side effects otherwise.
- (and (doom-packages--async-run 'doom//reload-autoloads)
- (load doom-autoload-file))
- (doom-initialize-packages t)
- (let ((targets
- (file-expand-wildcards
- (expand-file-name "autoload/*.el" doom-core-dir))))
- (dolist (path (doom-module-paths))
- (let ((auto-dir (expand-file-name "autoload" path))
- (auto-file (expand-file-name "autoload.el" path)))
- (when (file-exists-p auto-file)
- (push auto-file targets))
- (when (file-directory-p auto-dir)
- (dolist (file (directory-files-recursively auto-dir "\\.el$"))
- (push file targets)))))
- (when (file-exists-p doom-autoload-file)
- (delete-file doom-autoload-file)
- (message "Deleted old autoloads.el"))
- (dolist (file (reverse targets))
- (message
- (cond ((not (doom-packages--read-if-cookies file))
- "⚠ Ignoring %s")
- ((update-file-autoloads file nil doom-autoload-file)
- "✕ Nothing in %s")
- (t
- "✓ Scanned %s"))
- (file-relative-name file doom-emacs-dir)))
- (make-directory (file-name-directory doom-autoload-file) t)
- (let ((buf (get-file-buffer doom-autoload-file))
- current-sexp)
- (unwind-protect
- (condition-case-unless-debug ex
- (with-current-buffer buf
- (save-buffer)
- (goto-char (point-min))
- (while (re-search-forward "^(" nil t)
- (save-excursion
- (backward-char)
- (setq current-sexp (read (thing-at-point 'sexp t)))
- (eval current-sexp t))
- (forward-char))
- (message "Finished generating autoloads.el!"))
- ('error
- (delete-file doom-autoload-file)
- (error "Error in autoloads.el: (%s %s ...) %s -- %s"
- (nth 0 current-sexp)
- (nth 1 current-sexp)
- (car ex) (error-message-string ex))))
- (kill-buffer buf))))))
-
-(defun doom//byte-compile (&optional modules recompile-p)
- "Byte compiles your emacs configuration.
-
-init.el is always byte-compiled by this.
-
-If MODULES is specified (a list of module strings, e.g. \"lang/php\"), those are
-byte-compiled. Otherwise, all enabled modules are byte-compiled, including Doom
-core. It always ignores unit tests and files with `no-byte-compile' enabled.
-
-Doom was designed to benefit from byte-compilation, but the process may take a
-while. Also, while your config files are byte-compiled, changes to them will not
-take effect! Use `doom//clean-byte-compiled-files' or `make clean' to remove
-these files.
-
-If RECOMPILE-P is non-nil, only recompile out-of-date files."
- (interactive
- (list nil current-prefix-arg))
- (let ((default-directory doom-emacs-dir)
- (recompile-p (or recompile-p
- (and (member "-r" (cdr argv)) t))))
- (if (not noninteractive)
- ;; This is done in another instance to protect the current session's
- ;; state. `doom-initialize-packages' will have side effects otherwise.
- (doom-packages--async-run 'doom//byte-compile)
- (let ((total-ok 0)
- (total-fail 0)
- (total-noop 0)
- (modules (or modules (cdr argv)))
- compile-targets)
- (doom-initialize-packages t t)
- (setq compile-targets
- (cl-loop for target
- in (or modules (append (list doom-core-dir) (doom-module-paths)))
- if (equal target "core")
- nconc (nreverse (directory-files-recursively doom-core-dir "\\.el$"))
- else if (file-directory-p target)
- nconc (nreverse (directory-files-recursively target "\\.el$"))
- else if (file-directory-p (expand-file-name target doom-modules-dir))
- nconc (nreverse (directory-files-recursively (expand-file-name target doom-modules-dir) "\\.el$"))
- else if (file-exists-p target)
- collect target
- finally do (setq argv nil)))
- (unless compile-targets
- (error "No targets to compile"))
- (let ((use-package-expand-minimally t))
- (push (expand-file-name "init.el" doom-emacs-dir) compile-targets)
- (condition-case ex
- (progn
- (dolist (target compile-targets)
- (when (or (not recompile-p)
- (let ((elc-file (byte-compile-dest-file target)))
- (and (file-exists-p elc-file)
- (file-newer-than-file-p file elc-file))))
- (let ((result (if (doom-packages--read-if-cookies target)
- (byte-compile-file target)
- 'no-byte-compile))
- (short-name (file-relative-name target doom-emacs-dir)))
- (cl-incf
- (cond ((eq result 'no-byte-compile)
- (message! (dark (white "⚠ Ignored %s" short-name)))
- total-noop)
- ((null result)
- (message! (red "✕ Failed to compile %s" short-name))
- total-fail)
- (t
- (message! (green "✓ Compiled %s" short-name))
- (quiet! (load target t t))
- total-ok))))))
- (message!
- (bold
- (color (if (= total-fail 0) 'green 'red)
- "%s %s file(s) %s"
- (if recompile-p "Recompiled" "Compiled")
- (format "%d/%d" total-ok (- (length compile-targets) total-noop))
- (format "(%s ignored)" total-noop)))))
- (error
- (message! (red "\n%%s\n\n%%s\n\n%%s")
- "There were breaking errors."
- (error-message-string ex)
- "Reverting changes...")
- (doom//clean-byte-compiled-files)
- (message! (green "Finished (nothing was byte-compiled)")))))))))
-
-(defun doom//byte-compile-core (&optional recompile-p)
- "Byte compile the core Doom files.
-
-This is faster than `doom//byte-compile', still yields considerable performance
-benefits, and is more reliable in an ever-changing Emacs config (since you won't
-likely change core files directly).
-
-If RECOMPILE-P is non-nil, only recompile out-of-date core files."
- (interactive "P")
- (if (not noninteractive)
- ;; This is done in another instance to protect the current session's
- ;; state. `doom-initialize-packages' will have side effects otherwise.
- (doom-packages--async-run 'doom//byte-compile-core)
- (doom//byte-compile (list "core") recompile-p)))
-
-(defun doom//byte-recompile-plugins ()
- "Recompile all installed plugins. If you're getting odd errors after upgrading
-(or downgrading) Emacs, this may fix it."
- (interactive)
- (byte-recompile-directory package-user-dir 0 t))
-
-(defun doom//clean-byte-compiled-files ()
- "Delete all the compiled elc files in your Emacs configuration. This excludes
-compiled packages.'"
- (interactive)
- (let ((targets (append (list (expand-file-name "init.elc" doom-emacs-dir))
- (directory-files-recursively doom-core-dir "\\.elc$")
- (directory-files-recursively doom-modules-dir "\\.elc$")))
- (default-directory doom-emacs-dir))
- (unless (cl-loop for path in targets
- if (file-exists-p path)
- collect path
- and do (delete-file path)
- and do (message "✓ Deleted %s" (file-relative-name path)))
- (message "Everything is clean"))))
-
-
-;;
-;; Package.el modifications
-;;
-
-;; Updates QUELPA after deleting a package
-(advice-add #'package-delete :after #'doom*package-delete)
-
-;; It isn't safe to use `package-autoremove', so get rid of it
-(advice-add #'package-autoremove :override #'doom//packages-autoremove)
+Only use this macro in a CATEGORY's packages.el file."
+ (doom--assert-stage-p 'packages #'depends-on!)
+ `(let ((desired-flags ',flags))
+ (unless (doom-module-locate-path ,category ',module)
+ (error "The '%s %s' module is required, but doesn't exist"
+ ,category ',module))
+ (unless (doom-module-p ,category ',module)
+ (error "The '%s %s' module is required, but disabled"
+ ,category ',module))
+ (let ((flags (doom-module-get ,category ',module :flags)))
+ (when (and desired-flags
+ (/= (length (cl-intersection flags desired-flags))
+ (length desired-flags)))
+ (error "The '%s %s' module is missing the required %S flag(s)"
+ ,category ',module
+ (cl-set-difference desired-flags flags))))))
(provide 'core-packages)
;;; core-packages.el ends here
diff --git a/core/core-popups.el b/core/core-popups.el
deleted file mode 100644
index 5ca2fd044..000000000
--- a/core/core-popups.el
+++ /dev/null
@@ -1,538 +0,0 @@
-;;; core-popups.el -*- lexical-binding: t; -*-
-
-;; I want a "real"-buffer-first policy in my Emacsian utpoia; popup buffers
-;; ought to be second-class citizens to "real" buffers. No need for a wall or
-;; controversial immigration policies -- all we need is `shackle' (and it will
-;; actually work).
-;;
-;; The gist is: popups should be displayed on one side of the frame, away from
-;; 'real' buffers. They should be easy to dispose of when we don't want to see
-;; them and easily brought back in case we change our minds. Also, popups should
-;; typically have no mode-line.
-;;
-;; Be warned, this requires a lot of hackery voodoo that could break with an
-;; emacs update or an update to any of the packages it tries to tame (like helm
-;; or org-mode).
-
-(defvar doom-popup-history nil
- "A list of popups that were last closed. Used by `doom/popup-restore' and
-`doom*popups-save'.")
-
-(defvar doom-popup-other-window nil
- "The last window selected before a popup was opened.")
-
-(defvar doom-popup-no-fringes t
- "If non-nil, disable fringes in popup windows.")
-
-(defvar doom-popup-windows ()
- "A list of open popup windows.")
-
-(defvar-local doom-popup-rules nil
- "The shackle rule that caused this buffer to be recognized as a popup. Don't
-edit this directly.")
-(put 'doom-popup-rules 'permanent-local t)
-
-(defvar doom-popup-window-parameters
- '(:noesc :modeline :autokill :autoclose :autofit :static)
- "A list of window parameters that are set (and cleared) when `doom-popup-mode
-is enabled/disabled.'")
-
-(defvar doom-popup-remember-history t
- "Don't modify this directly. If non-nil, DOOM will remember the last popup(s)
-that was/were open in `doom-popup-history'.")
-
-(defvar doom-popup-inhibit-autokill nil
- "Don't modify this directly. When it is non-nil, no buffers will be killed
-when their associated popup windows are closed, despite their :autokill
-property.")
-
-(defvar doom-popup-mode-map (make-sparse-keymap)
- "Active keymap in popup windows.")
-
-
-(def-setting! :popup (&rest rules)
- "Prepend a new popup rule to `shackle-rules' (see for format details).
-
-Several custom properties have been added that are not part of shackle, but are
-recognized by DOOM's popup system. They are:
-
-:noesc If non-nil, the popup won't be closed if you press ESC from *inside*
- its window. Used by `doom/popup-close-maybe'.
-
-:modeline By default, mode-lines are hidden in popups unless this is non-nil.
- If it is a symbol, it'll use `doom-modeline' to fetch a modeline
- config (in `doom-popup-mode').
-
-:autokill If non-nil, the popup's buffer will be killed when the popup is
- closed. Used by `doom*delete-popup-window'. NOTE
- `doom/popup-restore' can't restore non-file popups that have an
- :autokill property.
-
-:autoclose If non-nil, close popup if ESC is pressed from outside the popup
- window.
-
-:autofit If non-nil, resize the popup to fit its content. Uses the value of
- the :size property as the maximum height/width. This will not work
- if the popup has no content when displayed.
-
-:static If non-nil, don't treat this window like a popup. This makes it
- impervious to being automatically closed or tracked in popup
- history. Excellent for permanent sidebars."
- (if (cl-every #'listp (mapcar #'doom-unquote rules))
- `(setq shackle-rules (nconc (list ,@rules) shackle-rules))
- `(push (list ,@rules) shackle-rules)))
-
-
-;;
-;;
-;;
-
-;; (defvar doom-popup-parameters
-;; '(:esc :modeline :transient :fit :align :size)
-;; "TODO")
-
-;; (defvar doom-popup-whitelist
-;; '(("^ ?\\*" :size 15 :noselect t :autokill t :autoclose t))
-;; "TODO")
-
-(defvar doom-popup-blacklist
- '("^\\*magit")
- "TODO")
-
-
-;;
-;; Bootstrap
-;;
-
-(def-package! shackle
- :init
- (setq shackle-default-alignment 'below
- shackle-default-size 8
- shackle-rules
- '(("^\\*eww" :regexp t :size 0.5 :select t :autokill t :noesc t)
- ("^\\*ftp " :noselect t :autokill t :noesc t)
- ;; doom
- ("^\\*doom:scratch" :regexp t :size 15 :noesc t :select t :modeline t :autokill t :static t)
- ("^\\*doom:" :regexp t :size 0.35 :noesc t :select t)
- ("^ ?\\*doom " :regexp t :noselect t :autokill t :autoclose t :autofit t)
- ;; built-in (emacs)
- ("*compilation*" :size 0.25 :noselect t :autokill t :autoclose t)
- ("*ert*" :same t :modeline t)
- ("*info*" :size 0.5 :select t :autokill t)
- ("*Backtrace*" :size 20 :noselect t)
- ("*Warnings*" :size 12 :noselect t :autofit t)
- ("*Messages*" :size 12 :noselect t)
- ("*Help*" :size 0.3 :autokill t)
- ("^\\*.*Shell Command.*\\*$" :regexp t :size 20 :noselect t :autokill t)
- (apropos-mode :size 0.3 :autokill t :autoclose t)
- (Buffer-menu-mode :size 20 :autokill t)
- (comint-mode :noesc t)
- (grep-mode :size 25 :noselect t :autokill t)
- (profiler-report-mode :size 0.3 :regexp t :autokill t :modeline minimal)
- (tabulated-list-mode :noesc t)
- ("^ ?\\*" :regexp t :size 15 :noselect t :autokill t :autoclose t)))
-
- :config
- ;; NOTE This is a temporary fix while I rewrite core-popups
- (defun doom-display-buffer-condition (buffer _action)
- (and (cl-loop for re in doom-popup-blacklist
- when (string-match-p re buffer)
- return nil
- finally return t)
- (shackle-match buffer)))
-
- (defun doom-display-buffer-action (buffer alist)
- (shackle-display-buffer buffer alist (shackle-match buffer)))
-
- (defun doom|autokill-popups ()
- (or (not (doom-popup-p))
- (prog1 (when (and (not doom-popup-inhibit-autokill)
- (plist-get doom-popup-rules :autokill))
- (doom-popup-mode -1)
- (when-let* ((process (get-buffer-process (current-buffer))))
- (set-process-query-on-exit-flag process nil))
- t))))
-
- (add-hook! doom-post-init
- (setq display-buffer-alist
- (cons '(doom-display-buffer-condition doom-display-buffer-action)
- display-buffer-alist))
- (add-hook 'kill-buffer-query-functions #'doom|autokill-popups))
-
- ;; no modeline in popups
- (add-hook 'doom-popup-mode-hook #'doom|hide-modeline-in-popup)
- ;; ensure every rule without an :align, :same or :frame property has an
- ;; implicit :align (see `shackle-default-alignment')
- (advice-add #'shackle--match :filter-return #'doom*shackle-always-align)
-
- ;; bootstrap popup system
- (advice-add #'shackle-display-buffer :around #'doom*popup-init)
- (advice-add #'balance-windows :around #'doom*popups-save)
- (advice-add #'delete-window :before #'doom*delete-popup-window)
-
- ;; Tell `window-state-get' and `current-window-configuration' to recognize
- ;; these custom parameters. Helpful for `persp-mode' and persisting window
- ;; configs that have popups in them.
- (dolist (param `(popup ,@doom-popup-window-parameters))
- (push (cons param 'writable) window-persistent-parameters))
-
- (let ((map doom-popup-mode-map))
- (define-key map [escape] #'doom/popup-close-maybe)
- (define-key map (kbd "ESC") #'doom/popup-close-maybe)
- (define-key map [remap quit-window] #'doom/popup-close-maybe)
- (define-key map [remap doom/kill-this-buffer] #'doom/popup-close-maybe)
- (define-key map [remap split-window-right] #'ignore)
- (define-key map [remap split-window-below] #'ignore)
- (define-key map [remap split-window-horizontally] #'ignore)
- (define-key map [remap split-window-vertically] #'ignore)
- (define-key map [remap mouse-split-window-horizontally] #'ignore)
- (define-key map [remap mouse-split-window-vertically] #'ignore)))
-
-
-;;
-;; Hacks
-;;
-
-(progn ; hacks for built-in functions
- (defun doom*suppress-pop-to-buffer-same-window (orig-fn &rest args)
- (cl-letf (((symbol-function 'pop-to-buffer-same-window)
- (symbol-function 'pop-to-buffer)))
- (apply orig-fn args)))
- (advice-add #'info :around #'doom*suppress-pop-to-buffer-same-window)
- (advice-add #'eww :around #'doom*suppress-pop-to-buffer-same-window)
- (advice-add #'eww-browse-url :around #'doom*suppress-pop-to-buffer-same-window)
-
- (defun doom*popup-buffer-menu (&optional arg)
- "Open `buffer-menu' in a popup window."
- (interactive "P")
- (with-selected-window (doom-popup-buffer (list-buffers-noselect arg))
- (setq mode-line-format "Commands: d, s, x, u; f, o, 1, 2, m, v; ~, %; q to quit; ? for help.")))
- (advice-add #'buffer-menu :override #'doom*popup-buffer-menu))
-
-
-(after! comint
- (defun doom|popup-close-comint-buffer ()
- (when (and (doom-popup-p)
- (derived-mode-p 'comint-mode)
- (not (process-live-p (get-buffer-process (current-buffer)))))
- (delete-window)))
- (add-hook '+evil-esc-hook #'doom|popup-close-comint-buffer t))
-
-
-(after! eshell
- ;; By tying buffer life to its process, we ensure that we land back in the
- ;; eshell buffer after term dies. May cause problems with short-lived
- ;; processes.
- ;; FIXME replace with a 'kill buffer' keybinding.
- (setq eshell-destroy-buffer-when-process-dies t)
-
- ;; When eshell runs a visual command (see `eshell-visual-commands'), it spawns
- ;; a term buffer to run it in, but where it spawns it is the problem...
- (defun doom*eshell-undedicate-popup (orig-fn &rest args)
- "Force spawned term buffer to share with the eshell popup (if necessary)."
- (when (doom-popup-p)
- (set-window-dedicated-p nil nil)
- (add-transient-hook! #'eshell-query-kill-processes :after
- (set-window-dedicated-p nil t)))
- (apply orig-fn args))
- (advice-add #'eshell-exec-visual :around #'doom*eshell-undedicate-popup))
-
-
-(after! evil
- (let ((map doom-popup-mode-map))
- (define-key map [remap evil-window-delete] #'doom/popup-close-maybe)
- (define-key map [remap evil-save-modified-and-close] #'doom/popup-close-maybe)
- (define-key map [remap evil-window-move-very-bottom] #'doom/popup-move-bottom)
- (define-key map [remap evil-window-move-very-top] #'doom/popup-move-top)
- (define-key map [remap evil-window-move-far-left] #'doom/popup-move-left)
- (define-key map [remap evil-window-move-far-right] #'doom/popup-move-right)
- (define-key map [remap evil-window-split] #'ignore)
- (define-key map [remap evil-window-vsplit] #'ignore))
-
- (defun doom|popup-close-maybe ()
- "If current window is a popup, close it. If minibuffer is open, close it. If
-not in a popup, close all popups with an :autoclose property."
- (if (doom-popup-p)
- (unless (doom-popup-property :noesc)
- (delete-window))
- (doom/popup-close-all)))
- (add-hook '+evil-esc-hook #'doom|popup-close-maybe t)
-
- ;; Make evil-mode cooperate with popups
- (advice-add #'evil-command-window :override #'doom*popup-evil-command-window)
- (advice-add #'evil-command-window-execute :override #'doom*popup-evil-command-window-execute)
-
- (defun doom*popup-evil-command-window (hist cmd-key execute-fn)
- "The evil command window has a mind of its own (uses `switch-to-buffer'). We
-monkey patch it to use pop-to-buffer, and to remember the previous window."
- (when (eq major-mode 'evil-command-window-mode)
- (user-error "Cannot recursively open command line window"))
- (dolist (win (window-list))
- (when (equal (buffer-name (window-buffer win))
- "*Command Line*")
- (kill-buffer (window-buffer win))
- (delete-window win)))
- (setq evil-command-window-current-buffer (current-buffer))
- (ignore-errors (kill-buffer "*Command Line*"))
- (with-current-buffer (pop-to-buffer "*Command Line*")
- (setq-local evil-command-window-execute-fn execute-fn)
- (setq-local evil-command-window-cmd-key cmd-key)
- (evil-command-window-mode)
- (evil-command-window-insert-commands hist)))
-
- (defun doom*popup-evil-command-window-execute ()
- "Execute the command under the cursor in the appropriate buffer, rather than
-the command buffer."
- (interactive)
- (let ((result (buffer-substring (line-beginning-position)
- (line-end-position)))
- (execute-fn evil-command-window-execute-fn)
- (popup (selected-window)))
- (select-window doom-popup-other-window)
- (unless (equal evil-command-window-current-buffer (current-buffer))
- (user-error "Originating buffer is no longer active"))
- ;; (kill-buffer "*Command Line*")
- (doom/popup-close popup)
- (funcall execute-fn result)
- (setq evil-command-window-current-buffer nil)))
-
- ;; Don't mess with popups
- (advice-add #'doom-evil-window-move :around #'doom*popups-save)
- (advice-add #'evil-window-move-very-bottom :around #'doom*popups-save)
- (advice-add #'evil-window-move-very-top :around #'doom*popups-save)
- (advice-add #'evil-window-move-far-left :around #'doom*popups-save)
- (advice-add #'evil-window-move-far-right :around #'doom*popups-save)
-
- ;; Don't block moving to/from popup windows
- (defun doom*ignore-window-parameters-in-popups (dir &optional arg window)
- (window-in-direction (cond ((eq dir 'up) 'above)
- ((eq dir 'down) 'below)
- (t dir))
- window t arg windmove-wrap-around t))
- (advice-add #'windmove-find-other-window :override #'doom*ignore-window-parameters-in-popups))
-
-
-(after! helm
- ;; Helm tries to clean up after itself, but shackle has already done this,
- ;; causing problems. This fixes that. To reproduce, add a helm rule in
- ;; `shackle-rules', open two splits side-by-side, move to the buffer on the
- ;; right and invoke helm. It will close all but the left-most buffer.
- (setq-default helm-reuse-last-window-split-state t
- helm-split-window-in-side-p t)
-
- (after! helm-swoop
- (setq helm-swoop-split-window-function #'pop-to-buffer))
-
- (after! helm-ag
- ;; This prevents helm-ag from switching between windows and buffers.
- (defun doom*helm-ag-edit-done (orig-fn &rest args)
- (cl-letf (((symbol-function 'select-window) #'ignore))
- (apply orig-fn args))
- (doom/popup-close))
- (advice-add #'helm-ag--edit-commit :around #'doom*helm-ag-edit-done)
- (advice-add #'helm-ag--edit-abort :around #'doom*helm-ag-edit-done)
-
- (defun doom*helm-ag-edit (orig-fn &rest args)
- (cl-letf (((symbol-function 'other-window) #'ignore)
- ((symbol-function 'switch-to-buffer) #'doom-popup-buffer))
- (apply orig-fn args)
- (with-current-buffer (get-buffer "*helm-ag-edit*")
- (use-local-map helm-ag-edit-map))))
- (advice-add #'helm-ag--edit :around #'doom*helm-ag-edit)))
-
-
-(defsubst doom--switch-from-popup (location)
- (doom/popup-close)
- (switch-to-buffer (car location) nil t)
- (if (not (cdr location))
- (message "Unable to find location in file")
- (goto-char (cdr location))
- (recenter)))
-
-(after! help-mode
- ;; Help buffers use `other-window' to decide where to open followed links,
- ;; which can be unpredictable. It should *only* replace the original buffer we
- ;; opened the popup from. To fix this these three button types need to be
- ;; redefined to set aside the popup before following a link.
- (define-button-type 'help-function-def
- :supertype 'help-xref
- 'help-function
- (lambda (fun file)
- (require 'find-func)
- (when (eq file 'C-source)
- (setq file (help-C-file-name (indirect-function fun) 'fun)))
- (doom--switch-from-popup (find-function-search-for-symbol fun nil file))))
-
- (define-button-type 'help-variable-def
- :supertype 'help-xref
- 'help-function
- (lambda (var &optional file)
- (when (eq file 'C-source)
- (setq file (help-C-file-name var 'var)))
- (doom--switch-from-popup (find-variable-noselect var file))))
-
- (define-button-type 'help-face-def
- :supertype 'help-xref
- 'help-function
- (lambda (fun file)
- (require 'find-func)
- (doom--switch-from-popup (find-function-search-for-symbol fun 'defface file)))))
-
-
-(after! magit
- (add-hook 'magit-mode-hook #'doom-hide-modeline-mode))
-
-
-(after! mu4e
- (defun doom*mu4e-popup-window (buf _height)
- (doom-popup-buffer buf '(:size 10 :noselect t))
- buf)
- (advice-add #'mu4e~temp-window :override #'doom*mu4e-popup-window))
-
-
-(after! multi-term
- (setq multi-term-buffer-name "doom:terminal"))
-
-
-(after! neotree
- ;; Neotree has its own window/popup management built-in, which is difficult to
- ;; police. For example, switching perspectives will cause neotree to forget it
- ;; is a neotree pane.
- ;;
- ;; By handing neotree over to shackle, which is better integrated into the
- ;; rest of my config (and persp-mode), this is no longer a problem.
- (set! :popup " *NeoTree*" :align neo-window-position :size neo-window-width :static t)
-
- (defun +evil-neotree-display-fn (buf _alist)
- "Hand neotree off to shackle."
- (let ((win (doom-popup-buffer buf)))
- (setq neo-global--buffer (window-buffer win)
- neo-global--window win)))
- (setq neo-display-action '(+evil-neotree-display-fn))
-
- (defun +evil|neotree-fix-popup ()
- "Repair neotree state whenever its popup state is restored. This ensures
-that `doom*popup-save' won't break it."
- (when (equal (buffer-name) neo-buffer-name)
- (setq neo-global--window (selected-window))
- ;; Fix neotree shrinking when closing nearby vertical splits
- (when neo-window-fixed-size
- (doom-resize-window neo-global--window neo-window-width t t))))
- (add-hook 'doom-popup-mode-hook #'+evil|neotree-fix-popup))
-
-
-(after! persp-mode
- (defun doom*persp-mode-restore-popups (&rest _)
- "Restore popup windows when loading a perspective from file."
- (dolist (window (window-list))
- (when-let* ((plist (doom-popup-properties window)))
- (with-selected-window window
- (unless doom-popup-mode
- (setq-local doom-popup-rules plist)
- (doom-popup-mode +1))))))
- (advice-add #'persp-load-state-from-file :after #'doom*persp-mode-restore-popups))
-
-
-(after! quickrun
- ;; don't auto-focus quickrun windows, shackle handles that
- (setq quickrun-focus-p nil))
-
-
-(after! twittering-mode
- (setq twittering-pop-to-buffer-function #'pop-to-buffer))
-
-
-(after! wgrep
- ;; close the popup after you're done with a wgrep buffer
- (advice-add #'wgrep-abort-changes :after #'doom/popup-close)
- (advice-add #'wgrep-finish-edit :after #'doom/popup-close))
-
-
-(after! xref
- (defun doom*xref-follow-and-close (orig-fn &rest args)
- "Jump to the xref on the current line, select its window and close the popup
-you came from."
- (interactive)
- (let ((popup-p (doom-popup-p))
- (window (selected-window)))
- (apply orig-fn args)
- (when popup-p (doom/popup-close window))))
- (advice-add #'xref-goto-xref :around #'doom*xref-follow-and-close))
-
-
-;;
-;; Major modes
-;;
-
-(after! plantuml-mode
- (defun doom*plantuml-preview-in-popup-window (orig-fn &rest args)
- (save-window-excursion
- (apply orig-fn args))
- (pop-to-buffer plantuml-preview-buffer))
- (advice-add #'plantuml-preview-string
- :around #'doom*plantuml-preview-in-popup-window))
-
-;; Ensure these settings are loaded as late as possible, giving other modules a
-;; chance to reconfigure org popup settings before the defaults kick in.
-(defun doom|init-org-popups ()
- (add-hook! org-load
- (set! :popup
- '("*Calendar*" :size 0.4 :noselect t)
- '(" *Org todo*" :size 5 :noselect t)
- '("*Org Note*" :size 10)
- '("*Org Select*" :size 20 :noselect t)
- '("*Org Links*" :size 5 :noselect t)
- '("*Org Export Dispatcher*" :noselect t)
- '(" *Agenda Commands*" :noselect t)
- '("^\\*Org Agenda" :regexp t :size 20)
- '("*Org Clock*" :noselect t)
- '("^\\*Org Src" :regexp t :size 0.35 :noesc t)
- '("*Edit Formulas*" :size 10)
- '("^\\*Org-Babel" :regexp t :size 25 :noselect t)
- '("^CAPTURE.*\\.org$" :regexp t :size 20))
-
- ;; Org has a scorched-earth window management system I'm not fond of. i.e.
- ;; it kills all windows and monopolizes the frame. No thanks. We can do
- ;; better with shackle's help.
- (defun doom*suppress-delete-other-windows (orig-fn &rest args)
- (cl-letf (((symbol-function 'delete-other-windows)
- (symbol-function 'ignore)))
- (apply orig-fn args)))
- (advice-add #'org-add-log-note :around #'doom*suppress-delete-other-windows)
- (advice-add #'org-capture-place-template :around #'doom*suppress-delete-other-windows)
- (advice-add #'org-export--dispatch-ui :around #'doom*suppress-delete-other-windows)
-
- ;; Hand off the src-block window to a shackle popup window.
- (defun doom*org-src-pop-to-buffer (buffer _context)
- "Open the src-edit in a way that shackle can detect."
- (if (eq org-src-window-setup 'switch-invisibly)
- (set-buffer buffer)
- (pop-to-buffer buffer)))
- (advice-add #'org-src-switch-to-buffer :override #'doom*org-src-pop-to-buffer)
-
- ;; Ensure todo, agenda, and other minor popups are delegated to shackle.
- (defun doom*org-pop-to-buffer (&rest args)
- "Use `pop-to-buffer' instead of `switch-to-buffer' to open buffer.'"
- (let ((buf (car args)))
- (pop-to-buffer
- (cond ((stringp buf) (get-buffer-create buf))
- ((bufferp buf) buf)
- (t (error "Invalid buffer %s" buf))))))
- (advice-add #'org-switch-to-buffer-other-window :override #'doom*org-pop-to-buffer)
-
- ;; org-agenda
- (setq org-agenda-window-setup 'other-window
- org-agenda-restore-windows-after-quit nil)
- ;; Hide modeline in org-agenda
- (add-hook 'org-agenda-finalize-hook #'doom-hide-modeline-mode)
- (add-hook 'org-agenda-finalize-hook #'org-fit-window-to-buffer)
- ;; Don't monopolize frame!
- (advice-add #'org-agenda :around #'doom*suppress-delete-other-windows)
- ;; ensure quit keybindings work propertly
- (map! :map* org-agenda-mode-map
- :m [escape] 'org-agenda-Quit
- :m "ESC" 'org-agenda-Quit)))
-(add-hook 'doom-init-hook #'doom|init-org-popups)
-
-(provide 'core-popups)
-;;; core-popups.el ends here
diff --git a/core/core-projects.el b/core/core-projects.el
index 24db2ee04..a52bcbc95 100644
--- a/core/core-projects.el
+++ b/core/core-projects.el
@@ -1,133 +1,128 @@
;;; core-projects.el -*- lexical-binding: t; -*-
+(defvar doom-projectile-cache-limit 25000
+ "If any project cache surpasses this many files it is purged when quitting
+Emacs.")
+
+(defvar doom-projectile-cache-blacklist '("~" "/tmp" "/")
+ "Directories that should never be cached.")
+
+(defvar doom-projectile-cache-purge-non-projects nil
+ "If non-nil, non-projects are purged from the cache on `kill-emacs-hook'.")
+
+(defvar doom-projectile-fd-binary "fd"
+ "name of `fd-find' executable binary")
+
+;;
+;;; Packages
+
(def-package! projectile
- :hook (doom-init . projectile-mode)
+ :after-call (after-find-file dired-before-readin-hook minibuffer-setup-hook)
+ :commands (projectile-project-root
+ projectile-project-name
+ projectile-project-p
+ projectile-add-known-project) ; TODO PR autoload upstream
:init
(setq projectile-cache-file (concat doom-cache-dir "projectile.cache")
projectile-enable-caching (not noninteractive)
- projectile-indexing-method 'alien
projectile-known-projects-file (concat doom-cache-dir "projectile.projects")
- projectile-require-project-root nil
+ projectile-require-project-root t
projectile-globally-ignored-files '(".DS_Store" "Icon
" "TAGS")
- projectile-globally-ignored-file-suffixes '(".elc" ".pyc" ".o"))
+ projectile-globally-ignored-file-suffixes '(".elc" ".pyc" ".o")
+ projectile-ignored-projects '("~/" "/tmp")
+ projectile-kill-buffers-filter 'kill-only-files
+ projectile-files-cache-expire 604800) ; expire after a week
:config
(add-hook 'dired-before-readin-hook #'projectile-track-known-projects-find-file-hook)
- (add-hook 'find-file-hook #'doom|autoload-project-mode)
+ (projectile-mode +1)
+
+ (global-set-key [remap evil-jump-to-tag] #'projectile-find-tag)
+ (global-set-key [remap find-tag] #'projectile-find-tag)
;; a more generic project root file
(push ".project" projectile-project-root-files-bottom-up)
+ (push (abbreviate-file-name doom-local-dir) projectile-globally-ignored-directories)
- (setq projectile-globally-ignored-directories
- (append projectile-globally-ignored-directories
- (list (abbreviate-file-name doom-local-dir) ".sync"))
- projectile-other-file-alist
- (append projectile-other-file-alist
- '(("css" . ("scss" "sass" "less" "styl"))
- ("scss" . ("css"))
- ("sass" . ("css"))
- ("less" . ("css"))
- ("styl" . ("css")))))
+ ;; Accidentally indexing big directories like $HOME or / will massively bloat
+ ;; projectile's cache (into the hundreds of MBs). This purges those entries
+ ;; when exiting Emacs to prevent slowdowns/freezing when cache files are
+ ;; loaded or written to.
+ (defun doom|cleanup-project-cache ()
+ "Purge projectile cache entries that:
+
+a) have too many files (see `doom-projectile-cache-limit'),
+b) represent blacklisted directories that are too big, change too often or are
+ private. (see `doom-projectile-cache-blacklist'),
+c) are not valid projectile projects."
+ (when (bound-and-true-p projectile-projects-cache)
+ (cl-loop with blacklist = (mapcar #'file-truename doom-projectile-cache-blacklist)
+ for proot in (hash-table-keys projectile-projects-cache)
+ for len = (length (gethash proot projectile-projects-cache))
+ if (or (>= len doom-projectile-cache-limit)
+ (member (substring proot 0 -1) blacklist)
+ (and doom-projectile-cache-purge-non-projects
+ (not (doom-project-p proot))))
+ do (doom-log "Removed %S from projectile cache" proot)
+ and do (remhash proot projectile-projects-cache)
+ and do (remhash proot projectile-projects-cache-time)
+ and do (remhash proot projectile-project-type-cache))
+ (projectile-serialize-cache)))
+ (add-hook 'kill-emacs-hook #'doom|cleanup-project-cache)
+
+ ;; It breaks projectile's project root resolution if HOME is a project (e.g.
+ ;; it's a git repo). In that case, we disable bottom-up root searching to
+ ;; prevent issues. This makes project resolution a little slower and less
+ ;; accurate in some cases.
+ (let ((default-directory "~"))
+ (when (cl-find-if #'projectile-file-exists-p
+ projectile-project-root-files-bottom-up)
+ (message "HOME appears to be a project. Disabling bottom-up root search.")
+ (setq projectile-project-root-files
+ (append projectile-project-root-files-bottom-up
+ projectile-project-root-files)
+ projectile-project-root-files-bottom-up nil)))
;; Projectile root-searching functions can cause an infinite loop on TRAMP
;; connections, so disable them.
- (defun doom*projectile-locate-dominating-file (orig-fn &rest args)
+ ;; TODO Is this still necessary?
+ (defun doom*projectile-locate-dominating-file (orig-fn file name)
"Don't traverse the file system if on a remote connection."
- (unless (file-remote-p default-directory)
- (apply orig-fn args)))
+ (when (and (stringp file)
+ (not (file-remote-p file)))
+ (funcall orig-fn file name)))
(advice-add #'projectile-locate-dominating-file :around #'doom*projectile-locate-dominating-file)
- (defun doom*projectile-cache-current-file (orig-fun &rest args)
- "Don't cache ignored files."
- (unless (cl-loop for path in (projectile-ignored-directories)
- if (string-prefix-p buffer-file-name (expand-file-name path))
- return t)
- (apply orig-fun args)))
- (advice-add #'projectile-cache-current-file :around #'doom*projectile-cache-current-file))
-
+ ;; If fd exists, use it for git and generic projects
+ ;; fd is a rust program that is significantly faster. It also respects
+ ;; .gitignore. This is recommended in the projectile docs
+ (when (executable-find doom-projectile-fd-binary)
+ (setq projectile-git-command (concat
+ doom-projectile-fd-binary
+ " . --type f -0 -H -E .git")
+ projectile-generic-command projectile-git-command)))
;;
-;; Library
-;;
-
-(defun doom//reload-project ()
- "Reload the project root cache."
- (interactive)
- (projectile-invalidate-cache nil)
- (projectile-reset-cached-project-root)
- (dolist (fn projectile-project-root-files-functions)
- (remhash (format "%s-%s" fn default-directory) projectile-project-root-cache)))
-
-(defun doom-project-p ()
- "Whether or not this buffer is currently in a project or not."
- (let ((projectile-require-project-root t))
- (projectile-project-p)))
-
-(defun doom-project-root ()
- "Get the path to the root of your project.
-If STRICT-P, return nil if no project was found, otherwise return
-`default-directory'."
- (let (projectile-require-project-root)
- (projectile-project-root)))
-
-(defalias 'doom-project-expand #'projectile-expand-root)
-
-(defmacro doom-project-has! (files)
- "Checks if the project has the specified FILES.
-Paths are relative to the project root, unless they start with ./ or ../ (in
-which case they're relative to `default-directory'). If they start with a slash,
-they are absolute."
- (doom--resolve-path-forms files (doom-project-root)))
-
-(defun doom-project-find-file (dir)
- "Fuzzy-find a file under DIR."
- (let ((default-directory dir)
- ;; Necessary to isolate this search from the current project
- projectile-project-name
- projectile-require-project-root
- projectile-cached-buffer-file-name
- projectile-cached-project-root)
- (call-interactively
- ;; completion modules may remap this command
- (or (command-remapping #'projectile-find-file)
- #'projectile-find-file))))
-
-(defun doom-project-browse (dir)
- "Traverse a file structure starting linearly from DIR."
- (let ((default-directory dir))
- (call-interactively
- ;; completion modules may remap this command
- (or (command-remapping #'find-file)
- #'find-file))))
-
-
-;;
-;; Projects
-;;
-
-(defvar-local doom-project nil
- "Either the symbol or a list of project modes you want to enable. Available
-for .dir-locals.el.")
+;; Project-based minor modes
(defvar doom-project-hook nil
"Hook run when a project is enabled. The name of the project's mode and its
state are passed in.")
-(defun doom|autoload-project-mode ()
- "Auto-enable the project(s) listed in `doom-project'."
- (when doom-project
- (if (symbolp doom-project)
- (funcall doom-project)
- (cl-loop for mode in doom-project
- unless (symbol-value mode)
- do (funcall mode)))))
-
-(defmacro def-project-mode! (name &rest plist)
+(cl-defmacro def-project-mode! (name &key
+ modes
+ files
+ when
+ match
+ add-hooks
+ on-load
+ on-enter
+ on-exit)
"Define a project minor-mode named NAME (a symbol) and declare where and how
it is activated. Project modes allow you to configure 'sub-modes' for
-major-modes that are specific to a specific folder, certain project structure,
-framework or arbitrary context you define. These project modes can have their
-own settings, keymaps, hooks, snippets, etc.
+major-modes that are specific to a folder, project structure, framework or
+whatever arbitrary context you define. These project modes can have their own
+settings, keymaps, hooks, snippets, etc.
This creates NAME-hook and NAME-map as well.
@@ -157,37 +152,26 @@ should be activated. If they are *all* true, NAME is activated.
:on-exit FORM -- FORM is run each time the mode is disabled.
Relevant: `doom-project-hook'."
- (declare (indent 1) (doc-string 2))
- (let ((doc-string (if (stringp (car plist))
- (prog1 (car plist)
- (setq plist (cdr plist)))
- "A project minor mode."))
- (modes (plist-get plist :modes))
- (files (plist-get plist :files))
- (when (plist-get plist :when))
- (match (plist-get plist :match))
- (hooks (plist-get plist :add-hooks))
- (load-form (plist-get plist :on-load))
- (enter-form (plist-get plist :on-enter))
- (exit-form (plist-get plist :on-exit))
- (init-var (intern (format "%s-init" name))))
+ (declare (indent 1))
+ (let ((init-var (intern (format "%s-init" name))))
`(progn
- ,(if load-form `(defvar ,init-var nil))
+ ,(if on-load `(defvar ,init-var nil))
(define-minor-mode ,name
- ,doc-string
+ "A project minor mode generated by `def-project-mode!'."
:init-value nil
:lighter ""
:keymap (make-sparse-keymap)
(if (not ,name)
- ,exit-form
+ ,on-exit
(run-hook-with-args 'doom-project-hook ',name ,name)
- ,(when load-form
+ ,(when on-load
`(unless ,init-var
- ,load-form
+ ,on-load
(setq ,init-var t)))
- ,enter-form))
- ,(when hooks
- `(setq ,(intern (format "%s-hook" name)) ',hooks))
+ ,on-enter))
+ ,@(cl-loop for hook in add-hooks
+ collect `(add-hook ',(intern (format "%s-hook" name))
+ #',hook))
,(when (or modes match files when)
`(associate! ,name
:modes ,modes
diff --git a/core/core-ui.el b/core/core-ui.el
index ecf053cae..d653e1140 100644
--- a/core/core-ui.el
+++ b/core/core-ui.el
@@ -1,87 +1,208 @@
;;; core-ui.el -*- lexical-binding: t; -*-
-(defvar doom-fringe-size '4
- "Default fringe width.")
+;;
+;;; Variables
(defvar doom-theme nil
- "A symbol representing the color theme to load.")
+ "A symbol representing the Emacs theme to load at startup.
+
+This is changed by `load-theme'.")
(defvar doom-font nil
- "The default font to use. Expects a `font-spec'.")
+ "The default font to use.
+
+Expects either a `font-spec', font object, an XFT font string or an XLFD font
+string.
+
+This affects the `default' and `fixed-pitch' faces.
+
+Examples:
+ (setq doom-font (font-spec :family \"Fira Mono\" :size 12))
+ (setq doom-font \"Terminus (TTF):pixelsize=12:antialias=off\")")
(defvar doom-big-font nil
- "The default large font to use when `doom-big-font-mode' is enabled. Expects a
-`font-spec'.")
+ "The font to use when `doom-big-font-mode' is enabled. Expects either a
+`font-spec' or a XFT font string. See `doom-font' for examples.")
(defvar doom-variable-pitch-font nil
- "The default font to use for variable-pitch text. Expects a `font-spec'.")
+ "The font to use for variable-pitch text.
+
+Expects either a `font-spec', font object, a XFT font string or XLFD string. See
+`doom-font' for examples.
+
+It is recommended you don't set specify a font-size, as to inherit `doom-font's
+size.")
+
+(defvar doom-serif-font nil
+ "The default font to use for the `fixed-pitch-serif' face.
+
+Expects either a `font-spec', font object, a XFT font string or XLFD string. See
+`doom-font' for examples.
+
+It is recommended you don't set specify a font-size, as to inherit `doom-font's
+size.")
(defvar doom-unicode-font nil
"Fallback font for unicode glyphs. Is ignored if :feature unicode is active.
-Expects a `font-spec'.")
-(defvar doom-major-mode-names
- '((sh-mode . "sh")
- (emacs-lisp-mode . "Elisp"))
- "An alist mapping major modes symbols to strings (or functions that will
-return a string). This changes the 'long' name of a major-mode, allowing for
-shorter major mode name in the mode-line. See `doom|set-mode-name'.")
+Expects either a `font-spec', font object, a XFT font string or XLFD string. See
+`doom-font' for examples.
+
+It is recommended you don't set specify a font-size, as to inherit `doom-font's
+size.")
+
+(defvar doom--prefer-theme-elc nil
+ "If non-nil, `load-theme' will prefer the compiled theme (unlike its default
+behavior). Do not set this directly, this is let-bound in `doom|init-theme'.")
-;; Hook(s)
+;;
+;;; Custom hooks
+
(defvar doom-init-ui-hook nil
- "List of hooks to run when the theme and font is initialized (or reloaded with
-`doom//reload-theme').")
+ "List of hooks to run when the UI has been initialized.")
+(defvar doom-load-theme-hook nil
+ "Hook run after the theme is loaded with `load-theme' or reloaded with
+`doom/reload-theme'.")
+
+(defvar doom-switch-buffer-hook nil
+ "A list of hooks run after changing the current buffer.")
+
+(defvar doom-switch-window-hook nil
+ "A list of hooks run after changing the focused windows.")
+
+(defvar doom-switch-frame-hook nil
+ "A list of hooks run after changing the focused frame.")
+
+(defvar doom-inhibit-switch-buffer-hooks nil
+ "Letvar for inhibiting `doom-switch-buffer-hook'. Do not set this directly.")
+(defvar doom-inhibit-switch-window-hooks nil
+ "Letvar for inhibiting `doom-switch-window-hook'. Do not set this directly.")
+(defvar doom-inhibit-switch-frame-hooks nil
+ "Letvar for inhibiting `doom-switch-frame-hook'. Do not set this directly.")
+
+(defvar doom--last-window nil)
+(defvar doom--last-frame nil)
+
+(defun doom|run-switch-window-hooks ()
+ (unless (or doom-inhibit-switch-window-hooks
+ (eq doom--last-window (selected-window))
+ (minibufferp))
+ (let ((doom-inhibit-switch-window-hooks t))
+ (run-hooks 'doom-switch-window-hook)
+ (doom-log "Window switched to %s" (selected-window))
+ (setq doom--last-window (selected-window)))))
+
+(defun doom|run-switch-frame-hooks (&rest _)
+ (unless (or doom-inhibit-switch-frame-hooks
+ (eq doom--last-frame (selected-frame))
+ (frame-parameter nil 'parent-frame))
+ (let ((doom-inhibit-switch-frame-hooks t))
+ (run-hooks 'doom-switch-frame-hook)
+ (doom-log "Frame switched to %s" (selected-frame))
+ (setq doom--last-frame (selected-frame)))))
+
+(defun doom*run-switch-buffer-hooks (orig-fn buffer-or-name &rest args)
+ (if (or doom-inhibit-switch-buffer-hooks
+ (if (eq orig-fn 'switch-to-buffer)
+ (car args) ; norecord
+ (eq (get-buffer buffer-or-name) (current-buffer))))
+ (apply orig-fn buffer-or-name args)
+ (let ((doom-inhibit-switch-buffer-hooks t))
+ (doom-log "Buffer switched in %s" (selected-window))
+ (prog1 (apply orig-fn buffer-or-name args)
+ (run-hooks 'doom-switch-buffer-hook)))))
+
+(defun doom*run-load-theme-hooks (theme &optional _no-confirm no-enable)
+ "Set up `doom-load-theme-hook' to run after `load-theme' is called."
+ (unless no-enable
+ (setq doom-theme theme)
+ (run-hooks 'doom-load-theme-hook)))
+
+(defun doom|protect-fallback-buffer ()
+ "Don't kill the scratch buffer. Meant for `kill-buffer-query-functions'."
+ (not (eq (current-buffer) (doom-fallback-buffer))))
+
+(defun doom|highlight-non-default-indentation ()
+ "Highlight whitespace that doesn't match your `indent-tabs-mode' setting.
+
+e.g. If you indent with spaces by default, tabs will be highlighted. If you
+indent with tabs, spaces at BOL are highlighted.
+
+Does nothing if `whitespace-mode' is already active or the current buffer is
+read-only or not file-visiting."
+ (unless (or (bound-and-true-p global-whitespace-mode)
+ (bound-and-true-p whitespace-mode)
+ (eq major-mode 'fundamental-mode)
+ buffer-read-only
+ (null buffer-file-name))
+ (require 'whitespace)
+ (set (make-local-variable 'whitespace-style)
+ (if (bound-and-true-p whitespace-newline-mode)
+ (cl-union (if indent-tabs-mode '(indentation) '(tabs tab-mark))
+ whitespace-style)
+ `(face ,@(if indent-tabs-mode '(indentation) '(tabs tab-mark))
+ trailing-lines tail)))
+ (whitespace-mode +1)))
+
+
+;;
+;;; General configuration
(setq-default
+ ansi-color-for-comint-mode t
bidi-display-reordering nil ; disable bidirectional text for tiny performance boost
blink-matching-paren nil ; don't blink--too distracting
- cursor-in-non-selected-windows nil ; hide cursors in other windows
+ compilation-always-kill t ; kill compilation process before starting another
+ compilation-ask-about-save nil ; save all buffers on `compile'
+ compilation-scroll-output 'first-error
+ confirm-nonexistent-file-or-buffer t
+ confirm-kill-emacs #'doom-quit-p ; custom confirmation when killing Emacs
+ cursor-in-non-selected-windows nil ; hide cursors in other windows
+ custom-theme-directory (expand-file-name "themes/" doom-private-dir)
display-line-numbers-width 3
+ enable-recursive-minibuffers nil
frame-inhibit-implied-resize t
+ frame-title-format '("%b – Doom Emacs") ; simple name in frame title
;; remove continuation arrow on right fringe
- fringe-indicator-alist (delq (assq 'continuation fringe-indicator-alist)
- fringe-indicator-alist)
+ fringe-indicator-alist
+ (delq (assq 'continuation fringe-indicator-alist)
+ fringe-indicator-alist)
highlight-nonselected-windows nil
image-animate-loop t
indicate-buffer-boundaries nil
indicate-empty-lines nil
+ inhibit-compacting-font-caches t
max-mini-window-height 0.3
mode-line-default-help-echo nil ; disable mode-line mouseovers
mouse-yank-at-point t ; middle-click paste at point, not at click
- ibuffer-use-other-window t
resize-mini-windows 'grow-only ; Minibuffer resizing
show-help-function nil ; hide :help-echo text
split-width-threshold 160 ; favor horizontal splits
- uniquify-buffer-name-style 'forward
+ uniquify-buffer-name-style nil ; custom modeline will show file paths anyway
use-dialog-box nil ; always avoid GUI
visible-cursor nil
x-stretch-cursor nil
- ;; defer jit font locking slightly to [try to] improve Emacs performance
- jit-lock-defer-time nil
- jit-lock-stealth-nice 0.1
- jit-lock-stealth-time 0.2
- jit-lock-stealth-verbose nil
;; `pos-tip' defaults
pos-tip-internal-border-width 6
pos-tip-border-width 1
;; no beeping or blinking please
ring-bell-function #'ignore
- visible-bell nil)
-
-(fset #'yes-or-no-p #'y-or-n-p) ; y/n instead of yes/no
-
-(defun doom-quit-p (&optional prompt)
- "Return t if this session should be killed. Prompts the user for
-confirmation."
- (if (ignore-errors (doom-real-buffer-list))
- (or (yes-or-no-p (format "››› %s" (or prompt "Quit Emacs?")))
- (ignore (message "Aborted")))
- t))
-(setq confirm-kill-emacs nil)
-(add-hook 'kill-emacs-query-functions #'doom-quit-p)
-
+ visible-bell nil
+ ;; don't resize emacs in steps, it looks weird
+ window-resize-pixelwise t
+ frame-resize-pixelwise t)
+;; y/n instead of yes/no
+(fset #'yes-or-no-p #'y-or-n-p)
+;; Truly silence startup message
+(fset #'display-startup-echo-area-message #'ignore)
+;; relegate tooltips to echo area only
+(if (bound-and-true-p tooltip-mode) (tooltip-mode -1))
+;; enabled by default; no thanks, too distracting
+(blink-cursor-mode -1)
+;; Handle ansi codes in compilation buffer
+(add-hook 'compilation-filter-hook #'doom|apply-ansi-color-to-compilation-buffer)
;; show typed keystrokes in minibuffer
(defun doom|enable-ui-keystrokes () (setq echo-keystrokes 0.02))
(defun doom|disable-ui-keystrokes () (setq echo-keystrokes 0))
@@ -89,192 +210,47 @@ confirmation."
;; ...but hide them while isearch is active
(add-hook 'isearch-mode-hook #'doom|disable-ui-keystrokes)
(add-hook 'isearch-mode-end-hook #'doom|enable-ui-keystrokes)
-
-;; A minor mode for toggling the mode-line
-(defvar-local doom--modeline-format nil
- "The modeline format to use when `doom-hide-modeline-mode' is active. Don't
-set this directly. Let-bind it instead.")
-(defvar-local doom--old-modeline-format nil
- "The old modeline format, so `doom-hide-modeline-mode' can revert when it's
-disabled.")
-(define-minor-mode doom-hide-modeline-mode
- "Minor mode to hide the mode-line in the current buffer."
- :init-value nil
- :global nil
- (if doom-hide-modeline-mode
- (setq doom--old-modeline-format mode-line-format
- mode-line-format doom--modeline-format)
- (setq mode-line-format doom--old-modeline-format
- doom--old-modeline-format nil))
- (force-mode-line-update))
-;; Ensure major-mode or theme changes don't overwrite these variables
-(put 'doom--modeline-format 'permanent-local t)
-(put 'doom--old-modeline-format 'permanent-local t)
-(put 'doom-hide-modeline-mode 'permanent-local t)
-
-(defun doom|hide-modeline-mode-reset ()
- "Sometimes, a major-mode is activated after `doom-hide-modeline-mode' is
-activated, thus disabling it (because changing major modes invokes
-`kill-all-local-variables' and specifically seems to kill `mode-line-format's
-local value, whether or not it's permanent-local. Therefore, we cycle
-`doom-hide-modeline-mode' to fix this."
- (when doom-hide-modeline-mode
- (doom-hide-modeline-mode -1)
- (doom-hide-modeline-mode +1)))
-(add-hook 'after-change-major-mode-hook #'doom|hide-modeline-mode-reset)
-
-;; no modeline in completion popups
-(add-hook 'completion-list-mode-hook #'doom-hide-modeline-mode)
-
-;; undo/redo changes to Emacs' window layout
-(defvar winner-dont-bind-my-keys t) ; I'll bind keys myself
-(autoload 'winner-mode "winner" nil t)
-(add-hook 'doom-init-ui-hook #'winner-mode)
-
-;; highlight matching delimiters
-(setq show-paren-delay 0.1
- show-paren-highlight-openparen t
- show-paren-when-point-inside-paren t)
-(add-hook 'doom-init-ui-hook #'show-paren-mode)
-
-;;; More reliable inter-window border
-;; The native border "consumes" a pixel of the fringe on righter-most splits,
-;; `window-divider' does not. Available since Emacs 25.1.
-(setq-default window-divider-default-places t
- window-divider-default-bottom-width 0
- window-divider-default-right-width 1)
-(add-hook 'doom-init-ui-hook #'window-divider-mode)
-
-;; like diminish, but for major-modes. [pedantry intensifies]
-(defun doom|set-mode-name ()
- "Set the major mode's `mode-name', as dictated by `doom-major-mode-names'."
- (when-let* ((name (cdr (assq major-mode doom-major-mode-names))))
- (setq mode-name
- (cond ((functionp name) (funcall name))
- ((stringp name) name)
- (t (error "'%s' isn't a valid name for %s" name major-mode))))))
-(add-hook 'after-change-major-mode-hook #'doom|set-mode-name)
-
-
-;;
-;; Themes & fonts
-;;
-
-;; Getting themes to remain consistent across GUI Emacs, terminal Emacs and
-;; daemon Emacs is hairy.
-;;
-;; + Running `doom|init-ui' directly sorts out the initial GUI frame.
-;; + Attaching it to `after-make-frame-functions' sorts out daemon Emacs.
-;; + Waiting for 0.1s in `doom|reload-ui-in-daemon' fixes daemon Emacs started
-;; with `server-start' in an interactive session of Emacs AND in tty Emacs.
-(defun doom|init-ui (&optional frame)
- "Set the theme and load the font, in that order."
- (when doom-theme
- (load-theme doom-theme t))
- (condition-case-unless-debug ex
- (when (display-graphic-p)
- (when (fontp doom-font)
- (set-frame-font doom-font nil (if frame (list frame) t))
- (set-face-attribute 'fixed-pitch frame :font doom-font))
- ;; Fallback to `doom-unicode-font' for Unicode characters
- (when (fontp doom-unicode-font)
- (set-fontset-font t 'unicode doom-unicode-font frame))
- ;; ...and for variable-pitch-mode:
- (when (fontp doom-variable-pitch-font)
- (set-face-attribute 'variable-pitch frame :font doom-variable-pitch-font)))
- ('error
- (if (string-prefix-p "Font not available: " (error-message-string ex))
- (lwarn 'doom-ui :warning
- "Could not find the '%s' font on your system, falling back to system font"
- (font-get (caddr ex) :family))
- (lwarn 'doom-ui :error
- "Unexpected error while initializing fonts: %s"
- (error-message-string ex)))))
- (run-hooks 'doom-init-ui-hook))
-
-(defun doom|reload-ui-in-daemon (frame)
- "Reload the theme (and font) in an daemon frame."
- (when (or (daemonp) (not (display-graphic-p)))
- (with-selected-frame frame
- (run-with-timer 0.1 nil #'doom|init-ui))))
-
-;; register UI init hooks
-(add-hook 'doom-post-init-hook #'doom|init-ui)
-(add-hook! 'after-make-frame-functions #'(doom|init-ui doom|reload-ui-in-daemon))
-
-
-;;
-;; Bootstrap
-;;
-
+;; Make `next-buffer', `other-buffer', etc. ignore unreal buffers.
+(add-to-list 'default-frame-alist '(buffer-predicate . doom-buffer-frame-predicate))
+;; Prevent the glimpse of un-styled Emacs by setting these early.
+(add-to-list 'default-frame-alist '(tool-bar-lines . 0))
+(add-to-list 'default-frame-alist '(menu-bar-lines . 0))
+(add-to-list 'default-frame-alist '(vertical-scroll-bars))
;; prompts the user for confirmation when deleting a non-empty frame
-(define-key global-map [remap delete-frame] #'doom/delete-frame)
-;; simple name in frame title
-(setq-default frame-title-format '("DOOM Emacs"))
-;; auto-enabled in Emacs 25+; I'll do it myself
-(global-eldoc-mode -1)
-;; a good indicator that Emacs isn't frozen
-(add-hook 'doom-post-init-hook #'blink-cursor-mode)
-;; standardize default fringe width
-(if (fboundp 'fringe-mode) (fringe-mode doom-fringe-size))
-;; draw me like one of your French editors
-(tooltip-mode -1) ; relegate tooltips to echo area only
-(menu-bar-mode -1)
-(if (fboundp 'tool-bar-mode) (tool-bar-mode -1))
-(if (fboundp 'scroll-bar-mode) (scroll-bar-mode -1))
-
-(defun doom|no-fringes-in-minibuffer ()
- "Disable fringes in the minibuffer window."
- (set-window-fringes (minibuffer-window) 0 0 nil))
-(add-hook! '(doom-post-init-hook minibuffer-setup-hook)
- #'doom|no-fringes-in-minibuffer)
-
-(defun doom|protect-visible-buffers ()
- "Don't kill the current buffer if it is visible in another window (bury it
-instead)."
- (not (delq (selected-window)
- (get-buffer-window-list nil nil t))))
-(add-hook! doom-post-init
- (add-hook 'kill-buffer-query-functions #'doom|protect-visible-buffers))
+(global-set-key [remap delete-frame] #'doom/delete-frame)
;;
-;; Plugins
-;;
+;;; Built-in packages
-(def-package! all-the-icons
- :commands (all-the-icons-octicon all-the-icons-faicon all-the-icons-fileicon
- all-the-icons-wicon all-the-icons-material all-the-icons-alltheicon
- all-the-icons-install-fonts)
+;; Disable these because whitespace should be customized programmatically
+;; (through `whitespace-style'), and not through these commands.
+(put 'whitespace-toggle-options 'disabled t)
+(put 'global-whitespace-toggle-options 'disabled t)
+
+
+(def-package! ediff
+ :defer t
:init
- (defun doom*disable-all-the-icons-in-tty (orig-fn &rest args)
- (when (display-graphic-p)
- (apply orig-fn args)))
- ;; all-the-icons doesn't work in the terminal, so we "disable" it.
- (dolist (fn '(all-the-icons-octicon all-the-icons-material
- all-the-icons-faicon all-the-icons-fileicon
- all-the-icons-wicon all-the-icons-alltheicon))
- (advice-add fn :around #'doom*disable-all-the-icons-in-tty)))
+ (setq ediff-diff-options "-w" ; turn off whitespace checking
+ ediff-split-window-function #'split-window-horizontally
+ ediff-window-setup-function #'ediff-setup-windows-plain)
+ :config
+ (defvar doom--ediff-saved-wconf nil)
+ ;; Restore window config after quitting ediff
+ (defun doom|ediff-save-wconf ()
+ (setq doom--ediff-saved-wconf (current-window-configuration)))
+ (add-hook 'ediff-before-setup-hook #'doom|ediff-save-wconf)
-(def-package! fringe-helper
- :commands (fringe-helper-define fringe-helper-convert)
- :init
- (unless (fboundp 'define-fringe-bitmap)
- ;; doesn't exist in terminal Emacs; define it to prevent errors
- (defun define-fringe-bitmap (&rest _))))
+ (defun doom|ediff-restore-wconf ()
+ (when (window-configuration-p doom--ediff-saved-wconf)
+ (set-window-configuration doom--ediff-saved-wconf)))
+ (add-hook 'ediff-quit-hook #'doom|ediff-restore-wconf 'append)
+ (add-hook 'ediff-suspend-hook #'doom|ediff-restore-wconf 'append))
-(def-package! hideshow ; built-in
- :commands (hs-minor-mode hs-toggle-hiding hs-already-hidden-p)
- :config (setq hs-hide-comments-when-hiding-all nil))
-(def-package! highlight-indentation
- :commands (highlight-indentation-mode highlight-indentation-current-column-mode))
-
-;; For modes with sub-par number fontification
-(def-package! highlight-numbers :commands highlight-numbers-mode)
-
-;; Highlights the current line
-(def-package! hl-line ; built-in
+(def-package! hl-line
+ ;; Highlights the current line
:hook ((prog-mode text-mode conf-mode) . hl-line-mode)
:config
;; I don't need hl-line showing in other windows. This also offers a small
@@ -282,98 +258,124 @@ instead)."
(setq hl-line-sticky-flag nil
global-hl-line-sticky-flag nil)
- ;; On Emacs 26+, when point is on the last line, hl-line highlights bleed into
- ;; the rest of the window after eob. This is the fix.
- (when (boundp 'display-line-numbers)
- (defun doom--line-range ()
- (cons (line-beginning-position)
- (cond ((save-excursion
- (goto-char (line-end-position))
- (and (eobp) (not (bolp))))
- (1- (line-end-position)))
- ((or (eobp) (save-excursion (forward-line) (eobp)))
- (line-end-position))
- (t
- (line-beginning-position 2)))))
- (setq hl-line-range-function #'doom--line-range))
-
+ ;; Disable `hl-line' in evil-visual mode (temporarily). `hl-line' can make the
+ ;; selection region harder to see while in evil visual mode.
(after! evil
(defvar-local doom-buffer-hl-line-mode nil)
-
- ;; Disable `hl-line' in evil-visual mode (temporarily). `hl-line' can make
- ;; the selection region harder to see while in evil visual mode.
(defun doom|disable-hl-line ()
(when hl-line-mode
(setq doom-buffer-hl-line-mode t)
(hl-line-mode -1)))
(defun doom|enable-hl-line-maybe ()
(if doom-buffer-hl-line-mode (hl-line-mode +1)))
-
(add-hook 'evil-visual-state-entry-hook #'doom|disable-hl-line)
(add-hook 'evil-visual-state-exit-hook #'doom|enable-hl-line-maybe)))
-;; Helps us distinguish stacked delimiter pairs. Especially in parentheses-drunk
-;; languages like Lisp.
-(def-package! rainbow-delimiters
- :hook (lisp-mode . rainbow-delimiters-mode)
- :config (setq rainbow-delimiters-max-face-count 3))
-;; For a distractions-free-like UI, that dynamically resizes margets and can
-;; center a buffer.
-(def-package! visual-fill-column
- :commands visual-fill-column-mode
+(def-package! winner
+ ;; undo/redo changes to Emacs' window layout
+ :after-call (after-find-file doom-switch-window-hook)
+ :preface (defvar winner-dont-bind-my-keys t)
+ :config (winner-mode +1)) ; I'll bind keys myself
+
+
+(def-package! paren
+ ;; highlight matching delimiters
+ :after-call (after-find-file doom-switch-buffer-hook)
+ :init
+ (defun doom|disable-show-paren-mode ()
+ "Turn off `show-paren-mode' buffer-locally."
+ (set (make-local-variable 'show-paren-mode) nil))
:config
- (setq-default
- visual-fill-column-center-text t
- visual-fill-column-width
- ;; take Emacs 26 line numbers into account
- (+ (if (boundp 'display-line-numbers) 6 0)
- fill-column)))
+ (setq show-paren-delay 0.1
+ show-paren-highlight-openparen t
+ show-paren-when-point-inside-paren t)
+ (show-paren-mode +1))
+
+
+;; The native border "consumes" a pixel of the fringe on righter-most splits,
+;; `window-divider' does not. Available since Emacs 25.1.
+(setq-default window-divider-default-places t
+ window-divider-default-bottom-width 1
+ window-divider-default-right-width 1)
+(add-hook 'doom-init-ui-hook #'window-divider-mode)
+
+
+;; `whitespace-mode'
+(setq whitespace-line-column nil
+ whitespace-style
+ '(face indentation tabs tab-mark spaces space-mark newline newline-mark
+ trailing lines-tail)
+ whitespace-display-mappings
+ '((tab-mark ?\t [?› ?\t])
+ (newline-mark ?\n [?¬ ?\n])
+ (space-mark ?\ [?·] [?.])))
;;
-;; Line numbers
+;;; Third party packages
+
+;;;###package avy
+(setq avy-all-windows nil
+ avy-background t)
+
+(def-package! all-the-icons
+ :commands (all-the-icons-octicon all-the-icons-faicon all-the-icons-fileicon
+ all-the-icons-wicon all-the-icons-material all-the-icons-alltheicon)
+ :init
+ (defun doom*disable-all-the-icons-in-tty (orig-fn &rest args)
+ (if (display-graphic-p)
+ (apply orig-fn args)
+ ""))
+ :config
+ ;; all-the-icons doesn't work in the terminal, so we "disable" it.
+ (dolist (fn '(all-the-icons-octicon all-the-icons-material
+ all-the-icons-faicon all-the-icons-fileicon
+ all-the-icons-wicon all-the-icons-alltheicon))
+ (advice-add fn :around #'doom*disable-all-the-icons-in-tty)))
+
+;;;###package hide-mode-line-mode
+(add-hook 'completion-list-mode-hook #'hide-mode-line-mode)
+(add-hook 'Man-mode-hook #'hide-mode-line-mode)
+
+;; Better fontification of number literals in code
+(def-package! highlight-numbers
+ :hook ((prog-mode conf-mode) . highlight-numbers-mode)
+ :config (setq highlight-numbers-generic-regexp "\\_<[[:digit:]]+\\(?:\\.[0-9]*\\)?\\_>"))
+
+;;;###package highlight-escape-sequences
+(def-package! highlight-escape-sequences
+ :hook ((prog-mode conf-mode) . highlight-escape-sequences-mode))
+
+;;;###package rainbow-delimiters
+;; Helps us distinguish stacked delimiter pairs, especially in parentheses-drunk
+;; languages like Lisp.
+(setq rainbow-delimiters-max-face-count 3)
+
+;;;###package visual-fill-column
+;; For a distractions-free-like UI, that dynamically resizes margins and can
+;; center a buffer.
+(setq visual-fill-column-center-text t
+ visual-fill-column-width
+ ;; take Emacs 26 line numbers into account
+ (+ (if EMACS26+ 6 0) fill-column))
+
+
;;
+;;; Line numbers
-(defvar doom-line-numbers-style t
- "The style to use for the line number display.
+;; line numbers in most modes
+(add-hook! (prog-mode text-mode conf-mode) #'display-line-numbers-mode)
-Accepts the same arguments as `display-line-numbers', which are:
+(defun doom|enable-line-numbers () (display-line-numbers-mode +1))
+(defun doom|disable-line-numbers () (display-line-numbers-mode -1))
-nil No line numbers
-t Ordinary line numbers
-'relative Relative line numbers")
-
-(defun doom|enable-line-numbers (&optional arg)
- "Enables the display of line numbers, using `display-line-numbers' (in Emacs
-26+) or `nlinum-mode'.
-
-See `doom-line-numbers-style' to control the style of line numbers to display."
- (cond ((boundp 'display-line-numbers)
- (setq display-line-numbers
- (pcase arg
- (+1 doom-line-numbers-style)
- (-1 nil)
- (_ doom-line-numbers-style))))
- ((eq doom-line-numbers-style 'relative)
- (if (= arg -1)
- (nlinum-relative-off)
- (nlinum-relative-on)))
- ((not (null doom-line-numbers-style))
- (nlinum-mode (or arg +1)))))
-
-(defun doom|disable-line-numbers ()
- "Disable the display of line numbers."
- (doom|enable-line-numbers -1))
-
-(add-hook! (prog-mode text-mode conf-mode) #'doom|enable-line-numbers)
-
-;; Emacs 26+ has native line number support.
-;; Line number column. A faster (or equivalent, in the worst case) line number
-;; plugin than `linum-mode'.
+;; `nlinum' is used for Emacs 25 users, as Emacs 26+ has native line numbers.
(def-package! nlinum
- :unless (boundp 'display-line-numbers)
- :commands nlinum-mode
+ ;; Line number column. A faster (or equivalent, in the worst case) line number
+ ;; plugin than `linum-mode'.
+ :unless EMACS26+
+ :defer t
:init
(defvar doom-line-number-lpad 4
"How much padding to place before line numbers.")
@@ -418,9 +420,9 @@ character that looks like a space that `whitespace-mode' won't affect.")
(format-mode-line "%l")))))
(add-hook 'nlinum-mode-hook #'doom|init-nlinum-width))
-;; Fixes disappearing line numbers in nlinum and other quirks
(def-package! nlinum-hl
- :unless (boundp 'display-line-numbers)
+ ;; Fixes disappearing line numbers in nlinum and other quirks
+ :unless EMACS26+
:after nlinum
:config
;; With `markdown-fontify-code-blocks-natively' enabled in `markdown-mode',
@@ -432,79 +434,171 @@ character that looks like a space that `whitespace-mode' won't affect.")
(advice-add #'web-mode-fold-or-unfold :after #'nlinum-hl-do-generic-flush)
;; Changing fonts can leave nlinum line numbers in their original size; this
;; forces them to resize.
- (advice-add #'set-frame-font :after #'nlinum-hl-flush-all-windows))
+ (add-hook 'after-setting-font-hook #'nlinum-hl-flush-all-windows))
(def-package! nlinum-relative
- :unless (boundp 'display-line-numbers)
- :commands nlinum-relative-mode
+ :unless EMACS26+
+ :defer t
:config
- (after! evil (nlinum-relative-setup-evil)))
+ (setq nlinum-format " %d ")
+ (add-hook 'evil-mode-hook #'nlinum-relative-setup-evil))
;;
-;; Modeline
+;;; Theme & font
+
+(defvar doom-last-window-system
+ (if (daemonp) 'daemon initial-window-system)
+ "The `window-system' of the last frame. If this doesn't match the current
+frame's window-system, the theme will be reloaded.")
+
+(defun doom|init-fonts ()
+ "Loads fonts.
+
+Fonts are specified by `doom-font', `doom-variable-pitch-font',
+`doom-serif-font' and `doom-unicode-font'."
+ (condition-case e
+ (progn
+ (cond (doom-font
+ (add-to-list
+ 'default-frame-alist
+ (cons 'font
+ (cond ((stringp doom-font) doom-font)
+ ((fontp doom-font) (font-xlfd-name doom-font))
+ ((signal 'wrong-type-argument (list '(fontp stringp) doom-font)))))))
+ ((display-graphic-p)
+ (setq doom-font (face-attribute 'default :font))))
+ (when doom-serif-font
+ (set-face-attribute 'fixed-pitch-serif nil :font doom-serif-font))
+ (when doom-variable-pitch-font
+ (set-face-attribute 'variable-pitch nil :font doom-variable-pitch-font))
+ ;; Fallback to `doom-unicode-font' for Unicode characters
+ (when (fontp doom-unicode-font)
+ (set-fontset-font t nil doom-unicode-font nil 'append)))
+ ((debug error)
+ (if (string-prefix-p "Font not available: " (error-message-string e))
+ (lwarn 'doom-ui :warning
+ "Could not find the '%s' font on your system, falling back to system font"
+ (font-get (caddr e) :family))
+ (signal 'doom-error e)))))
+
+(defun doom|init-theme ()
+ "Load the theme specified by `doom-theme'."
+ (when (and doom-theme (not (memq doom-theme custom-enabled-themes)))
+ (let ((doom--prefer-theme-elc t))
+ (load-theme doom-theme t))))
+
+(defun doom|reload-theme-maybe (_frame)
+ "Reloads the theme if the display device has changed."
+ (unless (cl-find doom-last-window-system (frame-list) :key #'framep-on-display)
+ (setq doom-last-window-system nil)
+ (doom|reload-theme-in-frame-maybe (selected-frame))))
+
+(defun doom|reload-theme-in-frame-maybe (frame)
+ "Reloads the theme if the display device has changed.
+
+Getting themes to remain consistent across GUI Emacs, terminal Emacs and daemon
+Emacs is hairy. `doom|init-theme' sorts out the initial GUI frame. Attaching
+`doom|reload-theme-in-frame-maybe' to `after-make-frame-functions' sorts out
+daemon and emacsclient frames.
+
+There will still be issues with simultaneous gui and terminal (emacsclient)
+frames, however. There's always `doom/reload-theme' if you need it!"
+ (when (and doom-theme
+ (framep frame)
+ (not (eq doom-last-window-system (framep-on-display frame))))
+ (with-selected-frame frame
+ (load-theme doom-theme t))
+ (setq doom-last-window-system (framep-on-display frame))))
+
+
;;
+;;; Bootstrap
-(defmacro def-modeline-segment! (name &rest forms)
- "Defines a modeline segment and byte compiles it."
- (declare (indent defun) (doc-string 2))
- (let ((sym (intern (format "doom-modeline-segment--%s" name))))
+(defun doom|init-ui ()
+ "Initialize Doom's user interface by applying all its advice and hooks."
+ (run-hook-wrapped 'doom-init-ui-hook #'doom-try-run-hook)
+
+ (add-to-list 'kill-buffer-query-functions #'doom|protect-fallback-buffer nil 'eq)
+ (add-hook 'after-change-major-mode-hook #'doom|highlight-non-default-indentation)
+
+ ;; Reload theme if the display device has changed
+ (add-hook 'after-make-frame-functions #'doom|reload-theme-in-frame-maybe)
+ (add-hook 'after-delete-frame-functions #'doom|reload-theme-maybe)
+
+ ;; Initialize custom switch-{buffer,window,frame} hooks:
+ ;; + `doom-switch-buffer-hook'
+ ;; + `doom-switch-window-hook'
+ ;; + `doom-switch-frame-hook'
+ (add-hook 'buffer-list-update-hook #'doom|run-switch-window-hooks)
+ (add-hook 'focus-in-hook #'doom|run-switch-frame-hooks)
+ (advice-add! '(switch-to-buffer display-buffer) :around #'doom*run-switch-buffer-hooks))
+
+;; Apply `doom-theme'
+(unless (daemonp)
+ (add-hook 'doom-init-ui-hook #'doom|init-theme))
+;; Apply `doom-font' et co
+(add-hook 'doom-after-init-modules-hook #'doom|init-fonts)
+;; Setup `doom-load-theme-hook'
+(advice-add #'load-theme :after #'doom*run-load-theme-hooks)
+
+(add-hook 'window-setup-hook #'doom|init-ui)
+
+
+;;
+;;; Fixes/hacks
+
+;; doesn't exist in terminal Emacs; we define it to prevent errors
+(unless (fboundp 'define-fringe-bitmap)
+ (defun define-fringe-bitmap (&rest _)))
+
+(defun doom*prefer-compiled-theme (orig-fn &rest args)
+ "Make `load-theme' prioritize the byte-compiled theme for a moderate boost in
+startup (or theme switch) time, so long as `doom--prefer-theme-elc' is non-nil."
+ (if (or (null after-init-time)
+ doom--prefer-theme-elc)
+ (cl-letf* ((old-locate-file (symbol-function 'locate-file))
+ ((symbol-function 'locate-file)
+ (lambda (filename path &optional _suffixes predicate)
+ (funcall old-locate-file filename path '("c" "") predicate))))
+ (apply orig-fn args))
+ (apply orig-fn args)))
+(advice-add #'load-theme :around #'doom*prefer-compiled-theme)
+
+(after! whitespace
+ (defun doom*disable-whitespace-mode-in-childframes (orig-fn)
+ "`whitespace-mode' inundates child frames with whitspace markers, so disable
+it to fix all that visual noise."
+ (unless (frame-parameter nil 'parent-frame)
+ (funcall orig-fn)))
+ (add-function :around whitespace-enable-predicate #'doom*disable-whitespace-mode-in-childframes)
+
+ (defun doom|disable-whitespace-mode-in-childframes (frame)
+ "`whitespace-mode' inundates child frames with whitspace markers, so disable
+it to fix all that visual noise."
+ (when (frame-parameter frame 'parent-frame)
+ (with-selected-frame frame
+ (setq-local whitespace-style nil)
+ frame)))
+ (add-hook 'after-make-frame-functions #'doom|disable-whitespace-mode-in-childframes))
+
+;; Don't allow cursor to enter the prompt
+(setq minibuffer-prompt-properties '(read-only t intangible t cursor-intangible t face minibuffer-prompt))
+(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
+
+;; Don't display messages in the minibuffer when using the minibuffer
+(defmacro doom-silence-motion-key (command key)
+ (let ((key-command (intern (format "doom/silent-%s" command))))
`(progn
- (defun ,sym () ,@forms)
- ,(unless (bound-and-true-p byte-compile-current-file)
- `(let (byte-compile-warnings)
- (byte-compile #',sym))))))
+ (defun ,key-command ()
+ (interactive)
+ (ignore-errors (call-interactively ',command)))
+ (define-key minibuffer-local-map (kbd ,key) #',key-command))))
+(doom-silence-motion-key backward-delete-char "")
+(doom-silence-motion-key delete-char "")
-(defsubst doom--prepare-modeline-segments (segments)
- (cl-loop for seg in segments
- if (stringp seg)
- collect seg
- else
- collect (list (intern (format "doom-modeline-segment--%s" (symbol-name seg))))))
-
-(defmacro def-modeline! (name lhs &optional rhs)
- "Defines a modeline format and byte-compiles it. NAME is a symbol to identify
-it (used by `doom-modeline' for retrieval). LHS and RHS are lists of symbols of
-modeline segments defined with `def-modeline-segment!'.
-
-Example:
- (def-modeline! minimal
- (bar matches \" \" buffer-info)
- (media-info major-mode))
- (doom-set-modeline 'minimal t)"
- (let ((sym (intern (format "doom-modeline-format--%s" name)))
- (lhs-forms (doom--prepare-modeline-segments lhs))
- (rhs-forms (doom--prepare-modeline-segments rhs)))
- `(progn
- (defun ,sym ()
- (let ((lhs (list ,@lhs-forms))
- (rhs (list ,@rhs-forms)))
- (let ((rhs-str (format-mode-line rhs)))
- (list lhs
- (propertize
- " " 'display
- `((space :align-to (- (+ right right-fringe right-margin)
- ,(+ 1 (string-width rhs-str))))))
- rhs-str))))
- ,(unless (bound-and-true-p byte-compile-current-file)
- `(let (byte-compile-warnings)
- (byte-compile #',sym))))))
-
-(defun doom-modeline (key)
- "Returns a mode-line configuration associated with KEY (a symbol). Throws an
-error if it doesn't exist."
- (let ((fn (intern (format "doom-modeline-format--%s" key))))
- (when (functionp fn)
- `(:eval (,fn)))))
-
-(defun doom-set-modeline (key &optional default)
- "Set the modeline format. Does nothing if the modeline KEY doesn't exist. If
-DEFAULT is non-nil, set the default mode-line for all buffers."
- (when-let* ((modeline (doom-modeline key)))
- (setf (if default
- (default-value 'mode-line-format)
- (buffer-local-value 'mode-line-format (current-buffer)))
- modeline)))
+;; Switch to `doom-fallback-buffer' if on last real buffer
+(advice-add #'kill-this-buffer :around #'doom*switch-to-fallback-buffer-maybe)
(provide 'core-ui)
;;; core-ui.el ends here
diff --git a/core/core.el b/core/core.el
index fbcc2348d..2df7dd7e5 100644
--- a/core/core.el
+++ b/core/core.el
@@ -1,38 +1,40 @@
;;; core.el --- the heart of the beast -*- lexical-binding: t; -*-
-;;; Naming conventions:
-;;
-;; doom-... public variables or non-interactive functions
-;; doom--... private anything (non-interactive), not safe for direct use
-;; doom/... an interactive function; safe for M-x or keybinding
-;; doom//... an interactive function for managing/maintaining Doom itself
-;; doom:... an evil operator, motion or command
-;; doom|... hook function
-;; doom*... advising functions
-;; doom@... a hydra command
-;; ...! a macro or function that configures DOOM
-;; =... an interactive command that starts an app module
-;; %... functions used for in-snippet logic
-;; +... Any of the above but part of a module, e.g. `+emacs-lisp|init-hook'
-;;
-;; Autoloaded functions are in core/autoload/*.el and modules/*/*/autoload.el or
-;; modules/*/*/autoload/*.el.
-
-(defvar doom-version "2.0.9"
- "Current version of DOOM emacs.")
+(eval-when-compile
+ (and (version< emacs-version "25.3")
+ (error "Detected Emacs %s. Doom only supports Emacs 25.3 and higher"
+ emacs-version)))
(defvar doom-debug-mode (or (getenv "DEBUG") init-file-debug)
"If non-nil, all doom functions will be verbose. Set DEBUG=1 in the command
line or use --debug-init to enable this.")
-(defvar doom-emacs-dir (file-truename user-emacs-directory)
- "The path to this emacs.d directory.")
+
+;;
+;;; Constants
+
+(defconst doom-version "2.0.9"
+ "Current version of DOOM emacs.")
+
+(defconst EMACS26+ (> emacs-major-version 25))
+(defconst EMACS27+ (> emacs-major-version 26))
+
+(defconst IS-MAC (eq system-type 'darwin))
+(defconst IS-LINUX (eq system-type 'gnu/linux))
+(defconst IS-WINDOWS (memq system-type '(cygwin windows-nt ms-dos)))
+(defconst IS-BSD (or IS-MAC (eq system-type 'berkeley-unix)))
+
+
+;;
+(defvar doom-emacs-dir
+ (eval-when-compile (file-truename user-emacs-directory))
+ "The path to this emacs.d directory. Must end in a slash.")
(defvar doom-core-dir (concat doom-emacs-dir "core/")
- "Where essential files are stored.")
+ "The root directory of core Doom files.")
(defvar doom-modules-dir (concat doom-emacs-dir "modules/")
- "Where configuration modules are stored.")
+ "The root directory for Doom's modules.")
(defvar doom-local-dir (concat doom-emacs-dir ".local/")
"Root directory for local Emacs files. Use this as permanent storage for files
@@ -53,152 +55,153 @@ Use this for files that change often, like cache files.")
(defvar doom-packages-dir (concat doom-local-dir "packages/")
"Where package.el and quelpa plugins (and their caches) are stored.")
+(defvar doom-docs-dir (concat doom-emacs-dir "docs/")
+ "Where the Doom manual is stored.")
+
+(defvar doom-private-dir
+ (or (getenv "DOOMDIR")
+ (let ((xdg-path
+ (expand-file-name "doom/"
+ (or (getenv "XDG_CONFIG_HOME")
+ "~/.config"))))
+ (if (file-directory-p xdg-path) xdg-path))
+ "~/.doom.d/")
+ "Where your private customizations are placed. Must end in a slash. Respects
+XDG directory conventions if ~/.config/doom exists.")
+
(defvar doom-autoload-file (concat doom-local-dir "autoloads.el")
- "Where `doom//reload-autoloads' will generate its autoloads file.")
+ "Where `doom-reload-doom-autoloads' will generate its core autoloads file.")
-(defgroup doom nil
- "DOOM Emacs, an Emacs configuration for a stubborn, shell-dwelling and
-melodramatic ex-vimmer disappointed with the text-editor status quo."
- :group 'emacs)
+(defvar doom-package-autoload-file (concat doom-local-dir "autoloads.pkg.el")
+ "Where `doom-reload-package-autoloads' will generate its package.el autoloads
+file.")
+
+(defvar doom-env-file (concat doom-local-dir "env")
+ "The location of your env file, generated by `doom env refresh`.
+
+This file contains environment variables scraped from your non and interactive
+shell environment, and is loaded at startup (if it exist)s. This is helpful if
+Emacs can't (easily) be launched from the correct shell session (particular for
+MacOS users).")
-;;;
+;;
+;;; Doom core variables
+
+(defvar doom-init-p nil
+ "Non-nil if `doom-initialize' has run.")
+
+(defvar doom-init-time nil
+ "The time it took, in seconds, for DOOM Emacs to initialize.")
+
+(defvar doom-emacs-changed-p nil
+ "If non-nil, the running version of Emacs is different from the first time
+Doom was setup, which can cause problems.")
+
+(defvar doom-site-load-path (cons doom-core-dir load-path)
+ "The initial value of `load-path', before it was altered by
+`doom-initialize'.")
+
+(defvar doom-site-process-environment process-environment
+ "The initial value of `process-environment', before it was altered by
+`doom-initialize'.")
+
+(defvar doom-site-exec-path exec-path
+ "The initial value of `exec-path', before it was altered by
+`doom-initialize'.")
+
+(defvar doom-site-shell-file-name shell-file-name
+ "The initial value of `shell-file-name', before it was altered by
+`doom-initialize'.")
+
+(defvar doom--last-emacs-file (concat doom-local-dir "emacs-version.el"))
+(defvar doom--last-emacs-version nil)
+(defvar doom--refreshed-p nil)
+(defvar doom--stage 'init)
+
+
+;;
+;;; Custom error types
+
+(define-error 'doom-error "Error in Doom Emacs core")
+(define-error 'doom-hook-error "Error in a Doom startup hook" 'doom-error)
+(define-error 'doom-autoload-error "Error in an autoloads file" 'doom-error)
+(define-error 'doom-module-error "Error in a Doom module" 'doom-error)
+(define-error 'doom-private-error "Error in private config" 'doom-error)
+(define-error 'doom-package-error "Error with packages" 'doom-error)
+
+
+;;
+;;; Custom hooks
+
+(defvar doom-reload-hook nil
+ "A list of hooks to run when `doom/reload' is called.")
+
+
+;;
+;;; Emacs core configuration
+
;; UTF-8 as the default coding system
(when (fboundp 'set-charset-priority)
(set-charset-priority 'unicode)) ; pretty
-(prefer-coding-system 'utf-8) ; pretty
-(set-terminal-coding-system 'utf-8) ; pretty
-(set-keyboard-coding-system 'utf-8) ; pretty
-(set-selection-coding-system 'utf-8) ; perdy
-(setq locale-coding-system 'utf-8) ; please
-(setq-default buffer-file-coding-system 'utf-8) ; with sugar on top
+(prefer-coding-system 'utf-8) ; pretty
+(setq locale-coding-system 'utf-8) ; please
+(unless IS-WINDOWS
+ (setq selection-coding-system 'utf-8)) ; with sugar on top
(setq-default
ad-redefinition-action 'accept ; silence advised function warnings
apropos-do-all t ; make `apropos' more useful
- compilation-always-kill t ; kill compilation process before starting another
- compilation-ask-about-save nil ; save all buffers on `compile'
- compilation-scroll-output t
- confirm-nonexistent-file-or-buffer t
- enable-recursive-minibuffers nil
- debug-on-error (and (not noninteractive) doom-debug-mode)
+ auto-mode-case-fold nil
+ autoload-compute-prefixes nil
+ debug-on-error doom-debug-mode
+ jka-compr-verbose doom-debug-mode ; silence compression messages
+ ffap-machine-p-known 'reject ; don't ping things that look like domain names
+ find-file-visit-truename t ; resolve symlinks when opening files
idle-update-delay 2 ; update ui less often
- load-prefer-newer (or noninteractive doom-debug-mode)
- ;; keep the point out of the minibuffer
- minibuffer-prompt-properties '(read-only t point-entered minibuffer-avoid-prompt face minibuffer-prompt)
+ ;; be quiet at startup; don't load or display anything unnecessary
+ inhibit-startup-message t
+ inhibit-startup-echo-area-message user-login-name
+ inhibit-default-init t
+ initial-major-mode 'fundamental-mode
+ initial-scratch-message nil
;; History & backup settings (save nothing, that's what git is for)
auto-save-default nil
create-lockfiles nil
history-length 500
- make-backup-files nil
+ make-backup-files nil ; don't create backup~ files
+ ;; byte compilation
+ byte-compile-verbose doom-debug-mode
+ byte-compile-warnings '(not free-vars unresolved noruntime lexical make-local)
+ ;; security
+ gnutls-verify-error (not (getenv "INSECURE")) ; you shouldn't use this
+ tls-checktrust gnutls-verify-error
+ tls-program (list "gnutls-cli --x509cafile %t -p %p %h"
+ ;; compatibility fallbacks
+ "gnutls-cli -p %p %h"
+ "openssl s_client -connect %h:%p -no_ssl2 -no_ssl3 -ign_eof")
+ ;; Don't store authinfo in plain text!
+ auth-sources (list (expand-file-name "authinfo.gpg" doom-etc-dir)
+ "~/.authinfo.gpg")
;; files
abbrev-file-name (concat doom-local-dir "abbrev.el")
+ async-byte-compile-log-file (concat doom-etc-dir "async-bytecomp.log")
auto-save-list-file-name (concat doom-cache-dir "autosave")
backup-directory-alist (list (cons "." (concat doom-cache-dir "backup/")))
+ desktop-dirname (concat doom-etc-dir "desktop")
+ desktop-base-file-name "autosave"
+ desktop-base-lock-name "autosave-lock"
pcache-directory (concat doom-cache-dir "pcache/")
- mc/list-file (concat doom-etc-dir "mc-lists.el")
+ request-storage-directory (concat doom-cache-dir "request")
server-auth-dir (concat doom-cache-dir "server/")
shared-game-score-directory (concat doom-etc-dir "shared-game-score/")
tramp-auto-save-directory (concat doom-cache-dir "tramp-auto-save/")
tramp-backup-directory-alist backup-directory-alist
tramp-persistency-file-name (concat doom-cache-dir "tramp-persistency.el")
url-cache-directory (concat doom-cache-dir "url/")
- url-configuration-directory (concat doom-etc-dir "url/"))
+ url-configuration-directory (concat doom-etc-dir "url/")
+ gamegrid-user-score-file-directory (concat doom-etc-dir "games/"))
-;; move custom defs out of init.el
-(setq custom-file (concat doom-etc-dir "custom.el"))
-(load custom-file t t)
-
-;; be quiet at startup; don't load or display anything unnecessary
-(unless noninteractive
- (advice-add #'display-startup-echo-area-message :override #'ignore)
- (setq inhibit-startup-message t
- inhibit-startup-echo-area-message user-login-name
- inhibit-default-init t
- initial-major-mode 'fundamental-mode
- initial-scratch-message nil
- mode-line-format nil))
-
-;; Custom init hooks; clearer than `after-init-hook', `emacs-startup-hook', and
-;; `window-setup-hook'.
-(defvar doom-init-hook nil
- "A list of hooks run when DOOM is initialized, before `doom-post-init-hook'.")
-
-(defvar doom-post-init-hook nil
- "A list of hooks run after DOOM initialization is complete, and after
-`doom-init-hook'.")
-
-(defun doom-try-run-hook (fn hook)
- "Runs a hook wrapped in a `condition-case-unless-debug' block; its objective
-is to include more information in the error message, without sacrificing your
-ability to invoke the debugger in debug mode."
- (condition-case-unless-debug ex
- (if noninteractive
- (quiet! (funcall fn))
- (funcall fn))
- ('error
- (lwarn hook :error
- "%s in '%s' -> %s"
- (car ex) fn (error-message-string ex))))
- nil)
-
-
-;;;
-;; Initialize
-(eval-and-compile
- (defvar doom--file-name-handler-alist file-name-handler-alist)
- (unless (or after-init-time noninteractive)
- ;; One of the contributors to long startup times is the garbage collector,
- ;; so we up its memory threshold, temporarily. It is reset later in
- ;; `doom|finalize'.
- (setq gc-cons-threshold 402653184
- gc-cons-percentage 0.6
- file-name-handler-alist nil))
-
- (require 'cl-lib)
- (load (concat doom-core-dir "core-packages") nil t)
- (setq load-path (eval-when-compile (doom-initialize t)
- (doom-initialize-load-path t))
- doom--package-load-path (eval-when-compile doom--package-load-path))
-
- (load! core-lib)
- (load! core-os) ; consistent behavior across OSes
- (condition-case-unless-debug ex
- (require 'autoloads doom-autoload-file t)
- ('error
- (lwarn 'doom-autoloads :warning
- "%s in autoloads.el -> %s"
- (car ex) (error-message-string ex))))
-
- (unless noninteractive
- (load! core-ui) ; draw me like one of your French editors
- (load! core-popups) ; taming sudden yet inevitable windows
- (load! core-editor) ; baseline configuration for text editing
- (load! core-projects) ; making Emacs project-aware
- (load! core-keybinds)) ; centralized keybind system + which-key
-
- (defun doom|finalize ()
- "Run `doom-init-hook', `doom-post-init-hook' and reset `gc-cons-threshold',
-`gc-cons-percentage' and `file-name-handler-alist'."
- (unless (or (not after-init-time) noninteractive)
- (dolist (hook '(doom-init-hook doom-post-init-hook))
- (run-hook-wrapped hook #'doom-try-run-hook hook)))
-
- ;; If you forget to reset this, you'll get stuttering and random freezes!
- (setq gc-cons-threshold 16777216
- gc-cons-percentage 0.1
- file-name-handler-alist doom--file-name-handler-alist)
- t)
-
- (add-hook! '(emacs-startup-hook doom-reload-hook)
- #'doom|finalize))
-
-
-;;
-;; Emacs fixes/hacks
-;;
-
-;; Automatic minor modes
(defvar doom-auto-minor-mode-alist '()
"Alist mapping filename patterns to corresponding minor mode functions, like
`auto-mode-alist'. All elements of this alist are checked, meaning you can
@@ -214,7 +217,7 @@ enable multiple minor modes for the same regexp.")
(setq name (file-name-sans-versions name))
;; Remove remote file name identification.
(when (and (stringp remote-id)
- (string-match-p (regexp-quote remote-id) name))
+ (string-match (regexp-quote remote-id) name))
(setq name (substring name (match-end 0))))
(while (and alist (caar alist) (cdar alist))
(if (string-match-p (caar alist) name)
@@ -222,24 +225,291 @@ enable multiple minor modes for the same regexp.")
(setq alist (cdr alist))))))
(add-hook 'find-file-hook #'doom|enable-minor-mode-maybe)
-(defun doom*set-indirect-buffer-filename (orig-fn base-buffer name &optional clone)
- "In indirect buffers, `buffer-file-name' is nil, which can cause problems
-with functions that require it (like modeline segments)."
- (let ((file-name (buffer-file-name base-buffer))
- (buffer (funcall orig-fn base-buffer name clone)))
- (when (and file-name buffer)
- (with-current-buffer buffer
- (unless buffer-file-name
- (setq buffer-file-name file-name
- buffer-file-truename (file-truename file-name)))))
- buffer))
-(advice-add #'make-indirect-buffer :around #'doom*set-indirect-buffer-filename)
+(defun doom*symbol-file (orig-fn symbol &optional type)
+ "If a `doom-file' symbol property exists on SYMBOL, use that instead of the
+original value of `symbol-file'."
+ (or (if (symbolp symbol) (get symbol 'doom-file))
+ (funcall orig-fn symbol type)))
+(advice-add #'symbol-file :around #'doom*symbol-file)
-(defun doom*no-authinfo-for-tramp (orig-fn &rest args)
- "Don't look into .authinfo for local sudo TRAMP buffers."
- (let ((auth-sources (if (equal tramp-current-method "sudo") nil auth-sources)))
- (apply orig-fn args)))
-(advice-add #'tramp-read-passwd :around #'doom*no-authinfo-for-tramp)
+;; To speed up minibuffer commands (like helm and ivy), defer garbage collection
+;; when the minibuffer is active. It may mean a pause when finished, but that's
+;; acceptable instead of pauses during.
+(defun doom|defer-garbage-collection ()
+ (setq gc-cons-threshold doom-gc-cons-upper-limit))
+(defun doom|restore-garbage-collection ()
+ (setq gc-cons-threshold doom-gc-cons-threshold))
+(add-hook 'minibuffer-setup-hook #'doom|defer-garbage-collection)
+(add-hook 'minibuffer-exit-hook #'doom|restore-garbage-collection)
+
+;; File+dir local variables are initialized after the major mode and its hooks
+;; have run. If you want hook functions to be aware of these customizations, add
+;; them to MODE-local-vars-hook instead.
+(defun doom|run-local-var-hooks ()
+ "Run MODE-local-vars-hook after local variables are initialized."
+ (run-hook-wrapped (intern-soft (format "%s-local-vars-hook" major-mode))
+ #'doom-try-run-hook))
+(add-hook 'hack-local-variables-hook #'doom|run-local-var-hooks)
+
+(defun doom|run-local-var-hooks-if-necessary ()
+ "If `enable-local-variables' is disabled, then `hack-local-variables-hook' is
+never triggered."
+ (unless enable-local-variables
+ (doom|run-local-var-hooks)))
+(add-hook 'after-change-major-mode-hook #'doom|run-local-var-hooks-if-necessary)
+
+
+;;
+;;; Incremental lazy-loading
+
+(defvar doom-incremental-packages '(t)
+ "A list of packages to load incrementally after startup. Any large packages
+here may cause noticable pauses, so it's recommended you break them up into
+sub-packages. For example, `org' is comprised of many packages, and can be broken up into:
+
+ (doom-load-packages-incrementally
+ '(calendar find-func format-spec org-macs org-compat
+ org-faces org-entities org-list org-pcomplete org-src
+ org-footnote org-macro ob org org-clock org-agenda
+ org-capture))
+
+This is already done by the lang/org module, however.
+
+If you want to disable incremental loading altogether, either remove
+`doom|load-packages-incrementally' from `emacs-startup-hook' or set
+`doom-incremental-first-idle-timer' to nil.")
+
+(defvar doom-incremental-first-idle-timer 2
+ "How long (in idle seconds) until incremental loading starts.
+
+Set this to nil to disable incremental loading.")
+
+(defvar doom-incremental-idle-timer 1.5
+ "How long (in idle seconds) in between incrementally loading packages.")
+
+(defun doom-load-packages-incrementally (packages &optional now)
+ "Registers PACKAGES to be loaded incrementally.
+
+If NOW is non-nil, load PACKAGES incrementally, in `doom-incremental-idle-timer'
+intervals."
+ (if (not now)
+ (nconc doom-incremental-packages packages)
+ (when packages
+ (let ((gc-cons-threshold doom-gc-cons-upper-limit)
+ file-name-handler-alist)
+ (let* ((reqs (cl-delete-if #'featurep packages))
+ (req (ignore-errors (pop reqs))))
+ (when req
+ (doom-log "Incrementally loading %s" req)
+ (condition-case e
+ (require req nil t)
+ ((error debug)
+ (message "Failed to load '%s' package incrementally, because: %s"
+ req e)))
+ (if reqs
+ (run-with-idle-timer doom-incremental-idle-timer
+ nil #'doom-load-packages-incrementally
+ reqs t)
+ (doom-log "Finished incremental loading"))))))))
+
+(defun doom|load-packages-incrementally ()
+ "Begin incrementally loading packages in `doom-incremental-packages'.
+
+If this is a daemon session, load them all immediately instead."
+ (if (daemonp)
+ (mapc #'require (cdr doom-incremental-packages))
+ (when (integerp doom-incremental-first-idle-timer)
+ (run-with-idle-timer doom-incremental-first-idle-timer
+ nil #'doom-load-packages-incrementally
+ (cdr doom-incremental-packages) t))))
+
+(add-hook 'window-setup-hook #'doom|load-packages-incrementally)
+
+
+;;
+;;; Bootstrap helpers
+
+(defun doom-try-run-hook (hook)
+ "Run HOOK (a hook function), but handle errors better, to make debugging
+issues easier.
+
+Meant to be used with `run-hook-wrapped'."
+ (doom-log "Running doom hook: %s" hook)
+ (condition-case e
+ (funcall hook)
+ ((debug error)
+ (signal 'doom-hook-error (list hook e))))
+ ;; return nil so `run-hook-wrapped' won't short circuit
+ nil)
+
+(defun doom-ensure-same-emacs-version-p ()
+ "Check if the running version of Emacs has changed and set
+`doom-emacs-changed-p' if it has."
+ (if (load doom--last-emacs-file 'noerror 'nomessage 'nosuffix)
+ (setq doom-emacs-changed-p
+ (not (equal emacs-version doom--last-emacs-version)))
+ (with-temp-file doom--last-emacs-file
+ (princ `(setq doom--last-emacs-version ,(prin1-to-string emacs-version))
+ (current-buffer))))
+ (cond ((not doom-emacs-changed-p))
+ ((y-or-n-p
+ (format
+ (concat "Your version of Emacs has changed from %s to %s, which may cause incompatibility\n"
+ "issues. If you run into errors, run `bin/doom compile :plugins` or reinstall your\n"
+ "plugins to resolve them.\n\n"
+ "Continue?")
+ doom--last-emacs-version
+ emacs-version))
+ (delete-file doom--last-emacs-file))
+ (noninteractive (error "Aborting"))
+ ((kill-emacs))))
+
+(defun doom-ensure-core-directories-exist ()
+ "Make sure all Doom's essential local directories (in and including
+`doom-local-dir') exist."
+ (dolist (dir (list doom-local-dir doom-etc-dir doom-cache-dir doom-packages-dir))
+ (unless (file-directory-p dir)
+ (make-directory dir t))))
+
+(defun doom|display-benchmark (&optional return-p)
+ "Display a benchmark, showing number of packages and modules, and how quickly
+they were loaded at startup.
+
+If RETURN-P, return the message as a string instead of displaying it."
+ (funcall (if return-p #'format #'message)
+ "Doom loaded %s packages across %d modules in %.03fs"
+ (length package-activated-list)
+ (if doom-modules (hash-table-count doom-modules) 0)
+ (or doom-init-time
+ (setq doom-init-time (float-time (time-subtract (current-time) before-init-time))))))
+
+(defun doom|run-all-startup-hooks ()
+ "Run all startup Emacs hooks. Meant to be executed after starting Emacs with
+-q or -Q, for example:
+
+ emacs -Q -l init.el -f doom|run-all-startup-hooks"
+ (run-hook-wrapped 'after-init-hook #'doom-try-run-hook)
+ (setq after-init-time (current-time))
+ (dolist (hook (list 'delayed-warnings-hook
+ 'emacs-startup-hook 'term-setup-hook
+ 'window-setup-hook))
+ (run-hook-wrapped hook #'doom-try-run-hook)))
+
+(defun doom-initialize-autoloads (file)
+ "Tries to load FILE (an autoloads file). Return t on success, throws an error
+in interactive sessions, nil otherwise (but logs a warning)."
+ (condition-case e
+ (load (file-name-sans-extension file) 'noerror 'nomessage)
+ ((debug error)
+ (if noninteractive
+ (message "Autoload file warning: %s -> %s" (car e) (error-message-string e))
+ (signal 'doom-autoload-error (list (file-name-nondirectory file) e))))))
+
+(defun doom-initialize (&optional force-p)
+ "Bootstrap Doom, if it hasn't already (or if FORCE-P is non-nil).
+
+The bootstrap process involves making sure 1) the essential directories exist,
+2) the core packages are installed, 3) `doom-autoload-file' and
+`doom-package-autoload-file' exist and have been loaded, and 4) Doom's core
+files are loaded.
+
+If the cache exists, much of this function isn't run, which substantially
+reduces startup time.
+
+The overall load order of Doom is as follows:
+
+ ~/.emacs.d/init.el
+ ~/.emacs.d/core/core.el
+ ~/.doom.d/init.el
+ Module init.el files
+ `doom-before-init-modules-hook'
+ Module config.el files
+ ~/.doom.d/config.el
+ `doom-init-modules-hook'
+ `after-init-hook'
+ `emacs-startup-hook'
+ `doom-init-ui-hook'
+ `window-setup-hook'
+
+Module load order is determined by your `doom!' block. See `doom-modules-dirs'
+for a list of all recognized module trees. Order defines precedence (from most
+to least)."
+ (when (or force-p (not doom-init-p))
+ (setq doom-init-p t) ; Prevent infinite recursion
+
+ ;; Reset as much state as possible
+ (setq exec-path doom-site-exec-path
+ load-path doom-site-load-path
+ process-environment doom-site-process-environment
+ shell-file-name doom-site-shell-file-name)
+
+ ;; `doom-autoload-file' tells Emacs where to load all its autoloaded
+ ;; functions from. This includes everything in core/autoload/*.el and all
+ ;; the autoload files in your enabled modules.
+ (when (or force-p (not (doom-initialize-autoloads doom-autoload-file)))
+ (doom-ensure-core-directories-exist)
+ (doom-ensure-same-emacs-version-p)
+
+ (require 'core-packages)
+ (doom-ensure-packages-initialized force-p)
+ (doom-ensure-core-packages)
+
+ (unless (or force-p noninteractive)
+ (user-error "Your doom autoloads are missing! Run `bin/doom refresh' to regenerate them")))
+
+ ;; Loads `doom-package-autoload-file', which loads a concatenated package
+ ;; autoloads file and caches `load-path', `auto-mode-alist',
+ ;; `Info-directory-list', `doom-disabled-packages' and
+ ;; `package-activated-list'. A big reduction in startup time.
+ (let (command-switch-alist)
+ (unless (or force-p
+ (doom-initialize-autoloads doom-package-autoload-file)
+ noninteractive)
+ (user-error "Your package autoloads are missing! Run `bin/doom refresh' to regenerate them")))
+
+ ;; Load shell environment
+ (when (and (not noninteractive)
+ (file-readable-p doom-env-file)
+ (require 'load-env-vars nil t))
+ (load-env-vars doom-env-file)
+ (setq exec-path (append (split-string (getenv "PATH") ":")
+ (list exec-directory))
+ shell-file-name (or (getenv "SHELL")
+ shell-file-name))))
+
+ (require 'core-lib)
+ (require 'core-modules)
+ (require 'core-os)
+ (if noninteractive
+ (require 'core-cli)
+ (add-hook 'window-setup-hook #'doom|display-benchmark)
+ (require 'core-keybinds)
+ (require 'core-ui)
+ (require 'core-projects)
+ (require 'core-editor)))
+
+
+;;
+;;; Bootstrap Doom
+
+(eval-and-compile
+ (require 'subr-x)
+ (require 'cl-lib)
+ (unless EMACS26+
+ (with-no-warnings
+ ;; if-let and when-let were moved to (if|when)-let* in Emacs 26+ so we
+ ;; alias them for 25 users.
+ (defalias 'if-let* #'if-let)
+ (defalias 'when-let* #'when-let))))
+
+(add-to-list 'load-path doom-core-dir)
+
+(doom-initialize noninteractive)
+(unless noninteractive
+ (doom-initialize-modules))
+(after! package
+ (require 'core-packages)
+ (doom-initialize-packages))
(provide 'core)
;;; core.el ends here
diff --git a/core/doctor.el b/core/doctor.el
new file mode 100644
index 000000000..8ac64af99
--- /dev/null
+++ b/core/doctor.el
@@ -0,0 +1,37 @@
+;;; core/doctor.el -*- lexical-binding: t; -*-
+
+(defun file-size (file &optional dir)
+ (setq file (expand-file-name file dir))
+ (when (file-exists-p file)
+ (/ (nth 7 (file-attributes file))
+ 1024.0)))
+
+;; Check for oversized problem files in cache that may cause unusual/tremendous
+;; delays or freezing. This shouldn't happen often.
+(dolist (file (list "savehist"
+ "projectile.cache"))
+ (let* ((path (expand-file-name file doom-cache-dir))
+ (size (file-size path)))
+ (when (and (numberp size) (> size 2000))
+ (warn! "%s is too large (%.02fmb). This may cause freezes or odd startup delays"
+ (file-relative-name path doom-core-dir)
+ (/ size 1024))
+ (explain! "Consider deleting it from your system (manually)"))))
+
+(when! (not (executable-find doom-projectile-fd-binary))
+ (warn! "Couldn't find the `fd' binary; project file searches will be slightly slower"))
+
+(let ((default-directory "~"))
+ (require 'projectile)
+ (when! (cl-find-if #'projectile-file-exists-p projectile-project-root-files-bottom-up)
+ (warn! "Your $HOME is recognized as a project root")
+ (explain! "Doom will disable bottom-up root search, which may reduce the accuracy of project\n"
+ "detection.")))
+
+;; There should only be one
+(when! (and (file-equal-p doom-private-dir "~/.config/doom")
+ (file-directory-p "~/.doom.d"))
+ (warn! "Both %S and '~/.doom.d' exist on your system"
+ (abbreviate-file-name doom-private-dir))
+ (explain! "Doom will only load one of these (~/.config/doom takes precedence). Since\n"
+ "it is rarely intentional that you have both, ~/.doom.d should be removed."))
diff --git a/core/packages.el b/core/packages.el
index 054197e4a..ad01ecc9a 100644
--- a/core/packages.el
+++ b/core/packages.el
@@ -1,44 +1,51 @@
;; -*- no-byte-compile: t; -*-
;;; core/packages.el
+;; core.el
+(package! dotenv-mode)
+
;; core-os.el
-;; In case this config is shared across multiple computers (like mine is), let's
-;; protect these from autoremoval.
-(package! exec-path-from-shell :ignore (not IS-MAC))
-(package! osx-clipboard :ignore (not IS-MAC))
+(if (not IS-MAC)
+ (package! xclip)
+ (package! osx-clipboard)
+ (package! ns-auto-titlebar))
;; core-ui.el
(package! all-the-icons)
-(package! fringe-helper)
-(package! highlight-indentation)
+(package! hide-mode-line)
(package! highlight-numbers)
-(unless (boundp 'display-line-numbers)
+(package! highlight-escape-sequences
+ :recipe (:fetcher github :repo "hlissner/highlight-escape-sequences"))
+(unless (locate-library "display-line-numbers")
(package! nlinum)
(package! nlinum-hl)
(package! nlinum-relative))
(package! rainbow-delimiters)
(package! visual-fill-column)
-
-;; core-popups.el
-(package! shackle)
+(package! restart-emacs)
;; core-editor.el
(package! ace-link)
(package! ace-window)
(package! avy)
(package! command-log-mode)
-(package! editorconfig)
-(package! expand-region)
-(package! help-fns+)
+(package! dtrt-indent)
+(package! helpful)
(package! pcre2el)
-(package! smart-forward)
(package! smartparens)
(package! undo-tree)
-(package! wgrep)
+(package! ws-butler)
;; core-projects.el
(package! projectile)
;; core-keybinds.el
+(package! general)
(package! which-key)
(package! hydra)
+
+;; autoload/debug.el
+(package! esup)
+
+;; autoload/test.el
+(package! buttercup)
diff --git a/core/templates/BUG_REPORT b/core/templates/BUG_REPORT
new file mode 100644
index 000000000..a46909cce
--- /dev/null
+++ b/core/templates/BUG_REPORT
@@ -0,0 +1,29 @@
+Please read through the following before you submit your issue.
+
++ [ ] Running `make` (then restarting Emacs) did not fix my issue
++ [ ] If I have byte-compiled, I've tried recompiling with `make compile`
++ [ ] If I changed the version of Emacs installed, I've recompiled by plugins
+ with `make compile-elpa`
++ [ ] I ran `make doctor` and it produced no leads
++ [ ] My issue cannot be found [on the wiki](/docs/troubleshoot.org)
++ [ ] I filled out the four fields in the template below
+
+-------------------------------------------------------------------
+
+### Observed behavior
+
+
+
+### Expected behavior
+
+
+
+### Steps to reproduce
+
+
+
+### Extra details
+
+
+
+-------------------------------------------------------------------
diff --git a/core/templates/QUICKSTART_INTRO b/core/templates/QUICKSTART_INTRO
new file mode 100644
index 000000000..bf3abef0e
--- /dev/null
+++ b/core/templates/QUICKSTART_INTRO
@@ -0,0 +1,30 @@
+Before you doom yourself, there are a few things you should know:
+
+1. If you use GUI Emacs, run `M-x all-the-icons-install-fonts` so you don't get
+ weird symbols all over the place.
+
+2. Whenever you edit ~/.doom.d/init.el or modify modules, run:
+
+ bin/doom refresh
+
+ This will ensure all needed packages are installed, all orphaned packages are
+ removed, and your autoloads files are up to date. This is important! If you
+ forget to do this you will get errors!
+
+3. If something inexplicably goes wrong, it's a good idea to try:
+
+ bin/doom doctor
+
+ This will diagnose common issues with your environment and setup, and may
+ give you clues about what is wrong.
+
+4. To update doom, run
+
+ bin/doom upgrade
+
+ Doing it any other way will require you run `bin/doom refresh` otherwise,
+
+5. Check out `bin/doom help` to see what else it can do (it is recommended you
+ add ~/.emacs.d/bin to your PATH).
+
+Have fun!
diff --git a/core/templates/VANILLA_SANDBOX b/core/templates/VANILLA_SANDBOX
new file mode 100644
index 000000000..f9fe77dcc
--- /dev/null
+++ b/core/templates/VANILLA_SANDBOX
@@ -0,0 +1,12 @@
+;; Welcome to the vanilla sandbox!
+;;
+;; This is a test bed for running Emacs Lisp in an instance of Emacs with varying
+;; amounts of Doom loaded:
+;;
+;; a) vanilla Emacs (nothing loaded) \\[doom--run-vanilla-emacs]
+;; b) vanilla Doom (only Doom core) \\[doom--run-vanilla-doom]
+;; c) Doom + modules - your private config \\[doom--run-vanilla-doom+]
+;; d) Doom (normal) \\[doom--run-full-doom]
+;;
+;; This is done without sacrificing access to installed packages. Use the sandbox
+;; to reproduce bugs and determine if Doom is to blame.
diff --git a/core/test/autoload-buffers.el b/core/test/autoload-buffers.el
deleted file mode 100644
index b39c75b3c..000000000
--- a/core/test/autoload-buffers.el
+++ /dev/null
@@ -1,135 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; core/test/autoload-buffers.el
-
-(defmacro with-temp-buffers!! (buffer-args &rest body)
- (declare (indent defun))
- (let (buffers)
- (dolist (bsym buffer-args)
- (push `(,bsym (get-buffer-create ,(symbol-name bsym)))
- buffers))
- `(cl-flet ((buffer-list
- (lambda ()
- (cl-remove-if-not #'buffer-live-p (list ,@(reverse (mapcar #'car buffers)))))))
- (let* (persp-mode
- ,@buffers)
- ,@body
- (mapc #'kill-buffer (buffer-list))))))
-
-;;
-(def-test! get-buffers
- (with-temp-buffers!! (a b c)
- (should (cl-every #'buffer-live-p (buffer-list)))
- (should (equal (buffer-list) (list a b c)))
- (dolist (buf (list (cons a doom-emacs-dir)
- (cons b doom-emacs-dir)
- (cons c "/tmp/")))
- (with-current-buffer (car buf)
- (setq-local default-directory (cdr buf))))
- (projectile-mode +1)
- (with-current-buffer a
- ;; should produce all buffers
- (let ((buffers (doom-buffer-list)))
- (should (cl-every (lambda (x) (memq x buffers)) (list a b c))))
- ;; should produce only project buffers
- (let ((buffers (doom-project-buffer-list)))
- (should (cl-every (lambda (x) (memq x buffers)) (list a b)))
- (should-not (memq c buffers))))
- ;; If no project is available, just get all buffers
- (with-current-buffer c
- (let ((buffers (doom-project-buffer-list)))
- (should (cl-every (lambda (x) (memq x buffers)) (list a b c)))))
- (projectile-mode -1)))
-
-(def-test! real-buffers
- (let (doom-real-buffer-functions)
- (with-temp-buffers!! (a b c d)
- (dolist (buf (list a b))
- (with-current-buffer buf
- (setq-local buffer-file-name "x")))
- (with-current-buffer c
- (rename-buffer "*C*"))
- (with-current-buffer d
- (doom-popup-mode +1))
- (should (doom-real-buffer-p a))
- (should (doom-real-buffer-p b))
- (should-not (doom-real-buffer-p c))
- (should-not (doom-real-buffer-p d))
- (let ((buffers (doom-real-buffer-list)))
- (should (= (length buffers) 2))
- (should (cl-every (lambda (x) (memq x buffers)) (list a b)))
- (should (cl-notany (lambda (x) (memq x buffers)) (list c d)))))))
-
-;; `doom-visible-windows'
-;; `doom-visible-buffers'
-;; `doom-buried-buffers'
-(def-test! visible-buffers-and-windows
- (with-temp-buffers!! (a b c d)
- (switch-to-buffer a)
- (should (eq (current-buffer) a))
- (should (eq (selected-window) (get-buffer-window a)))
- (split-window nil 1)
- (switch-to-buffer b)
- (should (eq (current-buffer) b))
- (should (eq (selected-window) (get-buffer-window b)))
- (should (cl-intersection (list a b) (doom-visible-buffers)))
- (should (cl-intersection (list c d) (doom-buried-buffers)))
- (should (cl-intersection (mapcar #'get-buffer-window (list a b))
- (doom-visible-windows)))))
-
-;; `doom-matching-buffers'
-(def-test! matching-buffers
- (with-temp-buffers!! (a b c)
- (let ((buffers (doom-matching-buffers "^[ac]$")))
- (should (= 2 (length buffers)))
- (should (cl-every #'bufferp buffers))
- (should (cl-every (lambda (x) (memq x buffers)) (list a c)))
- (should (equal buffers (doom-matching-buffers "^[ac]$"))))))
-
-;; `doom-buffers-in-mode'
-(def-test! buffers-in-mode
- (with-temp-buffers!! (a b c d e)
- (dolist (buf (list a b))
- (with-current-buffer buf
- (emacs-lisp-mode)))
- (dolist (buf (list c d e))
- (with-current-buffer buf
- (text-mode)))
- (let ((el-buffers (doom-buffers-in-mode 'emacs-lisp-mode))
- (txt-buffers (doom-buffers-in-mode 'text-mode)))
- (should (cl-every #'buffer-live-p (append el-buffers txt-buffers)))
- (should (= 2 (length el-buffers)))
- (should (= 3 (length txt-buffers))))))
-
-;; `doom-kill-buffer'
-(def-test! kill-buffer
- (with-temp-buffers!! (a b)
- (doom-kill-buffer a)
- (should-not (buffer-live-p a))
- ;; modified buffer
- (with-current-buffer b
- (set-buffer-modified-p t))
- (doom-kill-buffer b t)
- (should-not (buffer-live-p a))))
-
-;; `doom--cycle-real-buffers'
-(def-test! kill-buffer-then-show-real-buffer
- (with-temp-buffers!! (a b c d)
- (dolist (buf (list a b d))
- (with-current-buffer buf
- (setq-local buffer-file-name "x")))
- (should (cl-every #'buffer-live-p (buffer-list)))
- (switch-to-buffer a)
- (should (eq (current-buffer) a))
- (should (eq (selected-window) (get-buffer-window a)))
- (should (doom-kill-buffer a))
- ;; eventually end up in the fallback buffer
- (let ((fallback (doom-fallback-buffer)))
- (while (not (eq (current-buffer) fallback))
- (should (doom-real-buffer-p))
- (doom-kill-buffer))
- (should (eq (current-buffer) fallback)))))
-
-;; TODO doom/kill-all-buffers
-;; TODO doom/kill-other-buffers
-;; TODO doom/kill-matching-buffers
-;; TODO doom/cleanup-session
diff --git a/core/test/autoload-debug.el b/core/test/autoload-debug.el
deleted file mode 100644
index 062a16de5..000000000
--- a/core/test/autoload-debug.el
+++ /dev/null
@@ -1,17 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; core/test/autoload-debug.el
-
-(def-test! what-face
- (insert (propertize "Hello " 'face 'font-lock-keyword-face))
- (insert "world")
-
- (should (equal (doom/what-face (point-min)) '((font-lock-keyword-face) ())))
- (should-not (doom/what-face (point-max))))
-
-(def-test! what-face-overlays
- (insert "Hello world")
- (let ((ov (make-overlay 1 6)))
- (overlay-put ov 'face 'font-lock-keyword-face))
-
- (should (equal (doom/what-face (point-min)) '(() (font-lock-keyword-face))))
- (should-not (doom/what-face (point-max))))
diff --git a/core/test/autoload-message.el b/core/test/autoload-message.el
deleted file mode 100644
index c3e456a2f..000000000
--- a/core/test/autoload-message.el
+++ /dev/null
@@ -1,41 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; core/test/autoload-message.el
-
-;; ansi messages
-(def-test! ansi-format
- (let ((noninteractive t))
- (should (equal (format! "Hello %s" "World")
- "Hello World"))
- (should (equal (format! (red "Hello %s" "World"))
- "[31mHello World[0m"))
- (should (equal (format! (green "Hello %s" "World"))
- (format "\e[%dm%s\e[0m"
- (cdr (assq 'green doom-message-fg))
- "Hello World")))
- (should (equal (format! (on-red "Hello %s" "World"))
- (format "\e[%dm%s\e[0m"
- (cdr (assq 'on-red doom-message-bg))
- "Hello World")))
- (should (equal (format! (bold "Hello %s" "World"))
- (format "\e[%dm%s\e[0m"
- (cdr (assq 'bold doom-message-fx))
- "Hello World")))))
-
-(def-test! ansi-format-nested
- (let ((noninteractive t))
- (should (equal (format! (bold (red "Hello %s" "World")))
- (format "\e[%dm%s\e[0m" 1
- (format "\e[%dm%s\e[0m" 31 "Hello World"))))
- (should (equal (format! (on-red (bold "Hello %s" "World")))
- (format "\e[%dm%s\e[0m" 41
- (format "\e[%dm%s\e[0m" 1 "Hello World"))))
- (should (equal (format! (dark (white "Hello %s" "World")))
- (format "\e[%dm%s\e[0m" 2
- (format "\e[%dm%s\e[0m" 37 "Hello World"))))))
-
-(def-test! ansi-format-apply
- (let ((noninteractive t))
- (should (equal (format! (color 'red "Hello %s" "World"))
- (format! (red "Hello %s" "World"))))
- (should (equal (format! (color (if nil 'red 'blue) "Hello %s" "World"))
- (format! (blue "Hello %s" "World"))))))
diff --git a/core/test/autoload-package.el b/core/test/autoload-package.el
deleted file mode 100644
index 5d074b0b7..000000000
--- a/core/test/autoload-package.el
+++ /dev/null
@@ -1,70 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; core/test/autoload-package.el
-
-(defun -pkg (name version &optional reqs)
- (package-desc-create :name name :version version :reqs reqs))
-
-(defmacro with-packages!! (packages package-descs &rest body)
-`(let* ((doom-packages-dir ,(expand-file-name "packages/" (file-name-directory load-file-name)))
- (package-user-dir ,(expand-file-name "elpa" doom-packages-dir))
- (quelpa-dir ,(expand-file-name "quelpa" doom-packages-dir)))
- ;; (make-directory doom-packages-dir t)
- (let ((doom-packages ,packages)
- (package-alist ,package-descs)
- doom-core-packages)
- (cl-letf (((symbol-function 'doom-initialize-packages) (lambda (&rest _))))
- ,@body))
- ;; (delete-directory doom-packages-dir t)
- ))
-
-
-;;
-;; Tests
-;;
-
-(def-test! backend-detection
- (let ((package-alist `((doom-dummy ,(-pkg 'doom-dummy '(20160405 1234)))))
- (quelpa-cache '((doom-quelpa-dummy :fetcher github :repo "hlissner/does-not-exist")))
- (quelpa-initialized-p t))
- (should (eq (doom-package-backend 'doom-dummy) 'elpa))
- (should (eq (doom-package-backend 'doom-quelpa-dummy) 'quelpa))
- (should (eq (doom-package-backend 'org) 'emacs))))
-
-(def-test! elpa-outdated-detection
- (let* ((doom--last-refresh (current-time))
- (package-alist
- `((doom-dummy ,(-pkg 'doom-dummy '(20160405 1234)))))
- (package-archive-contents
- `((doom-dummy ,(-pkg 'doom-dummy '(20170405 1234))))))
- (cl-letf (((symbol-function 'package-refresh-contents) (lambda (&rest _))))
- (should (equal (doom-package-outdated-p 'doom-dummy)
- '(doom-dummy (20160405 1234) (20170405 1234)))))))
-
-;; TODO quelpa-outdated-detection
-
-(def-test! get-packages
- (let ((quelpa-initialized-p t))
- (with-packages!!
- '((doom-dummy))
- '((doom-dummy nil)
- (doom-dummy-unwanted nil)
- (doom-dummy-dep nil))
- (should (equal (doom-get-packages) '((doom-dummy)))))))
-
-(def-test! orphaned-packages
- "Test `doom-get-orphaned-packages', which gets a list of packages that are
-no longer enabled or depended on."
- (with-packages!!
- '((doom-dummy))
- `((doom-dummy ,(-pkg 'doom-dummy '(20160405 1234) '((doom-dummy-dep (1 0)))))
- (doom-dummy-unwanted ,(-pkg 'doom-dummy-unwanted '(20160601 1234)))
- (doom-dummy-dep ,(-pkg 'doom-dummy-dep '(20160301 1234))))
- (should (equal (doom-get-orphaned-packages) '(doom-dummy-unwanted)))))
-
-(def-test! missing-packages
- "Test `doom-get-missing-packages, which gets a list of enabled packages that
-aren't installed."
- (with-packages!!
- '((doom-dummy) (doom-dummy-installed))
- `((doom-dummy-installed ,(-pkg 'doom-dummy-installed '(20160405 1234))))
- (should (equal (doom-get-missing-packages) '((doom-dummy))))))
diff --git a/core/test/core-lib.el b/core/test/core-lib.el
deleted file mode 100644
index 30e59a690..000000000
--- a/core/test/core-lib.el
+++ /dev/null
@@ -1,159 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; core/test/core-lib.el
-
-;; --- Helpers ----------------------------
-
-;; `doom--resolve-path-forms'
-(def-test! resolve-path-forms
- (should
- (equal (doom--resolve-path-forms '(and "fileA" "fileB"))
- '(and (file-exists-p (expand-file-name "fileA" (doom-project-root)))
- (file-exists-p (expand-file-name "fileB" (doom-project-root)))))))
-
-;; `doom--resolve-hook-forms'
-(def-test! resolve-hook-forms
- (should (equal (doom--resolve-hook-forms '(js2-mode haskell-mode))
- '(js2-mode-hook haskell-mode-hook)))
- (should (equal (doom--resolve-hook-forms '(quote (js2-mode-hook haskell-mode-hook)))
- '(js2-mode-hook haskell-mode-hook))))
-
-;; `doom-unquote'
-(def-test! unquote
- (should (equal (doom-unquote '(quote (a b c))) '(a b c)))
- ;; nested
- (should (equal (doom-unquote '(quote (quote (a b c)))) '(a b c)))
- ;; sub-quote
- (should (equal (doom-unquote '(quote (a (quote b) c))) '(a (quote b) c)))
- ;; function
- (should (equal (doom-unquote '(function a)) 'a)))
-
-;; `doom-enlist'
-(def-test! enlist
- (should (equal (doom-enlist 'a) '(a)))
- (should (equal (doom-enlist '(a)) '(a))))
-
-;; `doom-resolve-vim-path'
-(def-test! resolve-vim-path
- (cl-flet ((do-it #'doom-resolve-vim-path))
- ;; file modifiers
- (let ((buffer-file-name "~/.emacs.d/test/modules/feature/test-evil.el")
- (default-directory "~/.emacs.d/test/modules/"))
- (should (equal (do-it "%") "feature/test-evil.el"))
- (should (equal (do-it "%:r") "feature/test-evil"))
- (should (equal (do-it "%:r.elc") "feature/test-evil.elc"))
- (should (equal (do-it "%:e") "el"))
- (should (equal (do-it "%:p") (expand-file-name buffer-file-name)))
- (should (equal (do-it "%:h") "feature"))
- (should (equal (do-it "%:t") "test-evil.el"))
- (should (equal (do-it "%:.") "feature/test-evil.el"))
- (should (equal (do-it "%:~") "~/.emacs.d/test/modules/feature/test-evil.el"))
- (should (equal (file-truename (do-it "%:p"))
- (file-truename buffer-file-name))))
- ;; nested file modifiers
- (let ((buffer-file-name "~/vim/src/version.c")
- (default-directory "~/vim/"))
- (should (equal (do-it "%:p") (expand-file-name "~/vim/src/version.c")))
- (should (equal (do-it "%:p:.") "src/version.c"))
- (should (equal (do-it "%:p:~") "~/vim/src/version.c"))
- (should (equal (do-it "%:h") "src"))
- (should (equal (do-it "%:p:h") (expand-file-name "~/vim/src")))
- (should (equal (do-it "%:p:h:h") (expand-file-name "~/vim")))
- (should (equal (do-it "%:t") "version.c"))
- (should (equal (do-it "%:p:t") "version.c"))
- (should (equal (do-it "%:r") "src/version"))
- (should (equal (do-it "%:p:r") (expand-file-name "~/vim/src/version")))
- (should (equal (do-it "%:t:r") "version")))
- ;; empty file modifiers
- (let (buffer-file-name default-directory)
- (should (equal (do-it "%") ""))
- (should (equal (do-it "%:r") ""))
- (should (equal (do-it "%:e") ""))
- (should (equal (do-it "%:h") ""))
- (should (equal (do-it "%:t") ""))
- (should (equal (do-it "%:.") ""))
- (should (equal (do-it "%:~") ""))
- (should (equal (do-it "%:P") "")))))
-
-
-;; --- Macros -----------------------------
-
-;; `add-hook!'
-(def-test! add-one-to-one-hook
- (let (hooks)
- (add-hook! 'hooks 'a-hook)
- (should (equal hooks '(a-hook)))))
-
-(def-test! add-many-to-one-hook
- (let (hooks)
- (add-hook! 'hooks '(hook-a hook-b hook-c))
- (should (equal hooks '(hook-c hook-b hook-a)))))
-
-(def-test! add-one-to-many-hooks
- (let (hooks-a hooks-b hooks-c)
- (add-hook! '(hooks-a hooks-b hooks-c) 'a-hook)
- (should (equal hooks-a '(a-hook)))
- (should (equal hooks-b '(a-hook)))
- (should (equal hooks-c '(a-hook)))))
-
-(def-test! add-many-to-many-hooks
- (let (hooks-a hooks-b hooks-c)
- (add-hook! '(hooks-a hooks-b hooks-c) '(hook-a hook-b hook-c))
- (should (equal hooks-a '(hook-c hook-b hook-a)))
- (should (equal hooks-b '(hook-c hook-b hook-a)))
- (should (equal hooks-c '(hook-c hook-b hook-a)))))
-
-(def-test! add-non-literal-hooks
- (let (some-mode-hook)
- (add-hook! some-mode 'a-hook)
- (should (equal some-mode-hook '(a-hook)))))
-
-;; `remove-hook!'
-(def-test! remove-hooks
- (let ((hooks-a '(hook-c hook-b hook-a))
- (hooks-b '(hook-c hook-b hook-a))
- (hooks-c '(hook-c hook-b hook-a)))
- (remove-hook! '(hooks-a hooks-b hooks-c) '(hook-a hook-b hook-c))
- (should (null hooks-a))
- (should (null hooks-b))
- (should (null hooks-c))))
-
-(def-test! remove-hook-forms
- (let (hooks)
- (add-hook! 'hooks (message "Hello world"))
- (should hooks)
- (remove-hook! 'hooks (message "Hello world"))
- (should (null hooks))))
-
-;; `add-transient-hook!'
-(def-test! transient-hooks
- (let (hooks value)
- (add-transient-hook! 'hooks (setq value t))
- (run-hooks 'hooks)
- (should (eq value t))
- (should (null hooks))))
-
-(def-test! transient-function
- (let (value)
- (add-transient-hook! #'ignore (setq value (not value)))
- (ignore t)
- (should (eq value t))
- ;; repeat to ensure it was only run once
- (ignore t)
- (should (eq value t))))
-
-
-;; TODO `associate!'
-
-
-;; --- Settings ---------------------------
-
-(def-test! set
- (eval-and-compile
- (let (doom-settings)
- (def-setting! :-test-setting (x) `(setq result ,x))
- (should (assq :-test-setting doom-settings))
- (let ((inhibit-message t)
- result)
- (set! :-test-setting t)
- (should result)
- (set! :non-existant-setting (error "This shouldn't trigger"))))))
diff --git a/core/test/core-projects.el b/core/test/core-projects.el
deleted file mode 100644
index 3ebf2bc9b..000000000
--- a/core/test/core-projects.el
+++ /dev/null
@@ -1,43 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; ../core/test/core-projects.el
-
-(require 'projectile)
-
-;;
-;; `doom-project-p'
-(def-test! project-p
- :minor-mode projectile-mode
- (let ((default-directory doom-emacs-dir))
- (should (doom-project-p)))
- (let ((default-directory (expand-file-name "~")))
- (should-not (doom-project-p))))
-
-;; `doom-project-p'
-(def-test! project-root
- :minor-mode projectile-mode
- ;; Should resolve to project root
- (let ((default-directory doom-core-dir))
- (should (equal (doom-project-root) doom-emacs-dir)))
- ;; Should resolve to `default-directory' if not a project
- (let ((default-directory (expand-file-name "~")))
- (should (equal (doom-project-root) default-directory))))
-
-;; `doom-project-expand'
-(def-test! project-expand
- :minor-mode projectile-mode
- (let ((default-directory doom-core-dir))
- (should (equal (doom-project-expand "init.el")
- (expand-file-name "init.el" (doom-project-root))))))
-
-;; `doom-project-has!'
-(def-test! project-has!
- :minor-mode projectile-mode
- (let ((default-directory doom-core-dir))
- ;; Resolve from project root
- (should (doom-project-has! "init.el"))
- ;; Chained file checks
- (should (doom-project-has! (and "init.el" "LICENSE")))
- (should (doom-project-has! (or "init.el" "does-not-exist")))
- (should (doom-project-has! (and "init.el" (or "LICENSE" "does-not-exist"))))
- ;; Should resolve relative paths from `default-directory'
- (should (doom-project-has! (and "./core.el" "../init.el")))))
diff --git a/core/test/core-ui.el b/core/test/core-ui.el
deleted file mode 100644
index c1c82a01d..000000000
--- a/core/test/core-ui.el
+++ /dev/null
@@ -1,45 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; ../core/test/core-ui.el
-
-(defmacro with-temp-windows!! (&rest body)
- (declare (indent defun))
- `(progn
- (delete-other-windows)
- (cl-flet ((split-window (symbol-function #'split-window-horizontally)))
- (let ((a (get-buffer-create "a"))
- (b (get-buffer-create "b"))
- (split-width-threshold 0)
- (window-min-width 0))
- ,@body))))
-
-;;
-(def-test! set-mode-name
- (let ((doom-major-mode-names '((text-mode . "abc")
- (lisp-mode . (lambda () "xyz"))
- (js-mode . t))))
- (text-mode)
- (should (equal mode-name "abc"))
- (lisp-mode)
- (should (equal mode-name "xyz"))
- (should-error (js-mode))))
-
-(def-test! protect-visible-buffers
- (with-temp-windows!!
- (let ((kill-buffer-query-functions '(doom|protect-visible-buffers)))
- (switch-to-buffer a) (split-window)
- (switch-to-buffer b) (split-window)
- (switch-to-buffer a)
- (should-not (kill-buffer))
- (select-window (get-buffer-window b))
- (should (kill-buffer)))))
-
-(def-test! *quit-window
- (with-temp-windows!!
- (let (kill-buffer-query-functions)
- (switch-to-buffer a) (split-window)
- (switch-to-buffer b)
- (save-window-excursion
- (quit-window t)
- (should (buffer-live-p b)))
- (quit-window)
- (should-not (buffer-live-p b)))))
diff --git a/core/test/test-autoload-buffers.el b/core/test/test-autoload-buffers.el
new file mode 100644
index 000000000..f80e2538c
--- /dev/null
+++ b/core/test/test-autoload-buffers.el
@@ -0,0 +1,115 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-autoload-buffers.el
+
+(require 'core-projects)
+(load! "autoload/buffers" doom-core-dir)
+
+;;
+(describe "core/autoload/buffers"
+ :var (a b c d)
+ (before-all
+ (spy-on 'buffer-list :and-call-fake
+ (lambda (&optional _)
+ (cl-remove-if-not #'buffer-live-p (list a b c d)))))
+ (before-each
+ (delete-other-windows)
+ (setq a (switch-to-buffer (get-buffer-create "a"))
+ b (get-buffer-create "b")
+ c (get-buffer-create "c")
+ d (get-buffer-create "d")))
+ (after-each
+ (kill-buffer a)
+ (kill-buffer b)
+ (kill-buffer c)
+ (kill-buffer d))
+
+ (describe "buffer-list"
+ (it "should only see four buffers"
+ (expect (doom-buffer-list) :to-have-same-items-as (list a b c d))))
+
+ (describe "project-buffer-list"
+ :var (projectile-projects-cache-time projectile-projects-cache)
+ (before-all (require 'projectile))
+ (after-all (unload-feature 'projectile t))
+
+ (before-each
+ (with-current-buffer a (setq default-directory doom-emacs-dir))
+ (with-current-buffer b (setq default-directory doom-emacs-dir))
+ (with-current-buffer c (setq default-directory "/tmp/"))
+ (with-current-buffer d (setq default-directory "~"))
+ (projectile-mode +1))
+ (after-each
+ (projectile-mode -1))
+
+ (it "returns buffers in the same project"
+ (with-current-buffer a
+ (expect (doom-project-buffer-list)
+ :to-have-same-items-as (list a b))))
+
+ (it "returns all buffers if not in a project"
+ (with-current-buffer c
+ (expect (doom-project-buffer-list)
+ :to-have-same-items-as (buffer-list)))))
+
+ (describe "fallback-buffer"
+ (it "returns a live buffer"
+ (expect (buffer-live-p (doom-fallback-buffer)))))
+
+ (describe "real buffers"
+ (before-each
+ (doom-set-buffer-real a t)
+ (with-current-buffer b (setq buffer-file-name "x"))
+ (with-current-buffer c (rename-buffer "*C*")))
+
+ (describe "real-buffer-p"
+ (it "returns t for buffers manually marked real"
+ (expect (doom-real-buffer-p a)))
+ (it "returns t for file-visiting buffers"
+ (expect (doom-real-buffer-p b)))
+ (it "returns nil for temporary buffers"
+ (expect (doom-real-buffer-p c) :to-be nil)
+ (expect (doom-real-buffer-p d) :to-be nil)))
+
+ (describe "real-buffer-list"
+ (it "returns only real buffers"
+ (expect (doom-real-buffer-list) :to-have-same-items-as (list a b)))))
+
+ (describe "buffer/window management"
+ (describe "buffer search methods"
+ (before-each
+ (with-current-buffer a (lisp-mode))
+ (with-current-buffer b (text-mode))
+ (with-current-buffer c (text-mode))
+ (split-window)
+ (switch-to-buffer b))
+
+ (it "can match buffers by regexp"
+ (expect (doom-matching-buffers "^[ac]$") :to-have-same-items-as (list a c)))
+ (it "can match buffers by major-mode"
+ (expect (doom-buffers-in-mode 'text-mode) :to-have-same-items-as (list b c)))
+ (it "can find all buried buffers"
+ (expect (doom-buried-buffers)
+ :to-have-same-items-as (list c d)))
+ (it "can find all visible buffers"
+ (expect (doom-visible-buffers)
+ :to-have-same-items-as (list a b)))
+ (it "can find all visible windows"
+ (expect (doom-visible-windows)
+ :to-have-same-items-as
+ (mapcar #'get-buffer-window (list a b)))))
+
+ (describe "kill-buffer-and-windows"
+ (before-each
+ (split-window) (switch-to-buffer b)
+ (split-window) (switch-to-buffer a))
+
+ (it "kills the selected buffers and all its windows"
+ (doom-kill-buffer-and-windows a)
+ (expect (buffer-live-p a) :to-be nil)
+ (expect (length (doom-visible-windows)) :to-be 1)))
+
+ ;; TODO
+ (describe "kill-all-buffers")
+ (describe "kill-other-buffers")
+ (describe "kill-matching-buffers")
+ (describe "cleanup-session")))
diff --git a/core/test/test-autoload-files.el b/core/test/test-autoload-files.el
new file mode 100644
index 000000000..4251e6f0d
--- /dev/null
+++ b/core/test/test-autoload-files.el
@@ -0,0 +1,54 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-autoload-files.el
+;;;
+(require 'core-projects)
+(require 'projectile)
+
+(describe "core/autoload/files"
+ :var (src dest projectile-projects-cache-time projectile-projects-cache)
+ (before-each
+ (setq src (make-temp-file "test-src")
+ existing (make-temp-file "test-existing")
+ dest (expand-file-name "test-dest" temporary-file-directory))
+ (quiet! (find-file-literally src))
+ (spy-on 'y-or-n-p :and-return-value nil)
+ (projectile-mode +1))
+
+ (after-each
+ (projectile-mode -1)
+ (switch-to-buffer (doom-fallback-buffer))
+ (ignore-errors (delete-file src))
+ (ignore-errors (delete-file existing))
+ (ignore-errors (delete-file dest)))
+
+ (describe "move-this-file"
+ (it "won't move to itself"
+ (expect (quiet! (doom/move-this-file src)) :to-throw))
+ (it "will move to another file"
+ (expect (quiet! (doom/move-this-file dest t)))
+ (expect (file-exists-p dest))
+ (expect (file-exists-p src) :to-be nil))
+ (it "will prompt if overwriting a file"
+ (quiet! (doom/move-this-file existing))
+ (expect 'y-or-n-p :to-have-been-called-times 1)
+ (expect (file-exists-p src))))
+
+ (describe "copy-this-file"
+ (it "refuses to copy to itself"
+ (expect (quiet! (doom/copy-this-file src)) :to-throw))
+ (it "copies to another file"
+ (expect (quiet! (doom/copy-this-file dest t)))
+ (expect (file-exists-p! src dest)))
+ (it "prompts if overwriting a file"
+ (quiet! (doom/copy-this-file existing))
+ (expect 'y-or-n-p :to-have-been-called-times 1)))
+
+ (describe "delete-this-file"
+ (it "fails gracefully on non-existent files"
+ (expect (quiet! (doom/delete-this-file dest)) :to-throw))
+ (it "deletes existing files"
+ (quiet! (doom/delete-this-file existing t))
+ (expect (file-exists-p existing) :to-be nil))
+ (it "prompts to delete any existing file"
+ (quiet! (doom/delete-this-file existing))
+ (expect 'y-or-n-p :to-have-been-called-times 1))))
diff --git a/core/test/test-autoload-help.el b/core/test/test-autoload-help.el
new file mode 100644
index 000000000..8df3a9a3a
--- /dev/null
+++ b/core/test/test-autoload-help.el
@@ -0,0 +1,10 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-autoload-help.el
+
+;; (load! "autoload/help" doom-core-dir)
+
+;;
+;; (describe "core/autoload/help"
+;; :var (a)
+;; (before-each (setq a (switch-to-buffer (get-buffer-create "a"))))
+;; (after-each (kill-buffer a)))
diff --git a/core/test/test-autoload-message.el b/core/test/test-autoload-message.el
new file mode 100644
index 000000000..30c20eaa9
--- /dev/null
+++ b/core/test/test-autoload-message.el
@@ -0,0 +1,37 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-autoload-message.el
+
+(describe "core/autoload/message"
+ (describe "format!"
+ :var (noninteractive)
+ (before-all (setq noninteractive t))
+
+ (it "should be a drop-in replacement for `format'"
+ (expect (format! "Hello %s" "World")
+ :to-equal "Hello World"))
+
+ (it "supports ansi coloring in noninteractive sessions"
+ (expect (format! (red "Hello %s") "World")
+ :to-equal "[31mHello World[0m"))
+
+ (it "supports faces in interactive sessions"
+ (let (noninteractive)
+ (expect (get-text-property 0 'face (format! (red "Hello %s") "World"))
+ :to-equal (list :foreground (face-foreground 'term-color-red)))))
+
+ (it "supports nested color specs"
+ (expect (format! (bold (red "Hello %s")) "World")
+ :to-equal (format "\e[%dm%s\e[0m" 1
+ (format "\e[%dm%s\e[0m" 31 "Hello World")))
+ (expect (format! (on-red (bold "Hello %s")) "World")
+ :to-equal (format "\e[%dm%s\e[0m" 41
+ (format "\e[%dm%s\e[0m" 1 "Hello World")))
+ (expect (format! (dark (white "Hello %s")) "World")
+ :to-equal (format "\e[%dm%s\e[0m" 2
+ (format "\e[%dm%s\e[0m" 37 "Hello World"))))
+
+ (it "supports dynamic color apply syntax"
+ (expect (format! (color 'red "Hello %s") "World")
+ :to-equal (format! (red "Hello %s") "World"))
+ (expect (format! (color (if nil 'red 'blue) "Hello %s") "World")
+ :to-equal (format! (blue "Hello %s") "World")))))
diff --git a/core/test/test-autoload-package.el b/core/test/test-autoload-package.el
new file mode 100644
index 000000000..ac5c81534
--- /dev/null
+++ b/core/test/test-autoload-package.el
@@ -0,0 +1,138 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-autoload-package.el
+
+(describe "core/autoload/packages"
+ :var (package-alist
+ package-archive-contents
+ package-selected-packages
+ doom-packages
+ quelpa-cache
+ quelpa-initialized-p
+ doom-packages-dir
+ doom-core-packages
+ package-user-dir
+ quelpa-dir
+ pkg)
+
+ (before-all
+ (fset 'pkg
+ (lambda (name version &optional reqs)
+ (package-desc-create
+ :name name :version version :reqs reqs
+ :dir (expand-file-name (format "%s/" name) package-user-dir))))
+ (require 'package)
+ (require 'quelpa)
+ (setq doom-packages-dir (expand-file-name "packages/" (file-name-directory load-file-name))
+ package-user-dir (expand-file-name "elpa" doom-packages-dir)
+ quelpa-dir (expand-file-name "quelpa" doom-packages-dir)
+ quelpa-initialized-p t
+ doom-core-packages nil)
+ (spy-on #'package--user-installed-p :and-call-fake (lambda (_p) t))
+ (spy-on #'doom-initialize-packages :and-call-fake (lambda (&optional _)))
+ (spy-on #'package-refresh-contents :and-call-fake (lambda (&optional _)))
+ (spy-on #'quelpa-checkout :and-call-fake
+ (lambda (rcp _dir)
+ (when (eq (car rcp) 'doom-quelpa-dummy)
+ "20170405.1234"))))
+
+ (after-all
+ (unload-feature 'package t)
+ (unload-feature 'quelpa t))
+
+ (before-each
+ (setq package-alist
+ `((doom-dummy ,(pkg 'doom-dummy '(20160405 1234)))
+ (doom-uptodate-dummy ,(pkg 'doom-uptodate-dummy '(20160605 1234)))
+ (doom-unwanted-dummy ,(pkg 'doom-unwanted-dummy '(20160605 1234)))
+ (doom-quelpa-dummy ,(pkg 'doom-quelpa-dummy '(20160405 1234)))
+ (doom-noquelpa-dummy ,(pkg 'doom-noquelpa-dummy '(20160405 1234))))
+ package-archive-contents
+ `((doom-dummy ,(pkg 'doom-dummy '(20170405 1234)))
+ (doom-uptodate-dummy ,(pkg 'doom-uptodate-dummy '(20160605 1234))))
+ doom-packages
+ '((doom-dummy)
+ (doom-uptodate-dummy)
+ (doom-missing-dummy)
+ (doom-noquelpa-dummy)
+ (doom-disabled-dummy :disable t)
+ (doom-private-dummy :modules ((:private)))
+ (doom-disabled-private-dummy :modules ((:private)) :disable t)
+ (doom-quelpa-dummy :recipe (doom-quelpa-dummy :fetcher github :repo "hlissner/does-not-exist")))
+ quelpa-cache
+ '((doom-quelpa-dummy :fetcher github :repo "hlissner/does-not-exist")
+ (doom-noquelpa-dummy :fetcher github :repo "hlissner/does-not-exist-3")
+ (doom-new-quelpa-dummy :fetcher github :repo "hlissner/does-not-exist-2"))
+ package-selected-packages (mapcar #'car doom-packages)))
+
+ (describe "package-backend"
+ (it "determines the correct backend of a package"
+ (expect (doom-package-backend 'doom-dummy) :to-be 'elpa)
+ (expect (doom-package-backend 'doom-quelpa-dummy) :to-be 'quelpa)
+ (expect (doom-package-backend 'org) :to-be 'emacs))
+ (it "errors out if package isn't installed"
+ (expect (doom-package-backend 'xyz) :to-throw)))
+
+ (describe "package-outdated-p (elpa)"
+ (it "detects outdated ELPA packages and returns both versions"
+ (expect (doom-package-outdated-p 'doom-dummy)
+ :to-equal '(doom-dummy (20160405 1234) (20170405 1234))))
+ (it "ignores up-to-date ELPA packages"
+ (expect (doom-package-outdated-p 'doom-uptodate-dummy) :to-be nil))
+
+ (it "detects outdated QUELPA packages and returns both versions"
+ (expect (doom-package-outdated-p 'doom-quelpa-dummy)
+ :to-equal '(doom-quelpa-dummy (20160405 1234) (20170405 1234))))
+ (it "ignores up-to-date QUELPA packages"
+ (expect (doom-package-outdated-p 'doom-uptodate-dummy) :to-be nil))
+
+ (it "returns nil if package isn't installed"
+ (expect (doom-package-outdated-p 'xyz) :to-be nil)))
+
+ (describe "get-packages"
+ (before-all
+ ;; In addition to `package-installed-p', `doom-package-installed-p' does
+ ;; file existence checks which won't work here, so we simplify it
+ (spy-on #'doom-package-installed-p :and-call-fake #'package-installed-p))
+
+ (it "returns all packages"
+ (expect (mapcar #'car (doom-find-packages))
+ :to-have-same-items-as
+ (mapcar #'car doom-packages)))
+ (it "returns only disabled packages"
+ (expect (mapcar #'car (doom-find-packages :disabled t))
+ :to-have-same-items-as
+ '(doom-disabled-dummy doom-disabled-private-dummy)))
+ (it "returns only non-disabled packages"
+ (expect (mapcar #'car (doom-find-packages :disabled nil))
+ :to-have-same-items-as
+ '(doom-dummy doom-uptodate-dummy doom-quelpa-dummy doom-missing-dummy doom-noquelpa-dummy doom-private-dummy)))
+ (it "returns only installed packages"
+ (expect (mapcar #'car (doom-find-packages :disabled nil :installed t))
+ :to-have-same-items-as
+ '(doom-dummy doom-uptodate-dummy doom-quelpa-dummy doom-noquelpa-dummy)))
+ (it "returns only non-installed packages"
+ (expect (mapcar #'car (doom-find-packages :disabled nil :installed nil))
+ :to-have-same-items-as
+ '(doom-missing-dummy doom-private-dummy)))
+ (it "returns only private packages"
+ (expect (mapcar #'car (doom-find-packages :private t))
+ :to-have-same-items-as
+ '(doom-private-dummy doom-disabled-private-dummy)))
+ (it "returns only disabled and private packages"
+ (expect (mapcar #'car (doom-find-packages :disabled t :private t))
+ :to-have-same-items-as
+ '(doom-disabled-private-dummy))))
+
+ (describe "get-orphaned-packages"
+ (it "returns orphaned packages"
+ (expect (doom-get-orphaned-packages) :to-contain 'doom-unwanted-dummy))
+ (it "returns packages that have changed backends"
+ (expect (doom-get-orphaned-packages) :to-contain 'doom-noquelpa-dummy)))
+
+ (describe "get-missing-packages"
+ (it "returns packages that haven't been installed"
+ (expect (mapcar #'car (doom-get-missing-packages))
+ :to-contain 'doom-missing-dummy))
+ (it "returns packages that have changed backends"
+ (expect (mapcar #'car (doom-get-missing-packages))
+ :to-contain 'doom-noquelpa-dummy))))
diff --git a/core/test/test-core-keybinds.el b/core/test/test-core-keybinds.el
new file mode 100644
index 000000000..2db47792e
--- /dev/null
+++ b/core/test/test-core-keybinds.el
@@ -0,0 +1,261 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-core-keybinds.el
+
+(require 'core-keybinds)
+
+(buttercup-define-matcher :to-expand-into (src result)
+ (let ((src (funcall src))
+ (result (funcall result)))
+ (or (equal (macroexpand-1 src) result)
+ (error "'%s' expanded into '%s' instead of '%s'"
+ src (macroexpand-1 src) result))))
+
+(describe "core/keybinds"
+ (describe "map!"
+ :var (doom--map-evil-p doom-map-states)
+ (before-each
+ (setq doom--map-evil-p t
+ doom-map-states '((:n . normal)
+ (:v . visual)
+ (:i . insert)
+ (:e . emacs)
+ (:o . operator)
+ (:m . motion)
+ (:r . replace))))
+
+ (describe "Single keybinds"
+ (it "binds a global key"
+ (expect '(map! "C-." #'a) :to-expand-into '(general-define-key "C-." #'a)))
+
+ (it "binds a key in one evil state"
+ (dolist (state doom-map-states)
+ (expect `(map! ,(car state) "C-." #'a)
+ :to-expand-into
+ `(general-define-key :states ',(cdr state) "C-." #'a))))
+
+ (it "binds a key in multiple evil states"
+ (expect `(map! :nvi "C-." #'a)
+ :to-expand-into
+ '(progn (general-define-key :states 'insert "C-." #'a)
+ (general-define-key :states 'visual "C-." #'a)
+ (general-define-key :states 'normal "C-." #'a))))
+
+ (it "binds evil keybinds together with global keybinds"
+ (expect '(map! :ng "C-." #'a)
+ :to-expand-into
+ '(progn
+ (general-define-key :states 'normal "C-." #'a)
+ (general-define-key "C-." #'a)))))
+
+ (describe "Multiple keybinds"
+ (it "binds global keys and preserves order"
+ (expect '(map! "C-." #'a "C-," #'b "C-/" #'c)
+ :to-expand-into
+ '(general-define-key "C-." #'a "C-," #'b "C-/" #'c)))
+
+ (it "binds multiple keybinds in an evil state and preserve order"
+ (dolist (state doom-map-states)
+ (expect `(map! ,(car state) "a" #'a
+ ,(car state) "b" #'b
+ ,(car state) "c" #'c)
+ :to-expand-into
+ `(general-define-key :states ',(cdr state)
+ "a" #'a
+ "b" #'b
+ "c" #'c))))
+
+ (it "binds multiple keybinds in different evil states"
+ (expect `(map! :n "a" #'a
+ :n "b" #'b
+ :n "e" #'e
+ :v "c" #'c
+ :i "d" #'d)
+ :to-expand-into
+ `(progn (general-define-key :states 'insert "d" #'d)
+ (general-define-key :states 'visual "c" #'c)
+ (general-define-key :states 'normal "a" #'a "b" #'b "e" #'e))))
+
+ (it "groups multi-state keybinds while preserving same-group key order"
+ (expect `(map! :n "a" #'a
+ :v "c" #'c
+ :n "b" #'b
+ :i "d" #'d
+ :n "e" #'e)
+ :to-expand-into
+ `(progn (general-define-key :states 'insert "d" #'d)
+ (general-define-key :states 'visual "c" #'c)
+ (general-define-key :states 'normal "a" #'a "b" #'b "e" #'e))))
+
+ (it "binds multiple keybinds in multiple evil states"
+ (expect `(map! :nvi "a" #'a
+ :nvi "b" #'b
+ :nvi "c" #'c)
+ :to-expand-into
+ '(progn (general-define-key :states 'insert "a" #'a "b" #'b "c" #'c)
+ (general-define-key :states 'visual "a" #'a "b" #'b "c" #'c)
+ (general-define-key :states 'normal "a" #'a "b" #'b "c" #'c)))))
+
+ (describe "Nested keybinds"
+ (it "binds global keys"
+ (expect '(map! "C-." #'a
+ ("C-a" #'b)
+ ("C-x" #'c))
+ :to-expand-into
+ '(progn (general-define-key "C-." #'a)
+ (general-define-key "C-a" #'b)
+ (general-define-key "C-x" #'c))))
+
+ (it "binds nested evil keybinds"
+ (expect '(map! :n "C-." #'a
+ (:n "C-a" #'b)
+ (:n "C-x" #'c))
+ :to-expand-into
+ '(progn (general-define-key :states 'normal "C-." #'a)
+ (general-define-key :states 'normal "C-a" #'b)
+ (general-define-key :states 'normal "C-x" #'c))))
+
+ (it "binds global keybinds in between evil keybinds"
+ (expect '(map! :n "a" #'a
+ "b" #'b
+ :n "c" #'c)
+ :to-expand-into
+ '(progn (general-define-key "b" #'b)
+ (general-define-key :states 'normal "a" #'a "c" #'c)))))
+
+ ;;
+ (describe "Properties"
+ (describe ":after"
+ (it "wraps `general-define-key' in a `after!' block"
+ (dolist (form '((map! :after helm "a" #'a "b" #'b)
+ (map! (:after helm "a" #'a "b" #'b))))
+ (expect form :to-expand-into '(after! helm (general-define-key "a" #'a "b" #'b))))
+ (expect '(map! "a" #'a (:after helm "b" #'b "c" #'c))
+ :to-expand-into
+ '(progn
+ (general-define-key "a" #'a)
+ (after! helm
+ (general-define-key "b" #'b "c" #'c))))
+ (expect '(map! (:after helm "b" #'b "c" #'c) "a" #'a)
+ :to-expand-into
+ '(progn
+ (after! helm
+ (general-define-key "b" #'b "c" #'c))
+ (general-define-key "a" #'a))))
+
+ (it "nests `after!' blocks"
+ (expect '(map! :after x "a" #'a
+ (:after y "b" #'b
+ (:after z "c" #'c)))
+ :to-expand-into
+ '(after! x
+ (progn
+ (general-define-key "a" #'a)
+ (after! y
+ (progn
+ (general-define-key "b" #'b)
+ (after! z
+ (general-define-key "c" #'c))))))))
+
+ (it "nests `after!' blocks in other nested blocks"
+ (expect '(map! :after x "a" #'a
+ (:when t "b" #'b
+ (:after z "c" #'c)))
+ :to-expand-into
+ '(after! x
+ (progn
+ (general-define-key "a" #'a)
+ (when t
+ (progn
+ (general-define-key "b" #'b)
+ (after! z (general-define-key "c" #'c)))))))))
+
+ (describe ":desc"
+ (it "add a :which-key property to a keybind's DEF"
+ (expect '(map! :desc "A" "a" #'a)
+ :to-expand-into
+ `(general-define-key "a" (list :def #'a :which-key "A")))))
+
+ (describe ":when/:unless"
+ (it "wraps keys in a conditional block"
+ (dolist (prop '(:when :unless))
+ (let ((prop-fn (intern (doom-keyword-name prop))))
+ (expect `(map! ,prop t "a" #'a "b" #'b)
+ :to-expand-into
+ `(,prop-fn t (general-define-key "a" #'a "b" #'b)))
+ (expect `(map! (,prop t "a" #'a "b" #'b))
+ :to-expand-into
+ `(,prop-fn t (general-define-key "a" #'a "b" #'b))))))
+
+ (it "nests conditional blocks"
+ (expect '(map! (:when t "a" #'a (:when t "b" #'b)))
+ :to-expand-into
+ '(when t
+ (progn (general-define-key "a" #'a)
+ (when t (general-define-key "b" #'b)))))))
+
+ (describe ":leader"
+ (it "uses leader definer"
+ (expect '(map! :leader "a" #'a "b" #'b)
+ :to-expand-into
+ '(doom--define-leader-key "a" #'a "b" #'b)))
+
+ (it "it persists for nested keys"
+ (expect '(map! :leader "a" #'a ("b" #'b))
+ :to-expand-into
+ '(progn (doom--define-leader-key "a" #'a)
+ (doom--define-leader-key "b" #'b)))))
+
+ (describe ":localleader"
+ (it "uses localleader definer"
+ (expect '(map! :localleader "a" #'a "b" #'b)
+ :to-expand-into
+ '(define-localleader-key! "a" #'a "b" #'b)))
+
+ (it "it persists for nested keys"
+ (expect '(map! :localleader "a" #'a ("b" #'b))
+ :to-expand-into
+ '(progn (define-localleader-key! "a" #'a)
+ (define-localleader-key! "b" #'b)))))
+
+ (describe ":map/:keymap"
+ (it "specifies a single keymap for keys"
+ (expect '(map! :map emacs-lisp-mode-map "a" #'a)
+ :to-expand-into
+ '(general-define-key :keymaps '(emacs-lisp-mode-map) "a" #'a)))
+
+ (it "specifies multiple keymap for keys"
+ (expect '(map! :map (lisp-mode-map emacs-lisp-mode-map) "a" #'a)
+ :to-expand-into
+ '(general-define-key :keymaps '(lisp-mode-map emacs-lisp-mode-map) "a" #'a))))
+
+ (describe ":mode"
+ (it "appends -map to MODE"
+ (expect '(map! :mode emacs-lisp-mode "a" #'a)
+ :to-expand-into
+ '(general-define-key :keymaps '(emacs-lisp-mode-map) "a" #'a))))
+
+ (describe ":prefix"
+ (it "specifies a prefix for all keys"
+ (expect '(map! :prefix "a" "x" #'x "y" #'y "z" #'z)
+ :to-expand-into
+ '(general-define-key :prefix "a" "x" #'x "y" #'y "z" #'z)))
+
+ (it "overwrites previous inline :prefix properties"
+ (expect '(map! :prefix "a" "x" #'x "y" #'y :prefix "b" "z" #'z)
+ :to-expand-into
+ '(progn (general-define-key :prefix "a" "x" #'x "y" #'y)
+ (general-define-key :prefix "b" "z" #'z))))
+
+ (it "accumulates keys when nested"
+ (expect '(map! (:prefix "a" "x" #'x (:prefix "b" "x" #'x)))
+ :to-expand-into
+ `(progn (general-define-key :prefix "a" "x" #'x)
+ (general-define-key :prefix (general--concat nil "a" "b")
+ "x" #'x)))))
+
+ (describe ":textobj"
+ (it "defines keys in evil-{inner,outer}-text-objects-map"
+ (expect '(map! :textobj "a" #'inner #'outer)
+ :to-expand-into
+ '(map! (:map evil-inner-text-objects-map "a" #'inner)
+ (:map evil-outer-text-objects-map "a" #'outer))))))))
diff --git a/core/test/test-core-lib.el b/core/test/test-core-lib.el
new file mode 100644
index 000000000..608fdb66e
--- /dev/null
+++ b/core/test/test-core-lib.el
@@ -0,0 +1,92 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-core-lib.el
+
+(require 'core-lib)
+
+(describe "core/lib"
+ ;; --- Helpers ----------------------------
+ (describe "doom-unquote"
+ (it "unquotes a quoted form"
+ (expect (doom-unquote '(quote hello)) :to-be 'hello))
+ (it "unquotes nested-quoted forms"
+ (expect (doom-unquote '(quote (quote (a b c)))) :to-equal '(a b c)))
+ (it "unquotes function-quoted forms"
+ (expect (doom-unquote '(function a)) :to-be 'a))
+ (it "does nothing to unquoted forms"
+ (expect (doom-unquote 'hello) :to-be 'hello)))
+
+ (describe "doom-enlist"
+ (it "creates a list out of non-lists"
+ (expect (doom-enlist 'a) :to-equal '(a)))
+ (it "does nothing to lists"
+ (expect (doom-enlist '(a)) :to-equal '(a))))
+
+
+ ;; --- Macros -----------------------------
+ (describe "hooks"
+ (describe "add-hook!"
+ :var (fake-mode-hook other-mode-hook some-mode-hook)
+ (before-each
+ (setq fake-mode-hook '(first-hook)
+ other-mode-hook nil
+ some-mode-hook '(first-hook second-hook)))
+
+ (it "adds one-to-one hook"
+ (add-hook! fake-mode #'hook-2)
+ (add-hook! 'fake-mode-hook #'hook-1)
+ (expect fake-mode-hook :to-equal '(hook-1 hook-2 first-hook)))
+
+ (it "adds many-to-one hook"
+ (add-hook! (fake-mode other-mode some-mode) #'hook-2)
+ (add-hook! '(fake-mode-hook other-mode-hook some-mode-hook) #'hook-1)
+ (add-hook! :append (fake-mode other-mode some-mode) #'last-hook)
+ (expect fake-mode-hook :to-equal '(hook-1 hook-2 first-hook last-hook))
+ (expect other-mode-hook :to-equal '(hook-1 hook-2 last-hook))
+ (expect some-mode-hook :to-equal '(hook-1 hook-2 first-hook second-hook last-hook)))
+
+ (it "adds many-to-many hooks and preserve provided order"
+ (add-hook! (fake-mode other-mode some-mode) #'(hook-3 hook-4))
+ (add-hook! '(fake-mode-hook other-mode-hook some-mode-hook) #'(hook-1 hook-2))
+ (add-hook! :append '(fake-mode-hook other-mode-hook some-mode-hook) #'(last-hook-1 last-hook-2))
+ (expect fake-mode-hook :to-equal '(hook-1 hook-2 hook-3 hook-4 first-hook last-hook-1 last-hook-2))
+ (expect other-mode-hook :to-equal '(hook-1 hook-2 hook-3 hook-4 last-hook-1 last-hook-2))
+ (expect some-mode-hook :to-equal '(hook-1 hook-2 hook-3 hook-4 first-hook second-hook last-hook-1 last-hook-2)))
+
+ (it "adds implicit lambda to one hook"
+ (add-hook! fake-mode (progn))
+ (add-hook! 'other-mode-hook (ignore))
+ (add-hook! :append 'some-mode-hook (ignore))
+ (expect (caar fake-mode-hook) :to-be 'lambda)
+ (expect (caar other-mode-hook) :to-be 'lambda)
+ (expect (caar (last other-mode-hook)) :to-be 'lambda)))
+
+ (describe "remove-hook!"
+ :var (fake-mode-hook)
+ (before-each
+ (setq fake-mode-hook '(first-hook second-hook third-hook fourth-hook)))
+ (it "removes one hook"
+ (remove-hook! fake-mode #'third-hook)
+ (remove-hook! 'fake-mode-hook #'second-hook)
+ (expect fake-mode-hook :to-equal '(first-hook fourth-hook)))
+ (it "removes multiple hooks"
+ (remove-hook! fake-mode #'(first-hook third-hook))
+ (remove-hook! 'fake-mode-hook #'(second-hook fourth-hook))
+ (expect fake-mode-hook :to-be nil))))
+
+ (describe "add-transient-hook!"
+ (it "adds a transient function to hooks"
+ (let (hooks value)
+ (add-transient-hook! 'hooks (setq value t))
+ (run-hooks 'hooks)
+ (expect value)
+ (expect hooks :to-be nil)))
+ (it "advises a function with a transient advisor"
+ (let (value)
+ (add-transient-hook! #'ignore (setq value (not value)))
+ (ignore t)
+ (expect value)
+ ;; repeat to ensure it was only run once
+ (ignore t)
+ (expect value))))
+
+ (xdescribe "associate!")) ; TODO
diff --git a/core/test/test-core-modules.el b/core/test/test-core-modules.el
new file mode 100644
index 000000000..a786ad2ea
--- /dev/null
+++ b/core/test/test-core-modules.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-core-modules.el
+
+;; (require 'core-modules)
+
+(describe "core-modules")
diff --git a/core/test/test-core-packages.el b/core/test/test-core-packages.el
new file mode 100644
index 000000000..ff3104b76
--- /dev/null
+++ b/core/test/test-core-packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-core-packages.el
+
+(describe "core-packages")
diff --git a/core/test/test-core-projects.el b/core/test/test-core-projects.el
new file mode 100644
index 000000000..3dbd953d5
--- /dev/null
+++ b/core/test/test-core-projects.el
@@ -0,0 +1,35 @@
+;; -*- no-byte-compile: t; -*-
+;;; ../core/test/test-core-projects.el
+
+(require 'core-projects)
+(require 'projectile)
+
+(describe "core/projects"
+ (before-each (projectile-mode +1))
+ (after-each (projectile-mode -1))
+
+ (describe "project-p"
+ (it "Should detect when in a valid project"
+ (expect (doom-project-p doom-emacs-dir)))
+ (it "Should detect when not in a valid project"
+ (expect (doom-project-p (expand-file-name "~")) :to-be nil)))
+
+ (describe "project-root"
+ (it "should resolve to the project's root"
+ (expect (doom-project-root doom-core-dir) :to-equal doom-emacs-dir))
+ (it "should return nil if not in a project"
+ (expect (doom-project-root (expand-file-name "~")) :to-be nil)))
+
+ (describe "project-expand"
+ (it "expands to a path relative to the project root"
+ (expect (doom-project-expand "init.el" doom-core-dir)
+ :to-equal (expand-file-name "init.el" (doom-project-root doom-core-dir)))))
+
+ (describe "project-file-exists-p!"
+ (let ((default-directory doom-core-dir))
+ ;; Resolve from project root
+ (expect (project-file-exists-p! "init.el"))
+ ;; Chained file checks
+ (expect (project-file-exists-p! (and "init.el" "LICENSE")))
+ (expect (project-file-exists-p! (or "init.el" "does-not-exist")))
+ (expect (project-file-exists-p! (and "init.el" (or "LICENSE" "does-not-exist")))))))
diff --git a/core/test/test-core-ui.el b/core/test/test-core-ui.el
new file mode 100644
index 000000000..0ace0bc73
--- /dev/null
+++ b/core/test/test-core-ui.el
@@ -0,0 +1,17 @@
+;; -*- no-byte-compile: t; -*-
+;;; ../core/test/test-core-ui.el
+
+(require 'core-ui)
+
+(describe "core/ui"
+ (describe "doom|protect-fallback-buffer"
+ :var (kill-buffer-query-functions a b)
+ (before-all
+ (setq kill-buffer-query-functions '(doom|protect-fallback-buffer)))
+
+ (it "should kill other buffers"
+ (expect (kill-buffer (get-buffer-create "a"))))
+
+ (it "shouldn't kill the fallback buffer"
+ (expect (not (kill-buffer (doom-fallback-buffer)))))))
+
diff --git a/core/test/test-core.el b/core/test/test-core.el
new file mode 100644
index 000000000..1a3fc80fc
--- /dev/null
+++ b/core/test/test-core.el
@@ -0,0 +1,117 @@
+;; -*- no-byte-compile: t; -*-
+;;; core/test/test-core.el
+
+(describe "core"
+ (xdescribe "initialize"
+ :var (doom-init-p doom-init-modules-p doom-private-dir)
+ (before-each
+ (setq doom-init-p nil
+ doom-init-modules-p nil
+ doom-private-dir doom-emacs-dir)
+
+ (spy-on 'require)
+ (spy-on 'load)
+ (spy-on 'doom-reload-doom-autoloads)
+ (spy-on 'doom-reload-package-autoloads)
+ (spy-on 'doom-initialize-autoloads)
+ (spy-on 'doom-ensure-core-directories)
+ (spy-on 'doom-ensure-core-packages)
+ (spy-on 'doom-ensure-packages-initialized)
+ (spy-on 'doom-ensure-same-emacs-version-p))
+
+ (describe "in interactive session"
+ :var (noninteractive)
+ (before-each (setq noninteractive t))
+
+ (it "initializes once, unless forced")
+ (it "does not initialize on consecutive invokations")
+ (it "loads all core libraries" )
+ (it "loads autoloads file" )
+ (it "does not load autoloads file if forced" )
+ (it "regenerates missing autoloads" ))
+
+ (describe "in non-interactive session"
+ :var (noninteractive)
+ (before-each (setq noninteractive nil))
+
+ (it "initializes once, unless forced")
+ (it "does not initialize on consecutive invokations")
+ (it "does not load all core libraries" )
+ (it "loads autoloads file" )
+ (it "does not load autoloads file if forced" )
+ (it "does not regenerate missing autoloads" )))
+
+ (xdescribe "initialize-packages"
+ (before-each (spy-on 'quelpa-setup-p))
+
+ (it "initializes package.el once, unless forced" )
+ (it "initializes quelpa once, unless forced" )
+ (it "initializes doom-packages once, unless forced" ))
+
+ (xdescribe "initialize-modules"
+ (it "loads private init.el once, unless forced" ))
+
+ (xdescribe "initialize-autoloads"
+ (it "loads autoloads file" )
+ (it "ignores autoloads file if cleared" ))
+
+ (describe "custom hooks"
+ (describe "switch hooks"
+ :var (before-hook after-hook a b)
+ (before-each
+ (setq a (switch-to-buffer (get-buffer-create "a"))
+ b (get-buffer-create "b"))
+ (spy-on 'hook)
+ (add-hook 'buffer-list-update-hook #'doom|run-switch-window-hooks)
+ (add-hook 'focus-in-hook #'doom|run-switch-frame-hooks)
+ (advice-add! '(switch-to-buffer display-buffer) :around #'doom*run-switch-buffer-hooks))
+ (after-each
+ (remove-hook 'buffer-list-update-hook #'doom|run-switch-window-hooks)
+ (remove-hook 'focus-in-hook #'doom|run-switch-frame-hooks)
+ (advice-remove! '(switch-to-buffer display-buffer) #'doom*run-switch-buffer-hooks)
+ (kill-buffer a)
+ (kill-buffer b))
+
+ (describe "switch-buffer"
+ :var (doom-switch-buffer-hook)
+ (before-each
+ (setq doom-switch-buffer-hook '(hook)))
+ (after-each
+ (setq doom-switch-buffer-hook nil))
+
+ (it "should trigger when switching buffers"
+ (switch-to-buffer b)
+ (switch-to-buffer a)
+ (switch-to-buffer b)
+ (expect 'hook :to-have-been-called-times 3))
+
+ (it "should trigger only once on the same buffer"
+ (switch-to-buffer b)
+ (switch-to-buffer b)
+ (switch-to-buffer a)
+ (expect 'hook :to-have-been-called-times 2)))
+
+
+ (describe "switch-window"
+ :var (doom-switch-window-hook x y)
+ (before-each
+ (delete-other-windows)
+ (setq x (get-buffer-window a)
+ y (save-selected-window (split-window)))
+ (with-selected-window y
+ (switch-to-buffer b))
+ (select-window x)
+ (spy-calls-reset 'hook)
+ (setq doom-switch-window-hook '(hook)))
+
+ (it "should trigger when switching windows"
+ (select-window y)
+ (select-window x)
+ (select-window y)
+ (expect 'hook :to-have-been-called-times 3))
+
+ (it "should trigger only once on the same window"
+ (select-window y)
+ (select-window y)
+ (select-window x)
+ (expect 'hook :to-have-been-called-times 2))))))
diff --git a/docs/ISSUE_TEMPLATE.md b/docs/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..a09ea6ab4
--- /dev/null
+++ b/docs/ISSUE_TEMPLATE.md
@@ -0,0 +1,30 @@
++ [ ] I've checked for duplicates of this issue.
++ [ ] `bin/doom clean` and `bin/doom refresh` (then restarting Emacs) did not
+ fix my issue.
++ [ ] I ran `make doctor` and it produced no leads.
++ [ ] My issue cannot be found [in the FAQ](/../../wiki/FAQ)
++ [ ] I filled out the four fields in the template below.
++ [ ] I have deleted this checklist and message.
+
+### Observed behavior
+
+(Describe what happened)
+
+### Expected behavior
+
+(Describe what you expected to happen)
+
+### Steps to reproduce
+
+1. Select these example steps,
+2. Delete them,
+3. And replace them with precise steps to reproduce your issue.
+4. Fill in "system information" below.
+
+### System information
+
+
+Click to expand
+
+Replace this line with the output of `M-x doom/info` OR `~/.emacs.d/bin/doom info`
+
diff --git a/.github/PULL_REQUEST_TEMPLATE b/docs/PULL_REQUEST_TEMPLATE.md
similarity index 65%
rename from .github/PULL_REQUEST_TEMPLATE
rename to docs/PULL_REQUEST_TEMPLATE.md
index c7cab1bfb..ee07642c4 100644
--- a/.github/PULL_REQUEST_TEMPLATE
+++ b/docs/PULL_REQUEST_TEMPLATE.md
@@ -1,5 +1,5 @@
Thank you for contributing to Doom!
-Before you submit this PR, please make sure your PR is targeted at develop, not
+Before you submit this PR, please make sure it is targeted at develop, not
master (unless this is a fix for a critical error). Then replace this message
with a description of your changes.
diff --git a/early-init.el b/early-init.el
new file mode 100644
index 000000000..8365ba3a8
--- /dev/null
+++ b/early-init.el
@@ -0,0 +1,20 @@
+;;; early-init.el -*- lexical-binding: t; -*-
+
+;; Emacs HEAD (27+) introduces early-init.el, which is run before init.el,
+;; before package and UI initialization happens.
+
+;; Defer garbage collection further back in the startup process
+(setq gc-cons-threshold 268435456)
+
+;; Package initialize occurs automatically, before `user-init-file' is
+;; loaded, but after `early-init-file'. Doom handles package
+;; initialization, so we must prevent Emacs from doing it early!
+(setq package-enable-at-startup nil)
+
+;; Prevent the glimpse of un-styled Emacs by setting these early.
+(add-to-list 'default-frame-alist '(tool-bar-lines . 0))
+(add-to-list 'default-frame-alist '(menu-bar-lines . 0))
+(add-to-list 'default-frame-alist '(vertical-scroll-bars))
+
+;; One less file to load at startup
+(setq site-run-file nil)
diff --git a/init.el b/init.el
new file mode 100644
index 000000000..3bab9f7e1
--- /dev/null
+++ b/init.el
@@ -0,0 +1,73 @@
+;;; init.el -*- lexical-binding: t; -*-
+;;
+;; Author: Henrik Lissner
+;; URL: https://github.com/hlissner/doom-emacs
+;;
+;; ================= =============== =============== ======== ========
+;; \\ . . . . . . .\\ //. . . . . . .\\ //. . . . . . .\\ \\. . .\\// . . //
+;; ||. . ._____. . .|| ||. . ._____. . .|| ||. . ._____. . .|| || . . .\/ . . .||
+;; || . .|| ||. . || || . .|| ||. . || || . .|| ||. . || ||. . . . . . . ||
+;; ||. . || || . .|| ||. . || || . .|| ||. . || || . .|| || . | . . . . .||
+;; || . .|| ||. _-|| ||-_ .|| ||. . || || . .|| ||. _-|| ||-_.|\ . . . . ||
+;; ||. . || ||-' || || `-|| || . .|| ||. . || ||-' || || `|\_ . .|. .||
+;; || . _|| || || || || ||_ . || || . _|| || || || |\ `-_/| . ||
+;; ||_-' || .|/ || || \|. || `-_|| ||_-' || .|/ || || | \ / |-_.||
+;; || ||_-' || || `-_|| || || ||_-' || || | \ / | `||
+;; || `' || || `' || || `' || || | \ / | ||
+;; || .===' `===. .==='.`===. .===' /==. | \/ | ||
+;; || .==' \_|-_ `===. .===' _|_ `===. .===' _-|/ `== \/ | ||
+;; || .==' _-' `-_ `=' _-' `-_ `=' _-' `-_ /| \/ | ||
+;; || .==' _-' '-__\._-' '-_./__-' `' |. /| | ||
+;; ||.==' _-' `' | /==.||
+;; ==' _-' \/ `==
+;; \ _-' `-_ /
+;; `'' ``'
+;;
+;; These demons are not part of GNU Emacs.
+;;
+;;; License: MIT
+
+(defvar doom-gc-cons-threshold 16777216 ; 16mb
+ "The default value to use for `gc-cons-threshold'. If you experience freezing,
+decrease this. If you experience stuttering, increase this.")
+
+(defvar doom-gc-cons-upper-limit 268435456 ; 256mb
+ "The temporary value for `gc-cons-threshold' to defer it.")
+
+
+(defvar doom--file-name-handler-alist file-name-handler-alist)
+
+(defun doom|restore-startup-optimizations ()
+ "Resets garbage collection settings to reasonable defaults (a large
+`gc-cons-threshold' can cause random freezes otherwise) and resets
+`file-name-handler-alist'."
+ (setq file-name-handler-alist doom--file-name-handler-alist)
+ ;; Do this on idle timer to defer a possible GC pause that could result; also
+ ;; allows deferred packages to take advantage of these optimizations.
+ (run-with-idle-timer
+ 3 nil (lambda () (setq-default gc-cons-threshold doom-gc-cons-threshold))))
+
+
+(if (or after-init-time noninteractive)
+ (setq gc-cons-threshold doom-gc-cons-threshold)
+ ;; A big contributor to startup times is garbage collection. We up the gc
+ ;; threshold to temporarily prevent it from running, then reset it later in
+ ;; `doom|restore-startup-optimizations'.
+ (setq gc-cons-threshold doom-gc-cons-upper-limit)
+ ;; This is consulted on every `require', `load' and various path/io functions.
+ ;; You get a minor speed up by nooping this.
+ (setq file-name-handler-alist nil)
+ ;; Not restoring these to their defaults will cause stuttering/freezes.
+ (add-hook 'after-init-hook #'doom|restore-startup-optimizations))
+
+
+;; Ensure Doom is running out of this file's directory
+(setq user-emacs-directory (file-name-directory load-file-name))
+;; In noninteractive sessions, prioritize non-byte-compiled source files to
+;; prevent stale, byte-compiled code from running. However, if you're getting
+;; recursive load errors, it may help to set this to nil.
+(setq load-prefer-newer noninteractive)
+
+
+;; Let 'er rip!
+(require 'core (concat user-emacs-directory "core/core"))
diff --git a/init.example.el b/init.example.el
index 3a7a691a7..6117947ba 100644
--- a/init.example.el
+++ b/init.example.el
@@ -1,139 +1,172 @@
;;; init.el -*- lexical-binding: t; -*-
-;;
-;; Author: Henrik Lissner
-;; URL: https://github.com/hlissner/.emacs.d
-;;
-;; ================= =============== =============== ======== ========
-;; \\ . . . . . . .\\ //. . . . . . .\\ //. . . . . . .\\ \\. . .\\// . . //
-;; ||. . ._____. . .|| ||. . ._____. . .|| ||. . ._____. . .|| || . . .\/ . . .||
-;; || . .|| ||. . || || . .|| ||. . || || . .|| ||. . || ||. . . . . . . ||
-;; ||. . || || . .|| ||. . || || . .|| ||. . || || . .|| || . | . . . . .||
-;; || . .|| ||. _-|| ||-_ .|| ||. . || || . .|| ||. _-|| ||-_.|\ . . . . ||
-;; ||. . || ||-' || || `-|| || . .|| ||. . || ||-' || || `|\_ . .|. .||
-;; || . _|| || || || || ||_ . || || . _|| || || || |\ `-_/| . ||
-;; ||_-' || .|/ || || \|. || `-_|| ||_-' || .|/ || || | \ / |-_.||
-;; || ||_-' || || `-_|| || || ||_-' || || | \ / | `||
-;; || `' || || `' || || `' || || | \ / | ||
-;; || .===' `===. .==='.`===. .===' /==. | \/ | ||
-;; || .==' \_|-_ `===. .===' _|_ `===. .===' _-|/ `== \/ | ||
-;; || .==' _-' `-_ `=' _-' `-_ `=' _-' `-_ /| \/ | ||
-;; || .==' _-' '-__\._-' '-_./__-' `' |. /| | ||
-;; ||.==' _-' `' | /==.||
-;; ==' _-' \/ `==
-;; \ _-' `-_ /
-;; `'' ``'
-;;
-;; These demons are not part of GNU Emacs.
-;;
-;;; License: MIT
-(require 'core (concat user-emacs-directory "core/core"))
+;; Copy this file to ~/.doom.d/init.el or ~/.config/doom/init.el ('doom
+;; quickstart' will do this for you). The `doom!' block below controls what
+;; modules are enabled and in what order they will be loaded. Remember to run
+;; 'doom refresh' after modifying it.
+;;
+;; More information about these modules (and what flags they support) can be
+;; found in modules/README.org.
(doom! :feature
- ;debugger ; FIXME stepping through code, to help you add bugs
+ ;;debugger ; FIXME stepping through code, to help you add bugs
eval ; run code, run (also, repls)
- evil ; come to the dark side, we have cookies
+ (evil +everywhere); come to the dark side, we have cookies
file-templates ; auto-snippets for empty files
- jump ; helping you get around
- services ; TODO managing external services & code builders
+ (lookup ; helps you navigate your code and documentation
+ +docsets) ; ...or in Dash docsets locally
snippets ; my elves. They type so I don't have to
- spellcheck ; tasing you for misspelling mispelling
- syntax-checker ; tasing you for every semicolon you forget
- version-control ; remember, remember that commit in November
workspaces ; tab emulation, persistence & separate workspaces
:completion
company ; the ultimate code completion backend
- ivy ; a search engine for love and life
- ;helm ; the *other* search engine for love and life
- ;ido ; the other *other* search engine...
+ ;;helm ; the *other* search engine for love and life
+ ;;ido ; the other *other* search engine...
+ ivy ; a search engine for love and life
:ui
+ ;;deft ; notational velocity for Emacs
doom ; what makes DOOM look the way it does
doom-dashboard ; a nifty splash screen for Emacs
- doom-modeline ; a snazzy Atom-inspired mode-line
doom-quit ; DOOM quit-message prompts when you quit Emacs
+ ;;fill-column ; a `fill-column' indicator
hl-todo ; highlight TODO/FIXME/NOTE tags
+ ;;indent-guides ; highlighted indent columns
+ modeline ; snazzy, Atom-inspired modeline, plus API
nav-flash ; blink the current line after jumping
- evil-goggles ; display visual hints when editing in evil
- ;unicode ; extended unicode support for various languages
- ;tabbar ; FIXME an (incomplete) tab bar for Emacs
+ ;;neotree ; a project drawer, like NERDTree for vim
+ ophints ; highlight the region an operation acts on
+ (popup ; tame sudden yet inevitable temporary windows
+ +all ; catch all popups that start with an asterix
+ +defaults) ; default popup rules
+ ;;pretty-code ; replace bits of code with pretty symbols
+ ;;tabbar ; FIXME an (incomplete) tab bar for Emacs
+ treemacs ; a project drawer, like neotree but cooler
+ ;;unicode ; extended unicode support for various languages
+ vc-gutter ; vcs diff in the fringe
vi-tilde-fringe ; fringe tildes to mark beyond EOB
- (window-select +ace-window) ; visually switch windows
+ window-select ; visually switch windows
+
+ :editor
+ fold ; (nigh) universal code folding
+ ;;(format +onsave) ; automated prettiness
+ ;;lispy ; vim for lisp, for people who dont like vim
+ multiple-cursors ; editing in many places at once
+ ;;parinfer ; turn lisp into python, sort of
+ rotate-text ; cycle region at point between text candidates
+
+ :emacs
+ (dired ; making dired pretty [functional]
+ ;;+ranger ; bringing the goodness of ranger to dired
+ ;;+icons ; colorful icons for dired-mode
+ )
+ electric ; smarter, keyword-based electric-indent
+ ;;eshell ; a consistent, cross-platform shell (WIP)
+ imenu ; an imenu sidebar and searchable code index
+ ;;term ; terminals in Emacs
+ vc ; version-control and Emacs, sitting in a tree
:tools
- dired ; making dired pretty [functional]
- electric-indent ; smarter, keyword-based electric-indent
- eshell ; a consistent, cross-platform shell (WIP)
- gist ; interacting with github gists
- imenu ; an imenu sidebar and searchable code index
- impatient-mode ; show off code over HTTP
- ;macos ; MacOS-specific commands
- make ; run make tasks from Emacs
- neotree ; a project drawer, like NERDTree for vim
- password-store ; password manager for nerds
- rotate-text ; cycle region at point between text candidates
- term ; terminals in Emacs
- tmux ; an API for interacting with tmux
- upload ; map local to remote projects via ssh/ftp
+ ;;ansible
+ ;;direnv
+ ;;docker
+ ;;editorconfig ; let someone else argue about tabs vs spaces
+ ;;ein ; tame Jupyter notebooks with emacs
+ flycheck ; tasing you for every semicolon you forget
+ ;;flyspell ; tasing you for misspelling mispelling
+ ;;gist ; interacting with github gists
+ ;;lsp
+ ;;macos ; MacOS-specific commands
+ magit ; a git porcelain for Emacs
+ ;;make ; run make tasks from Emacs
+ ;;password-store ; password manager for nerds
+ ;;pdf ; pdf enhancements
+ ;;prodigy ; FIXME managing external services & code builders
+ ;;rgb ; creating color strings
+ ;;terraform ; infrastructure as code
+ ;;tmux ; an API for interacting with tmux
+ ;;upload ; map local to remote projects via ssh/ftp
+ ;;wakatime
+ ;;vterm ; another terminals in Emacs
:lang
- assembly ; assembly for fun or debugging
- cc ; C/C++/Obj-C madness
- crystal ; ruby at the speed of c
- clojure ; java with a lisp
- csharp ; unity, .NET, and mono shenanigans
+ ;;agda ; types of types of types of types...
+ ;;assembly ; assembly for fun or debugging
+ ;;(cc +irony +rtags); C/C++/Obj-C madness
+ ;;clojure ; java with a lisp
+ ;;common-lisp ; if you've seen one lisp, you've seen them all
+ ;;coq ; proofs-as-programs
+ ;;crystal ; ruby at the speed of c
+ ;;csharp ; unity, .NET, and mono shenanigans
data ; config/data formats
- elixir ; erlang done right
- elm ; care for a cup of TEA?
+ ;;erlang ; an elegant language for a more civilized age
+ ;;elixir ; erlang done right
+ ;;elm ; care for a cup of TEA?
emacs-lisp ; drown in parentheses
- go ; the hipster dialect
- (haskell +intero) ; a language that's lazier than I am
- hy ; readability of scheme w/ speed of python
- (java +meghanada) ; the poster child for carpal tunnel syndrome
- javascript ; all(hope(abandon(ye(who(enter(here))))))
- julia ; a better, faster MATLAB
- latex ; writing papers in Emacs has never been so fun
- ledger ; an accounting system in Emacs
- lua ; one-based indices? one-based indices
+ ;;ess ; emacs speaks statistics
+ ;;go ; the hipster dialect
+ ;;(haskell +intero) ; a language that's lazier than I am
+ ;;hy ; readability of scheme w/ speed of python
+ ;;idris ;
+ ;;(java +meghanada) ; the poster child for carpal tunnel syndrome
+ ;;javascript ; all(hope(abandon(ye(who(enter(here))))))
+ ;;julia ; a better, faster MATLAB
+ ;;kotlin ; a better, slicker Java(Script)
+ ;;latex ; writing papers in Emacs has never been so fun
+ ;;ledger ; an accounting system in Emacs
+ ;;lua ; one-based indices? one-based indices
markdown ; writing docs for people to ignore
- ocaml ; an objective camel
+ ;;nim ; python + lisp at the speed of c
+ ;;nix ; I hereby declare "nix geht mehr!"
+ ;;ocaml ; an objective camel
(org ; organize your plain life in plain text
+attach ; custom attachment system
+babel ; running code in org
+capture ; org-capture in and outside of Emacs
- +export ; centralized export system + more backends
+ +export ; Exporting org to whatever you want
+ +habit ; Keep track of your habits
+present ; Emacs for presentations
- ;; TODO +publish
- )
- perl ; write code no one else can comprehend
- php ; make php less awful to work with
- plantuml ; diagrams for confusing people more
- purescript ; javascript, but functional
- python ; beautiful is better than ugly
- rest ; Emacs as a REST client
- ruby ; 1.step do {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
- rust ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
- scala ; java, but good
- sh ; she sells (ba|z)sh shells on the C xor
- swift ; who asked for emoji variables?
- typescript ; javascript, but better
- web ; the tubes
+ +protocol) ; Support for org-protocol:// links
+ ;;perl ; write code no one else can comprehend
+ ;;php ; perl's insecure younger brother
+ ;;plantuml ; diagrams for confusing people more
+ ;;purescript ; javascript, but functional
+ ;;python ; beautiful is better than ugly
+ ;;qt ; the 'cutest' gui framework ever
+ ;;racket ; a DSL for DSLs
+ ;;rest ; Emacs as a REST client
+ ;;ruby ; 1.step do {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
+ ;;rust ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
+ ;;scala ; java, but good
+ (sh +fish) ; she sells (ba|z|fi)sh shells on the C xor
+ ;;solidity ; do you need a blockchain? No.
+ ;;swift ; who asked for emoji variables?
+ ;;terra ; Earth and Moon in alignment for performance.
+ ;;web ; the tubes
+ ;;vala ; GObjective-C
;; Applications are complex and opinionated modules that transform Emacs
;; toward a specific purpose. They may have additional dependencies and
;; should be loaded late.
:app
- ;email ; emacs as an email client
- ;irc ; how neckbeards socialize
- ;rss ; emacs as an RSS reader
- ;twitter ; twitter client https://twitter.com/vnought
- ;write ; emacs as a word processor (latex + org + markdown)
+ ;;(email +gmail) ; emacs as an email client
+ ;;irc ; how neckbeards socialize
+ ;;(rss +org) ; emacs as an RSS reader
+ ;;twitter ; twitter client https://twitter.com/vnought
+ ;;(write ; emacs as a word processor (latex + org + markdown)
+ ;; +wordnut ; wordnet (wn) search
+ ;; +langtool) ; a proofreader (grammar/style check) for Emacs
- ;; Private modules are where you place your personal configuration files.
- ;; By default, they are not tracked. There is one module included here,
- ;; the defaults module. It contains a Spacemacs-inspired keybinding
- ;; scheme and additional ex commands for evil-mode. Use it as a reference
- ;; for your own.
- :private default)
+ :collab
+ ;;floobits ; peer programming for a price
+ ;;impatient-mode ; show off code over HTTP
+ :config
+ ;; For literate config users. This will tangle+compile a config.org
+ ;; literate config in your `doom-private-dir' whenever it changes.
+ ;;literate
+
+ ;; The default module sets reasonable defaults for Emacs. It also
+ ;; provides a Spacemacs-inspired keybinding scheme and a smartparens
+ ;; config. Use it as a reference for your own modules.
+ (default +bindings +smartparens))
diff --git a/init.test.el b/init.test.el
index 8d529cd2f..1d150befc 100644
--- a/init.test.el
+++ b/init.test.el
@@ -1,23 +1,15 @@
;;; init.test.el -- for automated unit tests -*- lexical-binding: t; -*-
-(require 'core (concat user-emacs-directory "core/core"))
-
(doom! :feature
evil
workspaces
-
:completion
company
-
:ui
doom-dashboard
-
+ popup
:tools
password-store
-
:lang
org
- web
-
- :private
- hlissner)
+ web)
diff --git a/modules/README.org b/modules/README.org
new file mode 100644
index 000000000..5f404b293
--- /dev/null
+++ b/modules/README.org
@@ -0,0 +1,174 @@
+#+TITLE: Doom Modules
+
+* Table of Contents :TOC:noexport:
+- [[#feature][:feature]]
+- [[#completion][:completion]]
+- [[#ui][:ui]]
+- [[#editor][:editor]]
+- [[#emacs][:emacs]]
+- [[#tools][:tools]]
+- [[#lang][:lang]]
+- [[#app][:app]]
+- [[#collab][:collab]]
+- [[#config][:config]]
+
+* :feature
+Broad modules that bring essential IDE functionality to Emacs.
+
++ debugger: A (nigh-)universal debugger in Emacs
++ [[file:feature/eval/README.org][eval]]: REPL & code evaluation support for a variety of languages
++ [[file:feature/evil/README.org][evil]] =+everywhere=: Vim in Emacs
++ [[file:feature/file-templates/README.org][file-templates]]: Auto-inserted templates in blank new files
++ [[file:feature/lookup/README.org][lookup]] =+docsets=: Universal jump-to & documentation lookup backend
++ [[file:feature/snippets/README.org][snippets]]: A templating system for Emacs for lazy typers (aka programmers)
++ [[file:feature/workspaces/README.org][workspaces]]: Isolated workspaces
+
+* :completion
+Swappable completion modules for quickly narrowing down lists of candidates.
+
++ [[file:completion/company/README.org][company]] =+auto +childframe=: The ultimate code completion backend
++ helm =+fuzzy +childframe=: *Another* search engine for love and life
++ ido: The /other/ *other* search engine for love and life
++ [[file:completion/ivy/README.org][ivy]] =+fuzzy +childframe=: /The/ search engine for love and life
+
+* :ui
+Aesthetic modules that affect the Emacs interface or user experience.
+
++ [[file:ui/deft/README.org][deft]]:
++ [[file:ui/doom/README.org][doom]]:
++ [[file:ui/doom-dashboard/README.org][doom-dashboard]]:
++ [[file:ui/doom-quit/README.org][doom-quit]]:
++ fill-column:
++ [[file:ui/hl-todo/README.org][hl-todo]]:
++ indent-guides:
++ [[file:ui/modeline/README.org][modeline]]:
++ [[file:ui/nav-flash/README.org][nav-flash]]:
++ [[file:ui/neotree/README.org][neotree]]:
++ [[file:ui/ophints/README.org][ophints]]:
++ [[file:ui/popup/README.org][popup]] =+all +defaults=: Makes temporary/disposable windows less intrusive
++ pretty-code:
++ [[file:ui/tabbar/README.org][tabbar]]:
++ treemacs:
++ [[file:ui/unicode/README.org][unicode]]:
++ vc-gutter:
++ vi-tilde-fringe:
++ [[file:ui/window-select/README.org][window-select]]:
+
+* :editor
+Modules that affect and augment your ability to write and edit text.
+
++ [[file:editor/fold/README.org][fold]]: universal code folding
++ [[file:editor/format/README.org][format]] =+onsave=:
++ [[file:editor/lispy/README.org][lispy]]:
++ multiple-cursors:
++ [[file:editor/parinfer/README.org][parinfer]]:
++ rotate-text:
+
+* :emacs
+Modules that reconfigure packages or features built into Emacs
+
++ dired =+ranger +icons=:
++ electric:
++ eshell:
++ imenu:
++ term:
++ vc:
+
+* :tools
+Small modules that give Emacs access to external tools & services.
+
++ ansible:
++ docker:
++ [[file:tools/editorconfig/README.org][editorconfig]]:
++ [[file:tools/ein/README.org][ein]]:
++ flycheck: Live error/warning highlights
++ flyspell: Spell checking
++ gist:
++ [[file:tools/lsp/README.org][lsp]]:
++ macos:
++ magit:
++ make:
++ password-store:
++ pdf:
++ prodigy:
++ rgb:
++ terraform:
++ tmux:
++ upload:
++ [[file:tools/wakatime/README.org][wakatime]]:
++ vterm:
+
+* :lang
+Modules that bring support for a language or group of languages to Emacs.
+
++ agda:
++ assembly:
++ [[file:lang/cc/README.org][cc]] =+lsp=:
++ clojure:
++ common-lisp:
++ [[file:lang/coq/README.org][coq]]:
++ crystal:
++ [[file:lang/csharp/README.org][csharp]]:
++ data:
++ erlang:
++ elixir:
++ elm:
++ emacs-lisp:
++ [[file:lang/ess/README.org][ess]]:
++ [[file:lang/go/README.org][go]] =+lsp=:
++ [[file:lang/haskell/README.org][haskell]] =+intero +dante=:
++ hy:
++ [[file:lang/idris/README.org][idris]]:
++ java =+meghanada=:
++ [[file:lang/javascript/README.org][javascript]] =+lsp=:
++ julia:
++ kotlin:
++ [[file:lang/latex/README.org][latex]]:
++ ledger:
++ lua:
++ markdown:
++ [[file:lang/nim/README.org][nim]]:
++ nix:
++ [[file:lang/ocaml/README.org][ocaml]] =+lsp=:
++ [[file:lang/org/README.org][org]] =+attach +babel +capture +export +present +ipython=:
++ [[file:lang/perl/README.org][perl]]:
++ [[file:lang/php/README.org][php]] =+lsp=:
++ plantuml:
++ purescript:
++ python =+lsp=:
++ qt:
++ racket:
++ [[file:lang/rest/README.org][rest]]:
++ ruby =+lsp=:
++ [[file:lang/rust/README.org][rust]] =+lsp=:
++ scala:
++ [[file:lang/sh/README.org][sh]] =+fish +lsp=:
++ [[file:lang/solidity/README.org][solidity]]:
++ swift:
++ terra:
++ web =+lsp=:
++ vala:
+
+* :app
+Large, opinionated modules that transform and take over Emacs, i.e.
+Doom-specific porcelains.
+
++ calendar:
++ [[file:app/email/README.org][email]] =+gmail=:
++ [[file:app/irc/README.org][irc]]:
++ rss =+org=:
++ twitter:
++ [[file:app/write/README.org][write]] =+wordnut +langtool=:
+
+* :collab
+Modules that enable collaborative programming over the internet.
+
++ floobits:
++ impatient-mode:
+
+* :config
+Modules that configure Emacs one way or another, or focus on making it easier
+for you to customize it yourself.
+
++ literate:
++ [[file:config/default/README.org][default]] =+bindings +smartparens=:
diff --git a/modules/app/calendar/autoload.el b/modules/app/calendar/autoload.el
new file mode 100644
index 000000000..3aa68053d
--- /dev/null
+++ b/modules/app/calendar/autoload.el
@@ -0,0 +1,61 @@
+;;; app/calendar/autoload.el -*- lexical-binding: t; -*-
+
+(defvar +calendar--wconf nil)
+
+(defun +calendar--init ()
+ (if-let* ((win (cl-loop for win in (doom-visible-windows)
+ if (string-match-p "^\\*cfw:" (buffer-name (window-buffer win)))
+ return win)))
+ (select-window win)
+ (call-interactively +calendar-open-function)))
+
+;;;###autoload
+(defun =calendar ()
+ "Activate (or switch to) `calendar' in its workspace."
+ (interactive)
+ (if (featurep! :feature workspaces)
+ (progn
+ (+workspace-switch "Calendar" t)
+ (doom/switch-to-scratch-buffer)
+ (+calendar--init)
+ (+workspace/display))
+ (setq +calendar--wconf (current-window-configuration))
+ (delete-other-windows)
+ (switch-to-buffer (doom-fallback-buffer))
+ (+calendar--init)))
+
+;;;###autoload
+(defun +calendar/quit ()
+ "TODO"
+ (interactive)
+ (if (featurep! :feature workspaces)
+ (+workspace/delete "Calendar")
+ (doom-kill-matching-buffers "^\\*cfw:")
+ (set-window-configuration +calendar--wconf)
+ (setq +calendar--wconf nil)))
+
+;;;###autoload
+(defun +calendar/open-calendar ()
+ "TODO"
+ (interactive)
+ (cfw:open-calendar-buffer
+ ;; :custom-map cfw:my-cal-map
+ :contents-sources
+ (list
+ (cfw:org-create-source (doom-color 'fg)) ; orgmode source
+ )))
+
+;;;###autoload
+(defun +calendar*cfw:render-button (title command &optional state)
+ "render-button
+ TITLE
+ COMMAND
+ STATE"
+ (let ((text (concat " " title " "))
+ (keymap (make-sparse-keymap)))
+ (cfw:rt text (if state 'cfw:face-toolbar-button-on
+ 'cfw:face-toolbar-button-off))
+ (define-key keymap [mouse-1] command)
+ (cfw:tp text 'keymap keymap)
+ (cfw:tp text 'mouse-face 'highlight)
+ text))
diff --git a/modules/app/calendar/config.el b/modules/app/calendar/config.el
new file mode 100644
index 000000000..197ed9f55
--- /dev/null
+++ b/modules/app/calendar/config.el
@@ -0,0 +1,56 @@
+;;; app/calendar/config.el -*- lexical-binding: t; -*-
+
+(defvar +calendar-org-gcal-secret-file
+ (expand-file-name "private/org/secret.el" doom-modules-dir)
+ "TODO")
+
+(defvar +calendar-open-function #'+calendar/open-calendar
+ "TODO")
+
+
+;;
+;; Packages
+
+(def-package! calfw
+ :commands (cfw:open-calendar-buffer)
+ :config
+ ;; better frame for calendar
+ (setq cfw:face-item-separator-color nil
+ cfw:render-line-breaker 'cfw:render-line-breaker-none
+ cfw:fchar-junction ?╋
+ cfw:fchar-vertical-line ?┃
+ cfw:fchar-horizontal-line ?━
+ cfw:fchar-left-junction ?┣
+ cfw:fchar-right-junction ?┫
+ cfw:fchar-top-junction ?┯
+ cfw:fchar-top-left-corner ?┏
+ cfw:fchar-top-right-corner ?┓)
+
+ (define-key cfw:calendar-mode-map "q" #'+calendar/quit)
+
+ (add-hook 'cfw:calendar-mode-hook #'doom|mark-buffer-as-real)
+ (add-hook 'cfw:calendar-mode-hook 'hide-mode-line-mode)
+
+ (advice-add #'cfw:render-button :override #'+calendar*cfw:render-button))
+
+
+(def-package! calfw-org
+ :commands (cfw:open-org-calendar
+ cfw:org-create-source
+ cfw:open-org-calendar-withkevin
+ my-open-calendar))
+
+
+(def-package! org-gcal
+ :commands (org-gcal-sync
+ org-gcal-fetch
+ org-gcal-post-at-point
+ org-gcal-delete-at-point)
+ :config
+ (load-file +calendar-org-gcal-secret-file)
+ ;; hack to avoid the deferred.el error
+ (defun org-gcal--notify (title mes)
+ (message "org-gcal::%s - %s" title mes)))
+
+
+;; (def-package! alert)
diff --git a/modules/app/calendar/packages.el b/modules/app/calendar/packages.el
new file mode 100644
index 000000000..41bd347ec
--- /dev/null
+++ b/modules/app/calendar/packages.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; app/calendar/packages.el
+
+(package! calfw)
+(package! calfw-org)
+(package! org-gcal)
diff --git a/modules/app/calendar/readme.org b/modules/app/calendar/readme.org
new file mode 100644
index 000000000..c10af46ea
--- /dev/null
+++ b/modules/app/calendar/readme.org
@@ -0,0 +1,71 @@
+#+TITLE: `=Calendar App`
+* Setup sync between google calendar and org file
+:PROPERTIES:
+:ID: 5E190E8A-CA26-4679-B5F8-BF9CFD289271
+:END:
+- Checkout https://github.com/myuhe/org-gcal.el, put the following content in a file ~secret.el~ and set the variable `+calendar-org-gcal-secret-file` to the path of that file.
+ #+BEGIN_SRC emacs-lisp
+(setq org-gcal-client-id "your-id-foo.apps.googleusercontent.com"
+ org-gcal-client-secret "your-secret"
+ org-gcal-file-alist '(("your-mail@gmail.com" . "~/schedule.org")
+ ("another-mail@gmail.com" . "~/task.org")))
+ #+END_SRC
+* Doom faces
+:PROPERTIES:
+:ID: 8223894E-EA68-4259-A2EA-AF7E3653C610
+:END:
+I'm using the following setting:
+#+BEGIN_SRC emacs-lisp
+;; calfw
+(cfw:face-title :foreground blue :bold bold :height 2.0 :inherit 'variable-pitch)
+(cfw:face-header :foreground (doom-blend blue bg 0.8) :bold bold)
+(cfw:face-sunday :foreground (doom-blend red bg 0.8) :bold bold)
+(cfw:face-saturday :foreground (doom-blend red bg 0.8) :bold bold)
+(cfw:face-holiday :foreground nil :background bg-alt :bold bold)
+(cfw:face-grid :foreground vertical-bar)
+(cfw:face-periods :foreground yellow)
+(cfw:face-toolbar :foreground nil :background nil)
+(cfw:face-toolbar-button-off :foreground base6 :bold bold :inherit 'variable-pitch)
+(cfw:face-toolbar-button-on :foreground blue :bold bold :inherit 'variable-pitch)
+
+(cfw:face-default-content :foreground fg)
+(cfw:face-day-title :foreground fg :bold bold)
+(cfw:face-today-title :foreground bg :background blue :bold bold)
+(cfw:face-default-day :bold bold)
+(cfw:face-today :foreground nil :background nil :bold bold)
+(cfw:face-annotation :foreground violet)
+(cfw:face-disable :foreground grey)
+(cfw:face-select :background region)
+#+END_SRC
+* Adjust calendar to be included
+:PROPERTIES:
+:ID: D734975C-4B49-4F66-A088-AB2707A77537
+:END:
+Checkout example from https://github.com/kiwanami/emacs-calfw
+#+BEGIN_SRC emacs-lisp
+(defun my-open-calendar ()
+ (interactive)
+ (cfw:open-calendar-buffer
+ :contents-sources
+ (list
+ (cfw:org-create-source "Green") ; orgmode source
+ (cfw:howm-create-source "Blue") ; howm source
+ (cfw:cal-create-source "Orange") ; diary source
+ (cfw:ical-create-source "Moon" "~/moon.ics" "Gray") ; ICS source1
+ (cfw:ical-create-source "gcal" "https://..../basic.ics" "IndianRed") ; google calendar ICS
+ )))
+#+END_SRC
+Specifically, if you want to adjust the org files to be included, use a ~let~ binding to set the ~org-agenda-files~ like below:
+#+BEGIN_SRC emacs-lisp
+;;;###autoload
+(defun cfw:open-org-calendar-with-cal1 ()
+ (interactive)
+ (let ((org-agenda-files '("/path/to/org/" "/path/to/cal1.org")))
+ (call-interactively '+calendar/open-calendar)))
+
+;;;###autoload
+(defun cfw:open-org-calendar-with-cal2 ()
+ (interactive)
+ (let ((org-agenda-files '("/path/to/org/" "/path/to/cal2.org")))
+ (call-interactively '+calendar/open-calendar)))
+#+END_SRC
diff --git a/modules/app/email/+gmail.el b/modules/app/email/+gmail.el
new file mode 100644
index 000000000..6dbbf924c
--- /dev/null
+++ b/modules/app/email/+gmail.el
@@ -0,0 +1,45 @@
+;;; app/email/+gmail.el -*- lexical-binding: t; -*-
+
+(after! mu4e
+ ;; don't save message to Sent Messages, Gmail/IMAP takes care of this
+ (setq mu4e-sent-messages-behavior 'delete
+
+ ;; don't need to run cleanup after indexing for gmail
+ mu4e-index-cleanup nil
+
+ ;; because gmail uses labels as folders we can use lazy check since
+ ;; messages don't really "move"
+ mu4e-index-lazy-check t)
+
+ ;; In my workflow, emails won't be moved at all. Only their flags/labels are
+ ;; changed. Se we redefine the trash and refile marks not to do any moving.
+ ;; However, the real magic happens in `+email|gmail-fix-flags'.
+ ;;
+ ;; Gmail will handle the rest.
+ (defun +email--mark-seen (docid msg target)
+ (mu4e~proc-move docid (mu4e~mark-check-target target) "+S-u-N"))
+
+ (delq (assq 'delete mu4e-marks) mu4e-marks)
+ (setf (alist-get 'trash mu4e-marks)
+ (list :char '("d" . "▼")
+ :prompt "dtrash"
+ :dyn-target (lambda (_target msg) (mu4e-get-trash-folder msg))
+ :action #'+email--mark-seen)
+ ;; Refile will be my "archive" function.
+ (alist-get 'refile mu4e-marks)
+ (list :char '("d" . "▼")
+ :prompt "dtrash"
+ :dyn-target (lambda (_target msg) (mu4e-get-trash-folder msg))
+ :action #'+email--mark-seen))
+
+ ;; This hook correctly modifies gmail flags on emails when they are marked.
+ ;; Without it, refiling (archiving), trashing, and flagging (starring) email
+ ;; won't properly result in the corresponding gmail action, since the marks
+ ;; are ineffectual otherwise.
+ (defun +email|gmail-fix-flags (mark msg)
+ (pcase mark
+ (`trash (mu4e-action-retag-message msg "-\\Inbox,+\\Trash,-\\Draft"))
+ (`refile (mu4e-action-retag-message msg "-\\Inbox"))
+ (`flag (mu4e-action-retag-message msg "+\\Starred"))
+ (`unflag (mu4e-action-retag-message msg "-\\Starred"))))
+ (add-hook 'mu4e-mark-execute-pre-hook #'+email|gmail-fix-flags))
diff --git a/modules/app/email/README.org b/modules/app/email/README.org
index 0e4fd9728..0e4ddb6f7 100644
--- a/modules/app/email/README.org
+++ b/modules/app/email/README.org
@@ -1,42 +1,75 @@
-#+TITLE: :app email
+#+TITLE: app/email
+#+DATE: April 8, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
+* Table of Contents :TOC:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Prerequisites][Prerequisites]]
+ - [[MacOS][MacOS]]
+ - [[Arch Linux][Arch Linux]]
+- [[Features][Features]]
+- [[Configuration][Configuration]]
+ - [[offlineimap][offlineimap]]
+ - [[mbsync][mbsync]]
+
+* Description
This module makes Emacs an email client, using ~mu4e~.
#+begin_quote
-I want to live in Emacs, but as we all know, living is incomplete without email. So I prayed to the text editor gods and they (I) answered. Emacs+evil's editing combined with org-mode for writing emails? /Yes please./
+I want to live in Emacs, but as we all know, living is incomplete without email.
+So I prayed to the text editor gods and they (I) answered. Emacs+evil's editing
+combined with org-mode for writing emails? /Yes please./
-It uses ~mu4e~ to read my email, but depends on ~offlineimap~ (to sync my email via IMAP) and ~mu~ (to index my mail into a format ~mu4e~ can understand).
-
-WARNING: my config is gmail/gsuite oriented, and since Google has its own opinions on the IMAP standard, it is unlikely to translate to other hosts.
+It uses ~mu4e~ to read my email, but depends on ~offlineimap~ (to sync my email
+via IMAP) and ~mu~ (to index my mail into a format ~mu4e~ can understand).
#+end_quote
-* Table of Contents :TOC:
-- [[#install][Install]]
- - [[#macos][MacOS]]
- - [[#arch-linux][Arch Linux]]
-- [[#dependencies][Dependencies]]
+** Module Flags
++ ~+gmail~ Enables gmail-specific configuration.
-* Install
+** Plugins
++ [[https://github.com/agpchil/mu4e-maildirs-extension][mu4e-maildirs-extension]]
+
+* Prerequisites
This module requires:
-+ ~offlineimap~ (to sync mail with)
++ Either ~mbsync~ (default) or ~offlineimap~ (to sync mail with)
+ ~mu~ (to index your downloaded messages)
** MacOS
-#+BEGIN_SRC sh :tangle (if (doom-system-os 'macos) "yes")
+#+BEGIN_SRC sh
brew install mu --with-emacs
+# And one of the following
+brew install isync # mbsync
brew install offlineimap
#+END_SRC
** Arch Linux
-#+BEGIN_SRC sh :dir /sudo:: :tangle (if (doom-system-os 'arch) "yes")
-sudo pacman --noconfirm --needed -S offlineimap mu
+#+BEGIN_SRC sh
+sudo pacman --noconfirm --needed -S mu
+# And one of the following
+sudo pacman -S isync # mbsync
+sudo pacman -S offlineimap
#+END_SRC
-* Dependencies
-You need to do the following:
+* TODO Features
-1. Write a ~\~/.offlineimaprc~. Mine can be found [[https://github.com/hlissner/dotfiles/tree/master/shell/+mu][in my dotfiles repository]]. It is configured to download mail to ~\~/.mail~. I use [[https://www.passwordstore.org/][unix pass]] to securely store my login credentials.
+* Configuration
+** offlineimap
+This module uses =mbsync= by default. To change this, change ~+email-backend~:
+
+#+BEGIN_SRC emacs-lisp
+(setq +email-backend 'offlineimap)
+#+END_SRC
+
+Then you must set up offlineimap and index your mail:
+
+1. Write a ~\~/.offlineimaprc~. Mine can be found [[https://github.com/hlissner/dotfiles/tree/master/shell/mu][in my dotfiles repository]]. It
+ is configured to download mail to ~\~/.mail~. I use [[https://www.passwordstore.org/][unix pass]] to securely
+ store my login credentials.
2. Download your email: ~offlineimap -o~ (may take a while)
3. Index it with mu: ~mu index --maildir ~/.mail~
@@ -44,14 +77,15 @@ Then configure Emacs to use your email address:
#+BEGIN_SRC emacs-lisp :tangle no
;; Each path is relative to `+email-mu4e-mail-path', which is ~/.mail by default
-(set! :email "Lissner.net"
- '((mu4e-sent-folder . "/Lissner.net/Sent Mail")
- (mu4e-drafts-folder . "/Lissner.net/Drafts")
- (mu4e-trash-folder . "/Lissner.net/Trash")
- (mu4e-refile-folder . "/Lissner.net/All Mail")
- (smtpmail-smtp-user . "henrik@lissner.net")
- (user-mail-address . "henrik@lissner.net")
- (mu4e-compose-signature . "---\nHenrik Lissner"))
- t)
+(set-email-account! "Lissner.net"
+ '((mu4e-sent-folder . "/Lissner.net/Sent Mail")
+ (mu4e-drafts-folder . "/Lissner.net/Drafts")
+ (mu4e-trash-folder . "/Lissner.net/Trash")
+ (mu4e-refile-folder . "/Lissner.net/All Mail")
+ (smtpmail-smtp-user . "henrik@lissner.net")
+ (user-mail-address . "henrik@lissner.net")
+ (mu4e-compose-signature . "---\nHenrik Lissner"))
+ t)
#+END_SRC
+** TODO mbsync
diff --git a/modules/app/email/autoload/email.el b/modules/app/email/autoload/email.el
index 284792762..5a468abc5 100644
--- a/modules/app/email/autoload/email.el
+++ b/modules/app/email/autoload/email.el
@@ -1,10 +1,62 @@
;;; app/email/autoload/email.el -*- lexical-binding: t; -*-
+;;;###autodef
+(defun set-email-account! (label letvars &optional default-p)
+ "Registers an email address for mu4e. The LABEL is a string. LETVARS are a
+list of cons cells (VARIABLE . VALUE) -- you may want to modify:
+
+ + `user-full-name' (this or the global `user-full-name' is required)
+ + `user-mail-address' (required)
+ + `smtpmail-smtp-user' (required for sending mail from Emacs)
+
+OPTIONAL:
+ + `mu4e-sent-folder'
+ + `mu4e-drafts-folder'
+ + `mu4e-trash-folder'
+ + `mu4e-refile-folder'
+ + `mu4e-compose-signature'
+
+DEFAULT-P is a boolean. If non-nil, it marks that email account as the
+default/fallback account."
+ (after! mu4e
+ (when-let* ((address (cdr (assq 'user-mail-address letvars))))
+ (add-to-list 'mu4e-user-mail-address-list address))
+ (setq mu4e-contexts
+ (cl-loop for context in mu4e-contexts
+ unless (string= (mu4e-context-name context) label)
+ collect context))
+ (let ((context (make-mu4e-context
+ :name label
+ :enter-func (lambda () (mu4e-message "Switched to %s" label))
+ :leave-func #'mu4e-clear-caches
+ :match-func
+ (lambda (msg)
+ (when msg
+ (string-prefix-p (format "/%s" label)
+ (mu4e-message-field msg :maildir))))
+ :vars letvars)))
+ (push context mu4e-contexts)
+ (when default-p
+ (setq-default mu4e-context-current context))
+ context)))
+
+
+
+(defvar +email-workspace-name "*mu4e*"
+ "TODO")
+
+(add-hook 'mu4e-main-mode-hook #'+email|init)
+
;;;###autoload
(defun =email ()
"Start email client."
(interactive)
- (call-interactively #'mu4e))
+ (require 'mu4e)
+ (+workspace-switch +email-workspace-name t)
+ (mu4e~start 'mu4e~main-view)
+ ;; (save-selected-window
+ ;; (prolusion-mail-show))
+ )
;;;###autoload
(defun +email/compose ()
@@ -13,3 +65,15 @@
;; TODO Interactively select email account
(call-interactively #'mu4e-compose-new))
+
+;;
+;; Hooks
+
+(defun +email|init ()
+ (add-hook 'kill-buffer-hook #'+email|kill-mu4e nil t))
+
+(defun +email|kill-mu4e ()
+ ;; (prolusion-mail-hide)
+ (when (+workspace-exists-p +email-workspace-name)
+ (+workspace/delete +email-workspace-name)))
+
diff --git a/modules/app/email/config.el b/modules/app/email/config.el
index c447d2b0b..3b8f4b556 100644
--- a/modules/app/email/config.el
+++ b/modules/app/email/config.el
@@ -4,88 +4,52 @@
;; to give me the ability to read, search, write and send my email. It does so
;; with `mu4e', and requires `offlineimap' and `mu' to be installed.
-(defvar +email-mu4e-mail-path "~/.mail"
- "The directory path of mu's maildir.")
+(defvar +email-backend 'mbsync
+ "Which backend to use. Can either be offlineimap, mbsync or nil (manual).")
;;
-;; Config
-;;
+;; Packages
-(def-setting! :email (label letvars &optional default-p)
- "Registers an email address for mu4e. The LABEL is a string. LETVARS are a
-list of cons cells (VARIABLE . VALUE) -- you may want to modify:
+(add-to-list 'auto-mode-alist '("\\.\\(?:offlineimap\\|mbsync\\)rc\\'" . conf-mode))
- + `user-full-name' (this or the global `user-full-name' is required)
- + `user-mail-address' (required)
- + `smtpmail-smtp-user' (required for sending mail from Emacs)
-
-OPTIONAL:
- + `mu4e-sent-folder'
- + `mu4e-drafts-folder'
- + `mu4e-trash-folder'
- + `mu4e-refile-folder'
- + `mu4e-compose-signature'
-
-DEFAULT-P is a boolean. If non-nil, it marks that email account as the
-default/fallback account."
- `(after! mu4e
- (let ((account-vars ,letvars))
- (when-let* ((address (cdr (assq 'user-mail-address account-vars))))
- (cl-pushnew address mu4e-user-mail-address-list :test #'equal))
- (let ((context (make-mu4e-context
- :name ,label
- :enter-func (lambda () (mu4e-message "Switched to %s" ,label))
- :leave-func (lambda () (mu4e-clear-caches))
- :match-func
- (lambda (msg)
- (when msg
- (string-prefix-p (format "/%s" ,label)
- (mu4e-message-field msg :maildir))))
- :vars ,letvars)))
- (push context mu4e-contexts)
- ,(when default-p
- `(setq-default mu4e-context-current context))))))
-
-
-;;
-;; Plugins
-;;
(def-package! mu4e
:commands (mu4e mu4e-compose-new)
+ :init
+ (provide 'html2text) ; disable obsolete package
+ (setq mu4e-maildir "~/.mail"
+ mu4e-attachment-dir "~/.mail/.attachments"
+ mu4e-user-mail-address-list nil)
:config
- (setq mu4e-maildir +email-mu4e-mail-path
- mu4e-attachment-dir "~/Downloads"
- mu4e-user-mail-address-list nil
- mu4e-update-interval nil
+ (pcase +email-backend
+ (`mbsync
+ (setq mu4e-get-mail-command "mbsync -a"
+ mu4e-change-filenames-when-moving t))
+ (`offlineimap
+ (setq mu4e-get-mail-command "offlineimap -o -q")))
+
+ (setq mu4e-update-interval nil
mu4e-compose-format-flowed t ; visual-line-mode + auto-fill upon sending
mu4e-view-show-addresses t
+ mu4e-sent-messages-behavior 'sent
+ mu4e-hide-index-messages t
;; try to show images
mu4e-view-show-images t
mu4e-view-image-max-width 800
- ;; don't save message to Sent Messages, Gmail/IMAP takes care of this
- mu4e-sent-messages-behavior 'delete
- ;; allow for updating mail using 'U' in the main view:
- ;; for mbsync
- ;; mu4e-headers-skip-duplicates t
- ;; mu4e-change-filenames-when-moving nil
- ;; mu4e-get-mail-command "mbsync -a"
- ;; for offlineimap
- mu4e-get-mail-command "offlineimap -o -q"
;; configuration for sending mail
message-send-mail-function #'smtpmail-send-it
smtpmail-stream-type 'starttls
+ message-kill-buffer-on-exit t ; close after sending
;; start with the first (default) context;
mu4e-context-policy 'pick-first
;; compose with the current context, or ask
mu4e-compose-context-policy 'ask-if-none
;; use helm/ivy
- mu4e-completing-read-function (cond ((featurep! :completion ivy) #'ivy-completing-read)
- ((featurep! :completion helm) #'completing-read)
- (t #'ido-completing-read))
- ;; close message after sending it
- message-kill-buffer-on-exit t
+ mu4e-completing-read-function
+ (cond ((featurep! :completion ivy) #'ivy-completing-read)
+ ((featurep! :completion helm) #'completing-read)
+ (t #'ido-completing-read))
;; no need to ask
mu4e-confirm-quit nil
;; remove 'lists' column
@@ -94,14 +58,28 @@ default/fallback account."
(:human-date . 12)
(:flags . 4)
(:from . 25)
- (:subject))
- mu4e-bookmarks `(("\\\\Inbox" "Inbox" ?i)
- ("\\\\Draft" "Drafts" ?d)
- ("flag:unread AND \\\\Inbox" "Unread messages" ?u)
- ("flag:flagged" "Starred messages" ?s)
- ("date:today..now" "Today's messages" ?t)
- ("date:7d..now" "Last 7 days" ?w)
- ("mime:image/*" "Messages with images" ?p)))
+ (:subject)))
+
+ ;; set mail user agent
+ (setq mail-user-agent 'mu4e-user-agent)
+
+ ;; Use fancy icons
+ (setq mu4e-headers-has-child-prefix '("+" . "")
+ mu4e-headers-empty-parent-prefix '("-" . "")
+ mu4e-headers-first-child-prefix '("\\" . "")
+ mu4e-headers-duplicate-prefix '("=" . "")
+ mu4e-headers-default-prefix '("|" . "")
+ mu4e-headers-draft-mark '("D" . "")
+ mu4e-headers-flagged-mark '("F" . "")
+ mu4e-headers-new-mark '("N" . "")
+ mu4e-headers-passed-mark '("P" . "")
+ mu4e-headers-replied-mark '("R" . "")
+ mu4e-headers-seen-mark '("S" . "")
+ mu4e-headers-trashed-mark '("T" . "")
+ mu4e-headers-attach-mark '("a" . "")
+ mu4e-headers-encrypted-mark '("x" . "")
+ mu4e-headers-signed-mark '("s" . "")
+ mu4e-headers-unread-mark '("u" . ""))
;; Add a column to display what email account the email belongs to.
(add-to-list 'mu4e-header-info-custom
@@ -114,152 +92,33 @@ default/fallback account."
(let ((maildir (mu4e-message-field msg :maildir)))
(format "%s" (substring maildir 1 (string-match-p "/" maildir 1)))))))
- ;; In my workflow, emails won't be moved at all. Only their flags/labels are
- ;; changed. Se we redefine the trash and refile marks not to do any moving.
- ;; However, the real magic happens in `+email|gmail-fix-flags'.
- ;;
- ;; Gmail will handle the rest.
- (setq mu4e-marks (assq-delete-all 'delete mu4e-marks))
- (setq mu4e-marks (assq-delete-all 'trash mu4e-marks))
- (push '(trash :char ("d" . "▼")
- :prompt "dtrash"
- :dyn-target (lambda (target msg) (mu4e-get-trash-folder msg))
- :action
- (lambda (docid msg target)
- (mu4e~proc-move docid (mu4e~mark-check-target target) "+S-u-N")))
- mu4e-marks)
-
- ;; Refile will be my "archive" function.
- (setq mu4e-marks (assq-delete-all 'refile mu4e-marks))
- (push '(refile :char ("r" . "▶")
- :prompt "refile"
- :show-target (lambda (target) "archive")
- :action
- (lambda (docid msg target)
- (mu4e~proc-move docid (mu4e~mark-check-target target) "+S-u-N")))
- mu4e-marks)
-
- ;; This hook correctly modifies gmail flags on emails when they are marked.
- ;; Without it, refiling (archiving), trashing, and flagging (starring) email
- ;; won't properly result in the corresponding gmail action, since the marks
- ;; are ineffectual otherwise.
- (defun +email|gmail-fix-flags (mark msg)
- (cond ((eq mark 'trash) (mu4e-action-retag-message msg "-\\Inbox,+\\Trash,-\\Draft"))
- ((eq mark 'refile) (mu4e-action-retag-message msg "-\\Inbox"))
- ((eq mark 'flag) (mu4e-action-retag-message msg "+\\Starred"))
- ((eq mark 'unflag) (mu4e-action-retag-message msg "-\\Starred"))))
- (add-hook 'mu4e-mark-execute-pre-hook #'+email|gmail-fix-flags)
-
;; Refresh the current view after marks are executed
(defun +email*refresh (&rest _) (mu4e-headers-rerun-search))
(advice-add #'mu4e-mark-execute-all :after #'+email*refresh)
- (when (featurep! :feature spellcheck)
+ (when (featurep! :tools flyspell)
(add-hook 'mu4e-compose-mode-hook #'flyspell-mode))
;; Wrap text in messages
- (add-hook! 'mu4e-view-mode-hook
- (setq-local truncate-lines nil))
+ (setq-hook! 'mu4e-view-mode-hook truncate-lines nil)
(when (fboundp 'imagemagick-register-types)
(imagemagick-register-types))
- (after! evil
- (cl-loop for str in '((mu4e-main-mode . normal)
- (mu4e-view-mode . normal)
- (mu4e-headers-mode . normal)
- (mu4e-compose-mode . normal)
- (mu4e~update-mail-mode . normal))
- do (evil-set-initial-state (car str) (cdr str)))
-
- (setq mu4e-view-mode-map (make-sparse-keymap)
- ;; mu4e-compose-mode-map (make-sparse-keymap)
- mu4e-headers-mode-map (make-sparse-keymap)
- mu4e-main-mode-map (make-sparse-keymap))
-
- (map! (:map (mu4e-main-mode-map mu4e-view-mode-map)
- :leader
- :n "," #'mu4e-context-switch
- :n "." #'mu4e-headers-search-bookmark
- :n ">" #'mu4e-headers-search-bookmark-edit
- :n "/" #'mu4e~headers-jump-to-maildir)
-
- (:map (mu4e-headers-mode-map mu4e-view-mode-map)
- :localleader
- :n "f" #'mu4e-compose-forward
- :n "r" #'mu4e-compose-reply
- :n "c" #'mu4e-compose-new
- :n "e" #'mu4e-compose-edit)
-
- (:map mu4e-main-mode-map
- :n "q" #'mu4e-quit
- :n "u" #'mu4e-update-index
- :n "U" #'mu4e-update-mail-and-index
- :n "J" #'mu4e~headers-jump-to-maildir
- :n "c" #'+email/compose
- :n "b" #'mu4e-headers-search-bookmark)
-
- (:map mu4e-headers-mode-map
- :n "q" #'mu4e~headers-quit-buffer
- :n "r" #'mu4e-compose-reply
- :n "c" #'mu4e-compose-edit
- :n "s" #'mu4e-headers-search-edit
- :n "S" #'mu4e-headers-search-narrow
- :n "RET" #'mu4e-headers-view-message
- :n "u" #'mu4e-headers-mark-for-unmark
- :n "U" #'mu4e-mark-unmark-all
- :n "v" #'evil-visual-line
- :nv "d" #'+email/mark
- :nv "=" #'+email/mark
- :nv "-" #'+email/mark
- :nv "+" #'+email/mark
- :nv "!" #'+email/mark
- :nv "?" #'+email/mark
- :nv "r" #'+email/mark
- :nv "m" #'+email/mark
- :n "x" #'mu4e-mark-execute-all
-
- :n "]]" #'mu4e-headers-next-unread
- :n "[[" #'mu4e-headers-prev-unread
-
- (:localleader
- :n "s" 'mu4e-headers-change-sorting
- :n "t" 'mu4e-headers-toggle-threading
- :n "r" 'mu4e-headers-toggle-include-related
-
- :n "%" #'mu4e-headers-mark-pattern
- :n "t" #'mu4e-headers-mark-subthread
- :n "T" #'mu4e-headers-mark-thread))
-
- (:map mu4e-view-mode-map
- :n "q" #'mu4e~view-quit-buffer
- :n "r" #'mu4e-compose-reply
- :n "c" #'mu4e-compose-edit
- :n "o" #'ace-link-mu4e
-
- :n "" #'mu4e-view-headers-prev
- :n "" #'mu4e-view-headers-next
- :n "[m" #'mu4e-view-headers-prev
- :n "]m" #'mu4e-view-headers-next
- :n "[u" #'mu4e-view-headers-prev-unread
- :n "]u" #'mu4e-view-headers-next-unread
-
- (:localleader
- :n "%" #'mu4e-view-mark-pattern
- :n "t" #'mu4e-view-mark-subthread
- :n "T" #'mu4e-view-mark-thread
-
- :n "d" #'mu4e-view-mark-for-trash
- :n "r" #'mu4e-view-mark-for-refile
- :n "m" #'mu4e-view-mark-for-move))
-
- (:map mu4e~update-mail-mode-map
- :n "q" #'mu4e-interrupt-update-mail))))
+ (set-evil-initial-state!
+ '(mu4e-main-mode
+ mu4e-view-mode
+ mu4e-headers-mode
+ mu4e-compose-mode
+ mu4e~update-mail-mode)
+ 'normal))
(def-package! mu4e-maildirs-extension
:after mu4e
- :config (mu4e-maildirs-extension-load))
+ :config
+ (mu4e-maildirs-extension)
+ (setq mu4e-maildirs-extension-title nil))
(def-package! org-mu4e
@@ -273,3 +132,8 @@ default/fallback account."
(add-hook! 'message-send-hook
(setq-local org-mu4e-convert-to-html nil)))
+
+;;
+;; Sub-modules
+
+(if (featurep! +gmail) (load! "+gmail"))
diff --git a/modules/app/irc/README.org b/modules/app/irc/README.org
index caf043a6d..0ac15e5df 100644
--- a/modules/app/irc/README.org
+++ b/modules/app/irc/README.org
@@ -1,59 +1,118 @@
-#+TITLE: :app irc
-
-This module turns adds an IRC client to Emacs ([[https://github.com/jorgenschaefer/circe][~circe~)]] with native notifications ([[https://github.com/eqyiel/circe-notifications][circe-notifications]]).
+#+TITLE: app/irc
+#+DATE: June 11, 2017
+#+SINCE: v2.0.3
+#+STARTUP: inlineimages
* Table of Contents :TOC:
-- [[#dependencies][Dependencies]]
-- [[#configure][Configure]]
- - [[#pass-the-unix-password-manager][Pass: the unix password manager]]
- - [[#emacs-auth-source-api][Emacs' auth-source API]]
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Dependencies][Dependencies]]
+- [[Prerequisites][Prerequisites]]
+- [[Features][Features]]
+ - [[An IRC Client in Emacs][An IRC Client in Emacs]]
+- [[Configuration][Configuration]]
+ - [[Pass: the unix password manager][Pass: the unix password manager]]
+ - [[Emacs' auth-source API][Emacs' auth-source API]]
+- [[Troubleshooting][Troubleshooting]]
+
+* Description
+This module turns adds an IRC client to Emacs with OS notifications.
+
+** Module Flags
+This module provides no flags.
+
+** Plugins
++ [[https://github.com/jorgenschaefer/circe][circe]]
++ [[https://github.com/eqyiel/circe-notifications][circe-notifications]]
* Dependencies
-This module has no dependencies, besides =gnutls-cli= or =openssl= for secure connections.
+This module requires =gnutls-cli= or =openssl= for secure connections.
-* Configure
-Use the ~:irc~ setting to configure IRC servers. Its second argument (a plist) takes the same arguments as ~circe-network-options~.
+* Prerequisites
+This module has no direct prerequisites.
+
+* Features
+** An IRC Client in Emacs
+To connect to IRC you can invoke the ~=irc~ function using =M-x= or your own
+custom keybinding.
+
+| command | description |
+|---------+-------------------------------------------|
+| ~=irc~ | Connect to IRC and all configured servers |
+
+When in a circe buffer these keybindings will be available.
+
+| command | key | description |
+|-----------------------------+-----------+----------------------------------------------|
+| ~+irc/tracking-next-buffer~ | =SPC m a= | Switch to the next active buffer |
+| ~circe-command-JOIN~ | =SPC m j= | Join a channel |
+| ~+irc/send-message~ | =SPC m m= | Send a private message |
+| ~circe-command-NAMES~ | =SPC m n= | List the names of the current channel |
+| ~circe-command-PART~ | =SPC m p= | Part the current channel |
+| ~+irc/quit~ | =SPC m Q= | Kill the current circe session and workgroup |
+| ~circe-reconnect~ | =SPC m R= | Reconnect the current server |
+
+* Configuration
+Use ~set-irc-server!~ to configure IRC servers. Its second argument (a plist)
+takes the same arguments as ~circe-network-options~.
#+BEGIN_SRC emacs-lisp :tangle no
-(set! :irc "chat.freenode.net"
- `(:tls t
- :nick "doom"
- :sasl-username "myusername"
- :sasl-password "mypassword"
- :channels ("#emacs")))
+(set-irc-server! "chat.freenode.net"
+ `(:tls t
+ :nick "doom"
+ :sasl-username "myusername"
+ :sasl-password "mypassword"
+ :channels ("#emacs")))
#+END_SRC
-*It is a obviously a bad idea to store auth-details in plaintext,* so here are some ways to avoid that:
+*It is a obviously a bad idea to store auth-details in plaintext,* so here are
+some ways to avoid that:
** Pass: the unix password manager
-[[https://www.passwordstore.org/][Pass]] is my tool of choice. I use it to manage my passwords. If you activate the [[/modules/tools/password-store/README.org][:tools password-store]] module you get an elisp API through which to access your password store.
+[[https://www.passwordstore.org/][Pass]] is my tool of choice. I use it to manage my passwords. If you activate the
+[[../../../modules/tools/password-store/README.org][:tools password-store]] module you get an elisp API through which to access your
+password store.
-~:irc~'s plist can use functions instead of strings. ~+pass-get-user~ and ~+pass-get-secret~ can help here:
+~set-irc-server!~ accepts a plist can use functions instead of strings.
+~+pass-get-user~ and ~+pass-get-secret~ can help here:
#+BEGIN_SRC emacs-lisp :tangle no
-(set! :irc "chat.freenode.net"
- `(:tls t
- :nick "doom"
- :sasl-username ,(+pass-get-user "irc/freenode.net")
- :sasl-password ,(+pass-get-secret "irc/freenode.net")
- :channels ("#emacs")))
+(set-irc-server! "chat.freenode.net"
+ `(:tls t
+ :nick "doom"
+ :sasl-username ,(+pass-get-user "irc/freenode.net")
+ :sasl-password ,(+pass-get-secret "irc/freenode.net")
+ :channels ("#emacs")))
#+END_SRC
-But wait, there's more! This stores your password in a public variable which could be accessed or appear in backtraces. Not good! So we go a step further:
+But wait, there's more! This stores your password in a public variable which
+could be accessed or appear in backtraces. Not good! So we go a step further:
#+BEGIN_SRC emacs-lisp :tangle no
-(set! :irc "chat.freenode.net"
- `(:tls t
- :nick "doom"
- :sasl-username ,(+pass-get-user "irc/freenode.net")
- :sasl-password (lambda (&rest _) (+pass-get-secret "irc/freenode.net"))
- :channels ("#emacs")))
+(set-irc-server! "chat.freenode.net"
+ `(:tls t
+ :port 6697
+ :nick "doom"
+ :sasl-username ,(+pass-get-user "irc/freenode.net")
+ :sasl-password (lambda (&rest _) (+pass-get-secret "irc/freenode.net"))
+ :channels ("#emacs")))
#+END_SRC
And you're good to go!
+Note that =+pass-get-user= tries to find your username by looking for the fields
+listed in =+pass-user-fields= (by default =login=, =user==, =username== and
+=email=)=). An example configuration looks like
+
+#+BEGIN_SRC txt :tangle no
+mysecretpassword
+username: myusername
+#+END_SRC
+
** Emacs' auth-source API
-~auth-source~ is built into Emacs. As suggested [[https://github.com/jorgenschaefer/circe/wiki/Configuration#safer-password-management][in the circe wiki]], you can store (and retrieve) encrypted passwords with it.
+~auth-source~ is built into Emacs. As suggested [[https://github.com/jorgenschaefer/circe/wiki/Configuration#safer-password-management][in the circe wiki]], you can store
+(and retrieve) encrypted passwords with it.
#+BEGIN_SRC emacs-lisp :tangle no
(setq auth-sources '("~/.authinfo.gpg"))
@@ -71,10 +130,12 @@ And you're good to go!
(defun my-nickserv-password (server)
(my-fetch-password :user "forcer" :host "irc.freenode.net"))
-(set! :irc "chat.freenode.net"
- '(:tls t
- :nick "doom"
- :sasl-password my-nickserver-password
- :channels ("#emacs")))
+(set-irc-server! "chat.freenode.net"
+ '(:tls t
+ :port 6697
+ :nick "doom"
+ :sasl-password my-nickserver-password
+ :channels ("#emacs")))
#+END_SRC
+* TODO Troubleshooting
diff --git a/modules/app/irc/autoload/irc.el b/modules/app/irc/autoload/irc.el
index 1e1f01210..442f05b77 100644
--- a/modules/app/irc/autoload/irc.el
+++ b/modules/app/irc/autoload/irc.el
@@ -20,11 +20,17 @@
If INHIBIT-WORKSPACE (the universal argument) is non-nil, don't spawn a new
workspace for it."
(interactive "P")
- (if (+workspace-exists-p +irc--workspace-name)
- (+workspace-switch +irc--workspace-name)
- (and (+irc-setup-wconf inhibit-workspace)
- (cl-loop for network in circe-network-options
- collect (circe (car network))))))
+ (cond ((and (featurep! :feature workspaces)
+ (+workspace-exists-p +irc--workspace-name))
+ (+workspace-switch +irc--workspace-name))
+ ((not (+irc-setup-wconf inhibit-workspace))
+ (user-error "Couldn't start up a workspace for IRC")))
+ (if (doom-buffers-in-mode 'circe-mode (buffer-list) t)
+ (message "Circe buffers are already open")
+ (if circe-network-options
+ (cl-loop for network in circe-network-options
+ collect (circe (car network)))
+ (call-interactively #'circe))))
;;;###autoload
(defun +irc/connect (&optional inhibit-workspace)
@@ -36,6 +42,12 @@ workspace for it."
(and (+irc-setup-wconf inhibit-workspace)
(call-interactively #'circe)))
+;;;###autoload
+(defun +irc/send-message (who what)
+ "Send WHO a message containing WHAT."
+ (interactive "sWho: \nsWhat: ")
+ (circe-command-MSG who what))
+
;;;###autoload
(defun +irc/quit ()
"Kill current circe session and workgroup."
@@ -78,3 +90,10 @@ argument) is non-nil only show channels in current server."
(defun +irc--ivy-switch-to-buffer-action (buffer)
(when (stringp buffer)
(ivy--switch-buffer-action (string-trim-left buffer))))
+
+;;;###autoload
+(defun +irc/tracking-next-buffer ()
+ "Dissables switching to an unread buffer unless in the irc workspace."
+ (interactive)
+ (when (derived-mode-p 'circe-mode)
+ (tracking-next-buffer)))
diff --git a/modules/app/irc/autoload/settings.el b/modules/app/irc/autoload/settings.el
new file mode 100644
index 000000000..59615a760
--- /dev/null
+++ b/modules/app/irc/autoload/settings.el
@@ -0,0 +1,9 @@
+;;; app/irc/autoload/settings.el -*- lexical-binding: t; -*-
+
+;;;###autodef
+(defun set-irc-server! (server letvars)
+ "Registers an irc SERVER for circe.
+
+See `circe-network-options' for details."
+ (after! circe
+ (push (cons server letvars) circe-network-options)))
diff --git a/modules/app/irc/config.el b/modules/app/irc/config.el
index 35d02432c..34be5d598 100644
--- a/modules/app/irc/config.el
+++ b/modules/app/irc/config.el
@@ -1,7 +1,9 @@
;;; app/irc/config.el -*- lexical-binding: t; -*-
(defvar +irc-left-padding 13
- "TODO")
+ "By how much spaces the left hand side of the line should be padded.
+Below a value of 12 this may result in uneven alignment between the various
+types of messages.")
(defvar +irc-truncate-nick-char ?…
"Character to displayed when nick > `+irc-left-padding' in length.")
@@ -11,27 +13,29 @@
"If these commands are called pre prompt the buffer will scroll to `point-max'.")
(defvar +irc-disconnect-hook nil
- "TODO")
+ "Runs each hook when circe noticies the connection has been disconnected.
+Useful for scenarios where an instant reconnect will not be successful.")
(defvar +irc-bot-list '("fsbot" "rudybot")
- "TODO")
+ "Nicks listed have `circe-fool-face' applied and will not be tracked.")
(defvar +irc-time-stamp-format "%H:%M"
- "TODO")
+ "The format of time stamps.
+
+See `format-time-string' for a full description of available
+formatting directives. ")
(defvar +irc-notifications-watch-strings nil
- "TODO")
+ "A list of strings which can trigger a notification. You don't need to put
+your nick here.
+
+See `circe-notifications-watch-strings'.")
(defvar +irc-defer-notifications nil
"How long to defer enabling notifications, in seconds (e.g. 5min = 300).
Useful for ZNC users who want to avoid the deluge of notifications during buffer
playback.")
-(def-setting! :irc (server letvars)
- "Registers an irc server for circe."
- `(after! circe
- (push (cons ,server ,letvars) circe-network-options)))
-
(defvar +irc--defer-timer nil)
(defsubst +irc--pad (left right)
@@ -40,8 +44,7 @@ playback.")
;;
-;; Plugins
-;;
+;; Packages
(def-package! circe
:commands (circe circe-server-buffers)
@@ -56,6 +59,10 @@ playback.")
circe-format-self-say circe-format-say
circe-format-action (format "{nick:+%ss} * {body}" +irc-left-padding)
circe-format-self-action circe-format-action
+ circe-format-server-notice
+ (let ((left "-Server-")) (concat (make-string (- +irc-left-padding (length left)) ? )
+ (concat left " _ {body}")))
+ circe-format-notice (format "{nick:%ss} _ {body}" +irc-left-padding)
circe-format-server-topic
(+irc--pad "Topic" "{userhost}: {topic-diff}")
circe-format-server-join-in-channel
@@ -70,6 +77,8 @@ playback.")
(+irc--pad "Quit" "{nick} ({userhost}) left {channel}: {reason}]")
circe-format-server-rejoin
(+irc--pad "Re-join" "{nick} ({userhost}), left {departuredelta} ago")
+ circe-format-server-netmerge
+ (+irc--pad "Netmerge" "{split}, split {ago} ago (Use /WL to see who's still missing)")
circe-format-server-nick-change
(+irc--pad "Nick" "{old-nick} ({userhost}) is now known as {new-nick}")
circe-format-server-nick-change-self
@@ -83,8 +92,9 @@ playback.")
(add-hook 'circe-channel-mode-hook #'turn-on-visual-line-mode)
- ;; Let `+irc/quit' and `circe' handle buffer cleanup
- (map! :map circe-mode-map [remap doom/kill-this-buffer] #'bury-buffer)
+ (defun +irc*circe-disconnect-hook (&rest _)
+ (run-hooks '+irc-disconnect-hook))
+ (advice-add 'circe--irc-conn-disconnected :after #'+irc*circe-disconnect-hook)
(defun +irc*circe-truncate-nicks ()
"Truncate long nicknames in chat output non-destructively."
@@ -98,15 +108,48 @@ playback.")
+irc-truncate-nick-char)))))
(add-hook 'lui-pre-output-hook #'+irc*circe-truncate-nicks)
+ (defun +circe-buffer-p (buf)
+ "Return non-nil if BUF is a `circe-mode' buffer."
+ (with-current-buffer buf
+ (and (derived-mode-p 'circe-mode)
+ (eq (safe-persp-name (get-current-persp))
+ +irc--workspace-name))))
+ (add-hook 'doom-real-buffer-functions #'+circe-buffer-p)
+
(defun +irc|circe-message-option-bot (nick &rest ignored)
"Fontify known bots and mark them to not be tracked."
(when (member nick +irc-bot-list)
'((text-properties . (face circe-fool-face lui-do-not-track t)))))
(add-hook 'circe-message-option-functions #'+irc|circe-message-option-bot)
- (after! solaire-mode
- ;; distinguish chat/channel buffers from server buffers.
- (add-hook 'circe-chat-mode-hook #'solaire-mode)))
+ (defun +irc|add-circe-buffer-to-persp ()
+ (let ((persp (get-current-persp))
+ (buf (current-buffer)))
+ ;; Add a new circe buffer to irc workspace when we're in another workspace
+ (unless (eq (safe-persp-name persp) +irc--workspace-name)
+ ;; Add new circe buffers to the persp containing circe buffers
+ (persp-add-buffer buf (persp-get-by-name +irc--workspace-name))
+ ;; Remove new buffer from accidental workspace
+ (persp-remove-buffer buf persp))))
+ (add-hook 'circe-mode-hook #'+irc|add-circe-buffer-to-persp)
+
+ ;; Let `+irc/quit' and `circe' handle buffer cleanup
+ (define-key circe-mode-map [remap kill-buffer] #'bury-buffer)
+ ;; Fail gracefully if not in a circe buffer
+ (global-set-key [remap tracking-next-buffer] #'+irc/tracking-next-buffer)
+
+ (map! :localleader
+ (:map circe-mode-map
+ "a" #'tracking-next-buffer
+ "j" #'circe-command-JOIN
+ "m" #'+irc/send-message
+ "p" #'circe-command-PART
+ "Q" #'+irc/quit
+ "R" #'circe-reconnect
+ (:when (featurep! :completion ivy)
+ "c" #'+irc/ivy-jump-to-channel))
+ (:map circe-channel-mode-map
+ "n" #'circe-command-NAMES)))
(def-package! circe-color-nicks
@@ -144,10 +187,11 @@ playback.")
(def-package! lui
:commands lui-mode
:config
- (map! :map lui-mode-map "C-u" #'lui-kill-to-beginning-of-line)
- (when (featurep! :feature spellcheck)
- (setq lui-flyspell-p t
- lui-fill-type nil))
+ (define-key lui-mode-map "\C-u" #'lui-kill-to-beginning-of-line)
+ (setq lui-fill-type nil)
+
+ (when (featurep! :tools flyspell)
+ (setq lui-flyspell-p t))
(after! evil
(defun +irc|evil-insert ()
@@ -182,6 +226,9 @@ Courtesy of esh-mode.el"
(add-hook! 'lui-mode-hook
(add-hook 'pre-command-hook #'+irc|preinput-scroll-to-bottom nil t))
+ ;; enable a horizontal line marking the last read message
+ (add-hook! 'lui-mode-hook #'enable-lui-track-bar)
+
(defun +irc|init-lui-margins ()
(setq lui-time-stamp-position 'right-margin
lui-time-stamp-format +irc-time-stamp-format
diff --git a/modules/app/notmuch/autoload.el b/modules/app/notmuch/autoload.el
new file mode 100644
index 000000000..67f4f5e0c
--- /dev/null
+++ b/modules/app/notmuch/autoload.el
@@ -0,0 +1,189 @@
+;;; app/notmuch/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun =notmuch ()
+ "Activate (or switch to) `notmuch' in its workspace."
+ (interactive)
+ (unless (featurep! :feature workspaces)
+ (user-error ":feature workspaces is required, but disabled"))
+ (condition-case-unless-debug e
+ (progn
+ (+workspace-switch "*MAIL*" t)
+ (if-let* ((buf (cl-find-if (lambda (it) (string-match-p "^\\*notmuch" (buffer-name (window-buffer it))))
+ (doom-visible-windows))))
+ (select-window (get-buffer-window buf))
+ (notmuch-search "tag:inbox"))
+ (+workspace/display))
+ ('error
+ (+notmuch/quit)
+ (signal (car e) (cdr e)))))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun +notmuch/quit ()
+ (interactive)
+ ;; (+popup/close (get-buffer-window "*notmuch-hello*"))
+ (doom-kill-matching-buffers "^\\*notmuch")
+ (+workspace/delete "*MAIL*"))
+
+;;;###autoload
+(defun +notmuch/update ()
+ (interactive)
+ (start-process-shell-command
+ "notmuch update" nil
+ (pcase +notmuch-sync-backend
+ (`gmi
+ (concat "cd " +notmuch-mail-folder " && gmi push && gmi pull && notmuch new && afew -a -t"))
+ (`mbsync
+ "mbsync -a && notmuch new && afew -a -t")
+ (`mbsync-xdg
+ "mbsync -c \"$XDG_CONFIG_HOME\"/isync/mbsyncrc -a && notmuch new && afew -a -t")
+ (`offlineimap
+ "offlineimap && notmuch new && afew -a -t"))))
+
+;;;###autoload
+(defun +notmuch/search-delete ()
+ (interactive)
+ (notmuch-search-add-tag (list "+trash" "-inbox" "-unread"))
+ (notmuch-tree-next-message))
+
+;;;###autoload
+(defun +notmuch/tree-delete ()
+ (interactive)
+ (notmuch-tree-add-tag (list "+trash" "-inbox" "-unread"))
+ (notmuch-tree-next-message))
+
+;;;###autoload
+(defun +notmuch/search-spam ()
+ (interactive)
+ (notmuch-search-add-tag (list "+spam" "-inbox" "-unread"))
+ (notmuch-search-next-thread))
+
+;;;###autoload
+(defun +notmuch/tree-spam ()
+ (interactive)
+ (notmuch-tree-add-tag (list "+spam" "-inbox" "-unread"))
+ (notmuch-tree-next-message))
+
+;;;###autoload
+(defun +notmuch/open-message-with-mail-app-notmuch-tree ()
+ (interactive)
+ (let* ((msg-path (car (plist-get (notmuch-tree-get-message-properties) :filename)))
+ (temp (make-temp-file "notmuch-message-" nil ".eml")))
+ (shell-command-to-string (format "cp '%s' '%s'" msg-path temp))
+ (start-process-shell-command "email" nil (format "xdg-open '%s'" temp))))
+
+;;;###autoload
+(defun +notmuch/open-message-with-mail-app-notmuch-show ()
+ (interactive)
+ (let* ((msg-path (car (plist-get (notmuch-show-get-message-properties) :filename)))
+ (temp (make-temp-file "notmuch-message-" nil ".eml")))
+ (shell-command-to-string (format "cp '%s' '%s'" msg-path temp))
+ (start-process-shell-command "email" nil (format "xdg-open '%s'" temp))))
+
+
+;;
+;; Advice
+
+;;;###autoload
+(defun +notmuch*dont-confirm-on-kill-process (orig-fn &rest args)
+ "Don't prompt for confirmation when killing notmuch sentinel."
+ (let (confirm-kill-processes)
+ (apply orig-fn args)))
+
+;; (defun +notmuch*hello-insert-searches (title query-list &rest options)
+;; (widget-insert (propertize title 'face 'org-agenda-structure))
+;; (if (and notmuch-hello-first-run (plist-get options :initially-hidden))
+;; (add-to-list 'notmuch-hello-hidden-sections title))
+;; (let ((is-hidden (member title notmuch-hello-hidden-sections))
+;; (widget-push-button-prefix "")
+;; (widget-push-button-suffix "")
+;; (start (point)))
+;; (if is-hidden
+;; (widget-create 'push-button
+;; :notify `(lambda (widget &rest ignore)
+;; (setq notmuch-hello-hidden-sections
+;; (delete ,title notmuch-hello-hidden-sections))
+;; (notmuch-hello-update))
+;; (propertize " +" 'face 'org-agenda-structure))
+;; (widget-create 'push-button
+;; :notify `(lambda (widget &rest ignore)
+;; (add-to-list 'notmuch-hello-hidden-sections
+;; ,title)
+;; (notmuch-hello-update))
+;; " -"))
+;; (widget-insert "\n")
+;; (when (not is-hidden)
+;; (let ((searches (apply 'notmuch-hello-query-counts query-list options)))
+;; (when (or (not (plist-get options :hide-if-empty))
+;; searches)
+;; (widget-insert "\n")
+;; (notmuch-hello-insert-buttons searches)
+;; (indent-rigidly start (point) notmuch-hello-indent))))))
+
+;; (defun +notmuch*hello-insert-saved-searches ()
+;; "Insert the saved-searches section."
+;; (let ((searches (notmuch-hello-query-counts
+;; (if notmuch-saved-search-sort-function
+;; (funcall notmuch-saved-search-sort-function
+;; notmuch-saved-searches)
+;; notmuch-saved-searches)
+;; :show-empty-searches notmuch-show-empty-saved-searches)))
+;; (when searches
+;; (widget-insert (propertize "Notmuch" 'face 'org-agenda-date-today))
+;; (widget-insert "\n\n")
+;; (widget-insert (propertize "Saved searches" 'face 'org-agenda-structure))
+;; (widget-insert "\n\n")
+;; (let ((start (point)))
+;; (notmuch-hello-insert-buttons searches)
+;; (indent-rigidly start (point) notmuch-hello-indent)))))
+
+;; (defun +notmuch*hello-insert-buttons (searches)
+;; (let* ((widest (notmuch-hello-longest-label searches))
+;; (tags-and-width (notmuch-hello-tags-per-line widest))
+;; (tags-per-line (car tags-and-width))
+;; (column-width (cdr tags-and-width))
+;; (column-indent 0)
+;; (count 0)
+;; (reordered-list (notmuch-hello-reflect searches tags-per-line))
+;; ;; Hack the display of the buttons used.
+;; (widget-push-button-prefix "")
+;; (widget-push-button-suffix ""))
+;; ;; dme: It feels as though there should be a better way to
+;; ;; implement this loop than using an incrementing counter.
+;; (mapc (lambda (elem)
+;; ;; (not elem) indicates an empty slot in the matrix.
+;; (when elem
+;; (if (> column-indent 0)
+;; (widget-insert (make-string column-indent ? )))
+;; (let* ((name (plist-get elem :name))
+;; (query (plist-get elem :query))
+;; (oldest-first (case (plist-get elem :sort-order)
+;; (newest-first nil)
+;; (oldest-first t)
+;; (otherwise notmuch-search-oldest-first)))
+;; (search-type (eq (plist-get elem :search-type) 'tree))
+;; (msg-count (plist-get elem :count)))
+;; (widget-insert (format "\n%5s "
+;; (notmuch-hello-nice-number msg-count)))
+;; (widget-create 'push-button
+;; :notify #'notmuch-hello-widget-search
+;; :notmuch-search-terms query
+;; :notmuch-search-oldest-first oldest-first
+;; :notmuch-search-type search-type
+;; name)
+;; (setq column-indent
+;; (1+ (max 0 (- column-width (length name)))))))
+;; (setq count (1+ count))
+;; (when (eq (% count tags-per-line) 0)
+;; (setq column-indent 0)
+;; (widget-insert "\n")))
+;; reordered-list)
+
+;; ;; If the last line was not full (and hence did not include a
+;; ;; carriage return), insert one now.
+;; (unless (eq (% count tags-per-line) 0)
+;; (widget-insert "\n"))))
diff --git a/modules/app/notmuch/config.el b/modules/app/notmuch/config.el
new file mode 100644
index 000000000..c9e6f47a8
--- /dev/null
+++ b/modules/app/notmuch/config.el
@@ -0,0 +1,74 @@
+;;; app/notmuch/config.el -*- lexical-binding: t; -*-
+
+;; FIXME This module is a WIP!
+
+(defvar +notmuch-sync-backend 'gmi
+ "Which backend to use. Can be either gmi, mbsync, offlineimap or nil (manual).")
+
+(defvar +notmuch-mail-folder "~/.mail/account.gmail"
+ "Where your email folder is located (for use with gmailieer).")
+
+(after! notmuch
+ (set-company-backend! 'notmuch-message-mode
+ '(notmuch-company (company-ispell :with company-yasnippet)))
+
+ (set-popup-rule! "^\\*notmuch-hello" :side 'left :size 30 :ttl 0)
+
+ (setq notmuch-fcc-dirs nil
+ notmuch-show-logo nil
+ notmuch-message-headers-visible nil
+ message-kill-buffer-on-exit t
+ message-send-mail-function 'message-send-mail-with-sendmail
+ notmuch-search-oldest-first nil
+ send-mail-function 'sendmail-send-it
+ ;; sendmail-program "/usr/local/bin/msmtp"
+ notmuch-search-result-format
+ '(("date" . "%12s ")
+ ("count" . "%-7s ")
+ ("authors" . "%-30s ")
+ ("subject" . "%-72s ")
+ ("tags" . "(%s)"))
+ notmuch-tag-formats
+ '(("unread" (propertize tag 'face 'notmuch-tag-unread)))
+ notmuch-hello-sections
+ '(notmuch-hello-insert-saved-searches
+ notmuch-hello-insert-alltags)
+ notmuch-saved-searches
+ '((:name "inbox" :query "tag:inbox not tag:trash" :key "i")
+ (:name "flagged" :query "tag:flagged" :key "f")
+ (:name "sent" :query "tag:sent" :key "s")
+ (:name "drafts" :query "tag:draft" :key "d"))
+ notmuch-archive-tags '("-inbox" "-unread"))
+
+ ;; (setq-hook! 'notmuch-show-mode-hook line-spacing 0)
+
+ (add-to-list 'doom-real-buffer-functions #'notmuch-interesting-buffer nil #'eq)
+
+ (advice-add #'notmuch-start-notmuch-sentinel :around #'+notmuch*dont-confirm-on-kill-process)
+
+ ;; Visual enhancements
+ (defun +notmuch|center-window ()
+ (setq-local visual-fill-column-width 90)
+ (visual-fill-column-mode))
+ (add-hook 'notmuch-show-mode-hook #'+notmuch|center-window)
+
+ ;; modeline doesn't have much use in these modes
+ (add-hook! (notmuch-show-mode notmuch-tree-mode notmuch-search-mode)
+ #'hide-mode-line-mode))
+
+
+(def-package! org-mime
+ :after (org notmuch)
+ :config (setq org-mime-library 'mml))
+
+
+(def-package! counsel-notmuch
+ :when (featurep! :completion ivy)
+ :commands counsel-notmuch
+ :after notmuch)
+
+(def-package! helm-notmuch
+ :when (featurep! :completion helm)
+ :commands helm-notmuch
+ :after notmuch)
+
diff --git a/modules/app/notmuch/packages.el b/modules/app/notmuch/packages.el
new file mode 100644
index 000000000..7caea8546
--- /dev/null
+++ b/modules/app/notmuch/packages.el
@@ -0,0 +1,9 @@
+;; -*- no-byte-compile: t; -*-
+;;; app/notmuch/packages.el
+
+(package! notmuch)
+(package! org-mime)
+(when (featurep! :completion ivy)
+ (package! counsel-notmuch))
+(when (featurep! :completion helm)
+ (package! helm-notmuch))
diff --git a/modules/app/regex/autoload/regex.el b/modules/app/regex/autoload/regex.el
index 7e43db77e..881200fd6 100644
--- a/modules/app/regex/autoload/regex.el
+++ b/modules/app/regex/autoload/regex.el
@@ -11,17 +11,17 @@
(defface +regex-match-0-face
`((t (:foreground "Black" :background ,(doom-color 'magenta) :bold t)))
"TODO"
- :group 'doom)
+ :group 'faces)
(defface +regex-match-1-face
`((t (:foreground "Black" :background ,(doom-color 'blue) :bold t)))
"TODO"
- :group 'doom)
+ :group 'faces)
(defface +regex-match-2-face
`((t (:foreground "Black" :background ,(doom-color 'green) :bold t)))
"TODO"
- :group 'doom)
+ :group 'faces)
(defvar +regex-faces
'(+regex-match-0-face +regex-match-1-face +regex-match-2-face)
@@ -33,8 +33,6 @@
(define-key map "\C-c\C-c" #'+regex-update-buffers)
(define-key map "\C-c\C-r" #'=regex/replace)
(define-key map "\C-c\C-k" #'+regex/quit)
- (define-key map [remap doom-kill-buffer] #'+regex/quit)
- (define-key map [remap doom/kill-this-buffer] #'+regex/quit)
(define-key map [remap kill-this-buffer] #'+regex/quit)
(define-key map [remap kill-buffer] #'+regex/quit)
map)
@@ -66,8 +64,8 @@
(switch-to-buffer +regex--text-buffer)
(with-current-buffer +regex--text-buffer
(insert +regex-dummy-text)))
- (doom-popup-buffer +regex--groups-buffer)
- (doom-popup-buffer +regex--expr-buffer)
+ (pop-to-buffer +regex--groups-buffer)
+ (pop-to-buffer +regex--expr-buffer)
(with-current-buffer +regex--expr-buffer
(conf-mode)
(rainbow-delimiters-mode +1)
diff --git a/modules/app/regex/config.el b/modules/app/regex/config.el
index cc073013b..f48488d01 100644
--- a/modules/app/regex/config.el
+++ b/modules/app/regex/config.el
@@ -46,7 +46,7 @@ http://regexr.com/foo.html?q=bar
https://mediatemple.net"
"TODO")
-(set! :popup
- '("*doom-regex*" :size 4 :select t :noesc t)
- '("*doom-regex-groups*" :align left :size 30 :noselect t :noesc t))
+(set-popup-rules!
+ '(("^\\*doom-regex\\*$" :size 4 :quit nil)
+ ("^\\*doom-regex-groups" :side 'left :size 28 :select nil :quit nil)))
diff --git a/modules/app/rss/autoload.el b/modules/app/rss/autoload.el
index ed9e10fbe..bee317f78 100644
--- a/modules/app/rss/autoload.el
+++ b/modules/app/rss/autoload.el
@@ -1,47 +1,18 @@
;;; app/rss/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
-(defun =rss ()
- "Activate (or switch to) `elfeed' in its workspace."
- (interactive)
- (call-interactively 'elfeed))
-
-;;;###autoload
-(defun +rss/quit ()
- (interactive)
- (doom-kill-matching-buffers "^\\*elfeed")
- (dolist (file +rss-elfeed-files)
- (when-let* ((buf (get-file-buffer (expand-file-name file +rss-org-dir))))
- (kill-buffer buf))))
-
-;;;###autoload
-(defun +rss|elfeed-wrap ()
- "Enhances an elfeed entry's readability by wrapping it to a width of
-`fill-column' and centering it with `visual-fill-column-mode'."
- (let ((inhibit-read-only t)
- (inhibit-modification-hooks t))
- (setq-local truncate-lines nil)
- (setq-local shr-width 85)
- (set-buffer-modified-p nil)))
+(defalias '=rss #'elfeed
+ "Activate (or switch to) `elfeed' in its workspace.")
;;;###autoload
(defun +rss/delete-pane ()
"Delete the *elfeed-entry* split pane."
(interactive)
- (let* ((buff (get-buffer "*elfeed-entry*"))
- (window (get-buffer-window buff)))
- (kill-buffer buff)
- (delete-window window)))
-
-;;;###autoload
-(defun +rss-popup-pane (buf)
- "Display BUF in a popup."
- (doom-popup-buffer buf
- '(:align +rss-split-direction
- :size 0.75
- :select t
- :autokill t
- :autoclose t)))
+ (let* ((buf (get-buffer "*elfeed-entry*"))
+ (window (get-buffer-window buf)))
+ (delete-window window)
+ (when (buffer-live-p buf)
+ (kill-buffer buf))))
;;;###autoload
(defun +rss/open (entry)
@@ -70,6 +41,48 @@
(forward-line -1)
(call-interactively '+rss/open)))
+
+;;
+;; Hooks
+
+;;;###autoload
+(defun +rss|elfeed-wrap ()
+ "Enhances an elfeed entry's readability by wrapping it to a width of
+`fill-column'."
+ (let ((inhibit-read-only t)
+ (inhibit-modification-hooks t))
+ (setq-local truncate-lines nil)
+ (setq-local shr-use-fonts nil)
+ (setq-local shr-width 85)
+ (set-buffer-modified-p nil)))
+
+;;;###autoload
+(defun +rss|cleanup ()
+ "Clean up after an elfeed session. Kills all elfeed and elfeed-org files."
+ (interactive)
+ ;; `delete-file-projectile-remove-from-cache' slows down `elfeed-db-compact'
+ ;; tremendously, so we disable the projectile cache:
+ (let (projectile-enable-caching)
+ (elfeed-db-compact))
+ (let ((buf (previous-buffer)))
+ (when (or (null buf) (not (doom-real-buffer-p buf)))
+ (switch-to-buffer (doom-fallback-buffer))))
+ (let ((search-buffers (doom-buffers-in-mode 'elfeed-search-mode))
+ (show-buffers (doom-buffers-in-mode 'elfeed-show-mode))
+ kill-buffer-query-functions)
+ (dolist (file +rss-elfeed-files)
+ (when-let* ((buf (get-file-buffer (expand-file-name file org-directory))))
+ (kill-buffer buf)))
+ (dolist (b search-buffers)
+ (with-current-buffer b
+ (remove-hook 'kill-buffer-hook #'+rss|cleanup :local)
+ (kill-buffer b)))
+ (mapc #'kill-buffer show-buffers)))
+
+
+;;
+;; Functions
+
;;;###autoload
(defun +rss-dead-feeds (&optional years)
"Return a list of feeds that haven't posted anything in YEARS."
@@ -84,3 +97,21 @@
(cl-loop for url in (elfeed-feed-list)
unless (gethash url living-feeds)
collect url)))
+
+;;;###autoload
+(defun +rss-put-sliced-image (spec alt &optional flags)
+ "TODO"
+ (cl-letf (((symbol-function #'insert-image)
+ (lambda (image &optional alt _area _slice)
+ (let ((height (cdr (image-size image t))))
+ (insert-sliced-image image alt nil (max 1 (/ height 20.0)) 1)))))
+ (shr-put-image spec alt flags)))
+
+;;;###autoload
+(defun +rss-render-image-tag-without-underline (dom &optional url)
+ "TODO"
+ (let ((start (point)))
+ (shr-tag-img dom url)
+ ;; And remove underlines in case images are links, otherwise we get an
+ ;; underline beneath every slice.
+ (put-text-property start (point) 'face '(:underline nil))))
diff --git a/modules/app/rss/config.el b/modules/app/rss/config.el
index 6cdce9a3c..59cadfc6c 100644
--- a/modules/app/rss/config.el
+++ b/modules/app/rss/config.el
@@ -4,16 +4,20 @@
;; by apps Reeder and Readkit. It can be invoked via `=rss'. Otherwise, if you
;; don't care for the UI you can invoke elfeed directly with `elfeed'.
-(defvar +rss-elfeed-files (list "rss/elfeed.org")
- "The files that configure `elfeed's rss feeds.")
+(defvar +rss-elfeed-files (list "elfeed.org")
+ "Where to look for elfeed.org files, relative to `org-directory'. Can be
+absolute paths.")
(defvar +rss-split-direction 'below
"What direction to pop up the entry buffer in elfeed.")
+(defvar +rss-enable-sliced-images t
+ "Automatically slice images shown in elfeed-show-mode buffers, making them
+easier to scroll through.")
+
;;
;; Packages
-;;
(def-package! elfeed
:commands elfeed
@@ -21,44 +25,50 @@
(setq elfeed-search-filter "@2-week-ago "
elfeed-db-directory (concat doom-local-dir "elfeed/db/")
elfeed-enclosure-default-dir (concat doom-local-dir "elfeed/enclosures/")
- elfeed-show-entry-switch #'+rss-popup-pane
+ elfeed-show-entry-switch #'pop-to-buffer
elfeed-show-entry-delete #'+rss/delete-pane
- shr-max-image-proportion 0.6)
+ shr-max-image-proportion 0.8)
+
+ (set-popup-rule! "^\\*elfeed-entry"
+ :size 0.75 :actions '(display-buffer-below-selected)
+ :select t :quit nil :ttl t)
(make-directory elfeed-db-directory t)
;; Ensure elfeed buffers are treated as real
- (push (lambda (buf) (string-match-p "^\\*elfeed" (buffer-name buf)))
- doom-real-buffer-functions)
+ (defun +rss-buffer-p (buf)
+ (string-match-p "^\\*elfeed" (buffer-name buf)))
+ (add-to-list 'doom-real-buffer-functions #'+rss-buffer-p nil #'eq)
;; Enhance readability of a post
(add-hook 'elfeed-show-mode-hook #'+rss|elfeed-wrap)
+ (add-hook! 'elfeed-search-mode-hook
+ (add-hook 'kill-buffer-hook #'+rss|cleanup nil t))
- (map! (:map (elfeed-search-mode-map elfeed-show-mode-map)
- [remap doom/kill-this-buffer] "q"
- [remap kill-this-buffer] "q"
- [remap kill-buffer] "q")
+ ;; Large images are annoying to scroll through, because scrolling follows the
+ ;; cursor, so we force shr to insert images in slices.
+ (when +rss-enable-sliced-images
+ (setq-hook! 'elfeed-show-mode-hook
+ shr-put-image-function #'+rss-put-sliced-image
+ shr-external-rendering-functions '((img . +rss-render-image-tag-without-underline))))
- (:map elfeed-search-mode-map
- :n "q" #'+rss/quit
- :n "r" #'elfeed-update
- :n "s" #'elfeed-search-live-filter
- :n "RET" #'elfeed-search-show-entry)
-
- (:map elfeed-show-mode-map
- :n "q" #'elfeed-kill-buffer
- :m "j" #'evil-next-visual-line
- :m "k" #'evil-previous-visual-line
- [remap doom/next-buffer] #'+rss/next
- [remap doom/previous-buffer] #'+rss/previous
- [remap next-buffer] #'+rss/next
- [remap previous-buffer] #'+rss/previous)))
+ ;; Keybindings
+ (after! elfeed-show
+ (define-key! elfeed-show-mode-map
+ [remap next-buffer] #'+rss/next
+ [remap previous-buffer] #'+rss/previous))
+ (when (featurep! :feature evil +everywhere)
+ (evil-define-key 'normal elfeed-search-mode-map
+ "q" #'elfeed-kill-buffer
+ "r" #'elfeed-search-update--force
+ (kbd "M-RET") #'elfeed-search-browse-url)))
(def-package! elfeed-org
- :after (:all org elfeed)
+ :when (featurep! +org)
+ :after elfeed
:config
(setq rmh-elfeed-org-files
- (let ((default-directory +org-dir))
+ (let ((default-directory org-directory))
(mapcar #'expand-file-name +rss-elfeed-files)))
(elfeed-org))
diff --git a/modules/app/twitter/autoload.el b/modules/app/twitter/autoload.el
index f845154b3..274bd02fd 100644
--- a/modules/app/twitter/autoload.el
+++ b/modules/app/twitter/autoload.el
@@ -1,12 +1,39 @@
;;; app/twitter/autoload.el -*- lexical-binding: t; -*-
+(defvar +twitter-workspace-name "*Twitter*"
+ "The name to use for the twitter workspace.")
+
;;;###autoload
-(defun =twitter ()
- (interactive)
- (+workspace-switch "*Twitter*" t)
- (delete-other-windows)
- (condition-case _ex
+(defun +twitter-display-buffer (buf)
+ "A replacement display-buffer command for `twittering-pop-to-buffer-function'
+that works with the feature/popup module."
+ (let ((win (selected-window)))
+ (display-buffer buf)
+ ;; This is required because the new window generated by `pop-to-buffer'
+ ;; may hide the region following the current position.
+ (twittering-ensure-whole-of-status-is-visible win)))
+
+;;;###autoload
+(defun +twitter-buffer-p (buf)
+ "Return non-nil if BUF is a `twittering-mode' buffer."
+ (eq 'twittering-mode (buffer-local-value 'major-mode buf)))
+
+
+;;
+;; Commands
+
+(defvar +twitter--old-wconf nil)
+;;;###autoload
+(defun =twitter (arg)
+ "Opens a workspace dedicated to `twittering-mode'."
+ (interactive "P")
+ (condition-case _
(progn
+ (if (and (not arg) (featurep! :feature workspaces))
+ (+workspace/new +twitter-workspace-name)
+ (setq +twitter--old-wconf (current-window-configuration))
+ (delete-other-windows)
+ (switch-to-buffer (doom-fallback-buffer)))
(call-interactively #'twit)
(unless (get-buffer (car twittering-initial-timeline-spec-string))
(error "Failed to open twitter"))
@@ -14,28 +41,62 @@
(dolist (name (cdr twittering-initial-timeline-spec-string))
(split-window-horizontally)
(switch-to-buffer name))
- (balance-windows))
- ('error
- (+twitter/quit-all))))
+ (balance-windows)
+ (call-interactively #'+twitter/rerender-all))
+ ('error (+twitter/quit-all))))
;;;###autoload
(defun +twitter/quit ()
+ "Close the current `twitter-mode' buffer."
(interactive)
(when (eq major-mode 'twittering-mode)
(twittering-kill-buffer)
- (+workspace/close-window-or-workspace)))
+ (cond ((one-window-p) (+twitter/quit-all))
+ ((featurep! :feature workspaces)
+ (+workspace/close-window-or-workspace))
+ ((delete-window)))))
;;;###autoload
(defun +twitter/quit-all ()
+ "Close all open `twitter-mode' buffers and the associated workspace, if any."
(interactive)
- (+workspace/delete "Twitter")
- (dolist (buf (doom-buffers-in-mode 'twittering-mode))
- (with-current-buffer buf
- (twittering-kill-buffer))))
+ (when (featurep! :feature workspaces)
+ (+workspace/delete +twitter-workspace-name))
+ (when +twitter--old-wconf
+ (set-window-configuration +twitter--old-wconf)
+ (setq +twitter--old-wconf nil))
+ (dolist (buf (doom-buffers-in-mode 'twittering-mode (buffer-list) t))
+ (twittering-kill-buffer buf)))
;;;###autoload
(defun +twitter/rerender-all ()
+ "Rerender all `twittering-mode' buffers."
(interactive)
- (dolist (buf (doom-buffers-in-mode 'twittering-mode))
+ (dolist (buf (doom-buffers-in-mode 'twittering-mode (buffer-list) t))
(with-current-buffer buf
- (twittering-rerender-timeline-all buf))))
+ (twittering-rerender-timeline-all buf)
+ (setq-local line-spacing 0.2)
+ (goto-char (point-min)))))
+
+;;;###autoload
+(defun +twitter/ace-link ()
+ "Open a visible link, username or hashtag in a `twittering-mode' buffer."
+ (interactive)
+ (let ((pt (avy-with +twitter/ace-link
+ (avy--process
+ (+twitter--collect-links)
+ (avy--style-fn avy-style)))))
+ (when (number-or-marker-p pt)
+ (goto-char pt)
+ (let ((uri (get-text-property (point) 'uri)))
+ (if uri (browse-url uri))))))
+
+(defun +twitter--collect-links ()
+ (let ((end (window-end))
+ points)
+ (save-excursion
+ (goto-char (window-start))
+ (while (and (< (point) end)
+ (ignore-errors (twittering-goto-next-thing) t))
+ (push (point) points))
+ (nreverse points))))
diff --git a/modules/app/twitter/config.el b/modules/app/twitter/config.el
index 23aef2356..bf6680ecd 100644
--- a/modules/app/twitter/config.el
+++ b/modules/app/twitter/config.el
@@ -3,34 +3,84 @@
(def-package! twittering-mode
:commands twit
:config
- (setq twittering-use-master-password t
- twittering-icon-mode nil
+ (setq twittering-private-info-file (expand-file-name "twittering-mode.gpg" doom-etc-dir)
+ twittering-use-master-password t
+ twittering-request-confirmation-on-posting t
+ ;; twittering-icon-mode t
;; twittering-use-icon-storage t
;; twittering-icon-storage-file (concat doom-cache-dir "twittering-mode-icons.gz")
;; twittering-convert-fix-size 12
twittering-timeline-header ""
twittering-timeline-footer ""
twittering-edit-skeleton 'inherit-any
- twittering-status-format
- "%RT{%FACE[bold]{RT }}%S (%FACE[bold]{@%s}), %@%r%R:\n%FOLD[ ]{%t %QT{\n+----\n%FOLD[|]{ %S (@%s), %@:\n%FOLD[ ]{%t}}\n+----}}\n "
+ twittering-status-format "%FACE[font-lock-function-name-face]{ @%s} %FACE[italic]{%@} %FACE[error]{%FIELD-IF-NONZERO[❤ %d]{favorite_count}} %FACE[warning]{%FIELD-IF-NONZERO[↺ %d]{retweet_count}}
+%FOLD[ ]{%FILL{%t}%QT{
+%FOLD[ ]{%FACE[font-lock-function-name-face]{@%s}\t%FACE[shadow]{%@}
+%FOLD[ ]{%FILL{%t}}
+}}}
+
+%FACE[twitter-divider]{ }
+"
+ ;; twittering-timeline-spec-alias '()
twittering-initial-timeline-spec-string
'(":home" ":mentions" ":direct_messages"))
- (set! :popup "*twittering-edit*" :size 12 :select t)
+ (set-popup-rule! "^\\*twittering-edit" :size 15 :ttl nil :quit nil :select t)
- (add-hook! twittering-mode
- (setq header-line-format (or (doom-modeline 'twitter) mode-line-format)
+ (defface twitter-divider
+ '((((background dark)) (:underline (:color "#141519")))
+ (((background light)) (:underline (:color "#d3d3d3"))))
+ "The vertical divider between tweets."
+ :group 'twittering-mode)
+
+ (add-hook 'doom-real-buffer-functions #'+twitter-buffer-p)
+ (when (featurep! :ui popup)
+ (setq twittering-pop-to-buffer-function #'+twitter-display-buffer))
+
+ (after! solaire-mode
+ (add-hook 'twittering-mode-hook #'solaire-mode))
+
+ ;; Custom header-line for twitter buffers
+ (defun +twitter|switch-mode-and-header-line ()
+ (setq header-line-format mode-line-format
mode-line-format nil))
+ (add-hook 'twittering-mode-hook #'+twitter|switch-mode-and-header-line)
- (map! :map twittering-mode-map
- [remap twittering-kill-buffer] #'+twitter/quit
- "Q" #'+twitter/quit-all
- "o" #'ace-link-addr
- "j" #'evil-next-visual-line
- "k" #'evil-previous-visual-line
- "J" #'twittering-goto-next-status
- "K" #'twittering-goto-previous-status)
+ (cond ((featurep! :ui doom-modeline +new)
+ (setq-hook! 'twittering-mode-hook mode-line-format-right nil))
+ ((featurep! :ui doom-modeline)
+ (def-modeline! 'twitter
+ '(bar matches " %b " selection-info)
+ '())
+ (add-hook! 'twittering-mode-hook (doom-set-modeline 'twitter))))
- (def-modeline! twitter
- (bar matches " %b " selection-info)
- ()))
+ ;; `epa--decode-coding-string' isn't defined in later versions of Emacs 27
+ (unless (fboundp 'epa--decode-coding-string)
+ (defalias 'epa--decode-coding-string #'decode-coding-string))
+
+ (define-key! twittering-mode-map
+ "q" #'+twitter/quit
+ "Q" #'+twitter/quit-all
+ [remap twittering-kill-buffer] #'+twitter/quit
+ [remap delete-window] #'+twitter/quit
+ [remap +workspace/close-window-or-workspace] #'+twitter/quit)
+ (when (featurep! :feature evil +everywhere)
+ (define-key! twittering-mode-map
+ [remap evil-window-delete] #'+twitter/quit
+ "f" #'twittering-favorite
+ "F" #'twittering-unfavorite
+ "\C-f" #'twittering-follow
+ "\C-F" #'twittering-unfollow
+ "d" #'twittering-delete-status
+ "r" #'twittering-retweet
+ "R" #'twittering-toggle-or-retrieve-replied-statuses
+ "o" #'twittering-update-status-interactive
+ "O" #'+twitter/ace-link
+ "/" #'twittering-search
+ "J" #'twittering-goto-next-status
+ "K" #'twittering-goto-previous-status
+ "g" nil
+ "gg" #'twittering-goto-first-status
+ "G" #'twittering-goto-last-status
+ "gj" #'twittering-goto-next-status-of-user
+ "gk" #'twittering-goto-previous-status-of-user)))
diff --git a/modules/app/write/README.org b/modules/app/write/README.org
new file mode 100644
index 000000000..a61493ab6
--- /dev/null
+++ b/modules/app/write/README.org
@@ -0,0 +1,113 @@
+#+TITLE: :app write
+
+Adds word processing tools and the ~+write-mode~ minor mode, which converts
+Emacs into a more comfortable writing environment.
+
+* Table of Contents :TOC:
+- [[Features][Features]]
+ - [[~M-x +write-mode~][~M-x +write-mode~]]
+ - [[Language Tool ~+langtool~][Language Tool ~+langtool~]]
+ - [[Wordnut ~+wordnut~][Wordnut ~+wordnut~]]
+ - [[Synosaurus][Synosaurus]]
+- [[Prerequisites][Prerequisites]]
+ - [[Language Tool][Language Tool]]
+ - [[Wordnut][Wordnut]]
+- [[Configuration][Configuration]]
+ - [[mixed-pitch-mode][mixed-pitch-mode]]
+- [[Appendix][Appendix]]
+ - [[Minor modes][Minor modes]]
+ - [[Commands][Commands]]
+
+* Features
+This module provides two module flags:
+
+- ~+langtool~ Enables language tool integration.
+- ~+wordnut~ Enables wordnet integration.
+
+** ~M-x +write-mode~
+Write mode makes Emacs a more comfortable writing environment by:
+
+- Centering the buffer (with ~visual-fill-column-mode~), ala distraction-free
+ mode from other text editors.
+- Soft-wrapping long text lines with ~visual-line-mode~.
+- Enabling ~mixed-pitch-mode~, allowing fixed-width and variable-pitch fonts to
+ co-exist in one buffer. For example, a monospace font for SRC blocks and Arial
+ for everything else.
+- In org-mode:
+ - Turns on ~org-indent-mode~
+ - Turns on ~+org-pretty-mode~
+
+** Language Tool ~+langtool~
+[[https://www.languagetool.org/][Language Tool]] is a polyglot proofreader service that checks for grammar and
+stylistic issues in your writing. This requires Java 1.8+.
+
+#+begin_quote
+This requires Java 1.8+
+#+end_quote
+
+*** Commands
+- ~langtool-check~
+- ~langtool-correct-buffer~
+
+** Wordnut ~+wordnut~
+Wordnut provides a searchable dictionary frontend for Emacs. This requires
+~wordnet~, which should be available in your OS's package manager.
+
+*** Commands
+- ~wordnut-search~
+- ~wordnut-lookup-curent-word~
+
+** Synosaurus
+Synosaurus provides a service for looking up synonyms. It requires an internet
+connection.
+
+*** Commands
+- ~synosaurus-lookup~
+- ~synosaurus-choose-and-replace~
+
+* Prerequisites
+** Language Tool
+Either download and deploy it from https://languagetool.org/ or install it
+through your OS package manager:
+
+#+BEGIN_SRC sh
+# MacOS/Homebrew users:
+brew install languagetool
+
+# Arch Linux users:
+sudo pacman -S languagetool
+#+END_SRC
+
+This module tries to guess the location of languagetool-commandline.jar. If you
+get a warning that Doom =couldn't find languagetool-commandline.jar=, you will
+need to find langaugetool-commandline.jar and set ~langtool-language-tool-jar~
+to its path.
+
+** Wordnut
+This requires =wordnet= to be installed, which should be available through your
+OS package manager:
+
+#+BEGIN_SRC sh
+# MacOS/Homebrew users:
+brew install wordnet
+
+# Arch Linux users:
+sudo pacaur -S wordnet # on the AUR
+#+END_SRC
+
+* Configuration
+** mixed-pitch-mode
+To configure which faces are displayed with fixed-pitch fonts in
+~mixed-pitch-mode~, look into ~mixed-pitch-fixed-pitch-faces~.
+
+* Appendix
+** Minor modes
+- ~+write-mode~
+- ~mixed-pitch-mode~
+** Commands
+- ~langtool-check~
+- ~langtool-correct-buffer~
+- ~synosaurus-choose-and-replace~
+- ~synosaurus-lookup~
+- ~wordnut-lookup-curent-word~
+- ~wordnut-search~
diff --git a/modules/app/write/autoload.el b/modules/app/write/autoload.el
index 6b370c667..8a2f21044 100644
--- a/modules/app/write/autoload.el
+++ b/modules/app/write/autoload.el
@@ -1,18 +1,43 @@
;;; app/write/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
-(define-minor-mode +write-mode
- "TODO"
- :init-value nil
- :keymap nil
- (let ((arg (if +write-mode +1 -1))
- (iarg (if +write-mode -1 +1)))
- (text-scale-set (if +write-mode 2 0))
- (doom/toggle-line-numbers iarg)
- (setq-local visual-fill-column-center-text +write-mode)
- (visual-fill-column-mode arg)
- (visual-line-mode arg)
- (when (eq major-mode 'org-mode)
- (+org-pretty-mode arg))
- (setq line-spacing (if +write-mode 4))))
+(defvar +write-mode-map (make-sparse-keymap)
+ "TODO")
+;;;###autoload
+(define-minor-mode +write-mode
+ "Turns Emacs into a more comfortable writing environment and word processor."
+ :init-value nil
+ :keymap +write-mode-map
+ (setq-local visual-fill-column-center-text t)
+ (when +write-text-scale
+ (text-scale-set (if +write-mode 2 0)))
+ (when +write-line-spacing
+ (setq-local line-spacing +write-line-spacing)))
+
+;;;###autoload
+(defun +write|init-org-mode ()
+ "Initializes `org-mode' specific settings for `+write-mode'."
+ (when (eq major-mode 'org-mode)
+ (+org-pretty-mode (if +write-mode +1 -1))))
+
+;;;###autoload
+(defun +write|init-line-numbers ()
+ (display-line-numbers-mode (if +write-mode +1 -1)))
+
+;;;###autoload
+(defun +write|init-mixed-pitch ()
+ (mixed-pitch-mode (if +write-mode +1 -1)))
+
+;;;###autoload
+(defun +write|init-visual-fill-column ()
+ (visual-fill-column-mode (if +write-mode +1 -1)))
+
+;;;###autoload
+(add-hook! '+write-mode-hook
+ #'(flyspell-mode
+ visual-line-mode
+ +write|init-mixed-pitch
+ +write|init-visual-fill-column
+ +write|init-line-numbers
+ +write|init-org-mode))
diff --git a/modules/app/write/config.el b/modules/app/write/config.el
new file mode 100644
index 000000000..e82a0137c
--- /dev/null
+++ b/modules/app/write/config.el
@@ -0,0 +1,56 @@
+;;; app/write/config.el -*- lexical-binding: t; -*-
+
+(defvar +write-text-scale nil
+ "What to scale the text up to in `+write-mode'. Uses `text-scale-set'.")
+
+(defvar +write-line-spacing nil
+ "What to set `line-spacing' in `+write-mode'.")
+
+;;
+;; Packages
+
+(def-package! langtool
+ :when (featurep! +langtool)
+ :commands (langtool-check
+ langtool-check-done
+ langtool-show-message-at-point
+ langtool-correct-buffer)
+ :init (setq langtool-default-language "en-US")
+ :config
+ (unless langtool-language-tool-jar
+ (setq langtool-language-tool-jar
+ (cond (IS-MAC
+ (locate-file "libexec/languagetool-commandline.jar"
+ (doom-files-in "/usr/local/Cellar/languagetool"
+ :type 'dirs
+ :depth 1)))
+ (IS-LINUX
+ "/usr/share/java/languagetool/languagetool-commandline.jar")))))
+
+
+;; `synosaurus'
+(setq synosaurus-choose-method 'default)
+
+
+;; `mixed-pitch'
+(after! mixed-pitch
+ (setq mixed-pitch-fixed-pitch-faces
+ (append mixed-pitch-fixed-pitch-faces
+ '(org-todo-keyword-todo
+ org-todo-keyword-habt
+ org-todo-keyword-done
+ org-todo-keyword-wait
+ org-todo-keyword-kill
+ org-todo-keyword-outd
+ org-todo
+ org-indent
+ line-number
+ line-number-current-line
+ org-special-keyword
+ org-date
+ org-property-value
+ org-special-keyword
+ org-property-value
+ org-ref-cite-face
+ org-tag
+ font-lock-comment-face))))
diff --git a/modules/app/write/doctor.el b/modules/app/write/doctor.el
new file mode 100644
index 000000000..655adea7d
--- /dev/null
+++ b/modules/app/write/doctor.el
@@ -0,0 +1,7 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; app/write/doctor.el
+
+(when (featurep! +langtool)
+ (require 'langtool)
+ (unless (file-exists-p langtool-language-tool-jar)
+ (warn! "Couldn't find languagetool-commandline.jar")))
diff --git a/modules/app/write/packages.el b/modules/app/write/packages.el
new file mode 100644
index 000000000..fa7c69667
--- /dev/null
+++ b/modules/app/write/packages.el
@@ -0,0 +1,11 @@
+;; -*- no-byte-compile: t; -*-
+;;; app/write/packages.el
+
+(package! synosaurus)
+(package! mixed-pitch)
+
+(when (featurep! +langtool)
+ (package! langtool))
+(when (featurep! +wordnut)
+ (package! wordnut))
+
diff --git a/modules/collab/floobits/packages.el b/modules/collab/floobits/packages.el
new file mode 100644
index 000000000..98eb4d29b
--- /dev/null
+++ b/modules/collab/floobits/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; collab/foobits/packages.el
+
+(package! floobits)
diff --git a/modules/tools/impatient-mode/autoload.el b/modules/collab/impatient-mode/autoload.el
similarity index 86%
rename from modules/tools/impatient-mode/autoload.el
rename to modules/collab/impatient-mode/autoload.el
index 7973e0178..41047d359 100644
--- a/modules/tools/impatient-mode/autoload.el
+++ b/modules/collab/impatient-mode/autoload.el
@@ -1,10 +1,9 @@
-;;; tools/impatient-mode/autoload.el -*- lexical-binding: t; -*-
+;;; collab/impatient-mode/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
(defun +impatient-mode/toggle ()
- "TODO"
+ "Toggle `impatient-mode' in the current buffer."
(interactive)
- (require 'simple-httpd)
(unless (process-status "httpd")
(httpd-start))
(impatient-mode)
diff --git a/modules/tools/impatient-mode/packages.el b/modules/collab/impatient-mode/packages.el
similarity index 66%
rename from modules/tools/impatient-mode/packages.el
rename to modules/collab/impatient-mode/packages.el
index 002eb1734..2469c675e 100644
--- a/modules/tools/impatient-mode/packages.el
+++ b/modules/collab/impatient-mode/packages.el
@@ -1,5 +1,5 @@
;; -*- no-byte-compile: t; -*-
-;;; tools/impatient-mode/packages.el
+;;; collab/impatient-mode/packages.el
(package! htmlize)
(package! impatient-mode)
diff --git a/modules/completion/company/README.org b/modules/completion/company/README.org
index f2a1bc6fc..a3d97b899 100644
--- a/modules/completion/company/README.org
+++ b/modules/completion/company/README.org
@@ -1,48 +1,150 @@
-#+TITLE: :completion company
+#+TITLE: completion/company
+#+DATE: February 19, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
-This module adds code-completion support, powered by [[https://github.com/company-mode/company-mode][company]].
+* Table of Contents :TOC_3:noexport:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Prerequisites][Prerequisites]]
+- [[Features][Features]]
+ - [[Code completion][Code completion]]
+ - [[Vim-esque omni-completion prefix (C-x)][Vim-esque omni-completion prefix (C-x)]]
+- [[Configuration][Configuration]]
+ - [[Enable as-you-type code completion][Enable as-you-type code completion]]
+ - [[Enable company backend(s) in certain modes][Enable company backend(s) in certain modes]]
+- [[Troubleshooting][Troubleshooting]]
+ - [[Code-completion doesn't pop up automatically.][Code-completion doesn't pop up automatically.]]
+ - [[X-mode doesn't have code completion support or requires extra setup.][X-mode doesn't have code completion support or requires extra setup.]]
+ - [[No backends (or the incorrect ones) have been registered for X-mode.][No backends (or the incorrect ones) have been registered for X-mode.]]
-+ Uses ~company-quickhelp~ for documentation tooltips
-+ Uses ~company-statistics~ to order results by usage frequency
+* Description
+This module provides code completion, powered by [[https://github.com/company-mode/company-mode][company-mode]]. It is required
+for code completion in many of Doom's :lang modules.
-[[/../screenshots/company.png]]
+https://assets.doomemacs.org/completion/company/overlay.png
-* Table of Contents :TOC:
-- [[#install][Install]]
-- [[#configure][Configure]]
- - [[#auto-completion][Auto-completion]]
-- [[#troubleshooting][Troubleshooting]]
+** Module Flags
++ =+auto= Enables as-you-type completion.
++ =+childframe= Enables displaying completion candidates in a child frame,
+ rather than an overlay or tooltip (among with other UI enhancements). *This
+ requires GUI Emacs 26.1+.*
++ =+tng= Enables completion using only ~TAB~. Pressing ~TAB~ will select the
+ next completion suggestion, while ~S-TAB~ will select the previous one.
-* Install
-Some languages require additional setup, and some languages may have no
-completion support at all.
+** Plugins
++ [[https://github.com/company-mode/company-mode][company-mode]]
++ [[https://github.com/hlissner/emacs-company-dict][company-dict]]
++ [[https://github.com/raxod502/prescient.el][company-prescient]]
++ [[https://github.com/sebastiencs/company-box][company-box]]
-Check the README.org in that language's module for details.
+* Prerequisites
+This module has no direct prerequisites.
-* Configure
-** Auto-completion
-By default, I've disabled auto-completion. This is my preference. I prefer to
-invoke company when I need it by calling ~company-complete~ manually (typically,
-bound to =C-SPC= in insert mode). However, some may not share my preference.
+However, some major modes may require additional setup for code completion to
+work in them. Some major modes may have no completion support at all. Check that
+major mode's module's documentation for details.
-To enable auto-completion you must:
+* Features
+** Code completion
+Ccompletion must be triggered manually with the =C-SPC= key. If you want
+as-you-type code completion, the ~+auto~ module flag will enable it.
-1. Load ~company~,
-2. and change ~company-idle-delay~ to a non-nil float (the default is 0.5)
+| Keybind | Description |
+|---------+------------------------------------------|
+| =C-SPC= | Invoke code completion manually |
+| =C-n= | Go to next candidate |
+| =C-p= | Go to previous candidate |
+| =C-j= | (evil) Go to next candidate |
+| =C-k= | (evil) Go to previous candidate |
+| =C-h= | Display documentation (if available) |
+| =C-u= | Move to previous page of candidates |
+| =C-d= | Move to next page of candidates |
+| =C-s= | Filter candidates |
+| =C-S-s= | Search candidates with helm/ivy |
+| =C-SPC= | Complete common |
+| =TAB= | Complete common or select next candidate |
+| =S-TAB= | Select previous candidate |
-For example:
+** Vim-esque omni-completion prefix (C-x)
+In the spirit of Vim's omni-completion, the following insert mode keybinds are
+available to evil users to access specific company backends:
+
+| Keybind | Description |
+|-----------+-----------------------------------|
+| =C-x C-]= | Complete etags |
+| =C-x C-f= | Complete file path |
+| =C-x C-k= | Complete from dictionary/keyword |
+| =C-x C-l= | Complete full line |
+| =C-x C-o= | Invoke complete-at-point function |
+| =C-x C-n= | Complete next symbol at point |
+| =C-x C-p= | Complete previous symbol at point |
+| =C-x C-s= | Complete snippet |
+| =C-x s= | Complete spelling suggestions |
+
+* Configuration
+** Enable as-you-type code completion
+The =+auto= module flag enables this. You may customize ~company-idle-delay~ to
+control how quickly the popup should appear.
+
+The ~+company/toggle-auto-completion~ command is also available to toggle this
+interactively.
+
+** Enable company backend(s) in certain modes
+The ~set-company-backend!~ function exists for setting ~company-backends~
+buffer-locally in MODES, which is either a major-mode symbol, a minor-mode
+symbol, or a list of either. BACKENDS are prepended to ~company-backends~ for
+those modes.
#+BEGIN_SRC emacs-lisp
-(require 'company)
-(setq company-idle-delay 0.2
- company-minimum-prefix-length 3)
+(after! js2-mode
+ (set-company-backend! 'js2-mode 'company-tide 'company-yasnippet))
+
+(after! sh-script
+ (set-company-backend! 'sh-mode
+ '(company-shell :with company-yasnippet)))
+
+(after! cc-mode
+ (set-company-backend! 'c-mode
+ '(:separate company-irony-c-headers company-irony)))
+#+END_SRC
+
+To unset the backends for a particular mode, pass ~nil~ to it:
+
+#+BEGIN_SRC emacs-lisp
+(after! sh-script
+ (set-company-backend! 'sh-mode nil))
#+END_SRC
* Troubleshooting
-If completion isn't working for you, please consider the following before
-posting a bug report:
+If code completion isn't working for you, consider the following common causes
+before you file a bug report:
-+ If what you are expecting is popup-as-you-type completion (which is disabled
- by default), see the "Configure > Auto-completion" section above, which will
- instruct you on how to enable this.
-+ Some languages don't have any auto-completion support at all.
+** Code-completion doesn't pop up automatically.
+This is by design. The expectation is that you invoke completion manually with
+=C-SPC=. This was decided because code-completion backends can be slow, some
+dreadfully so, and invoking them every time you move your cursor can add pauses
+and delays while editing.
+
+If, despite that, you still want this functionality, use the =+auto= flag to
+enable it.
+
+** X-mode doesn't have code completion support or requires extra setup.
+There is no guarantee your language mode will have completion support.
+
+Some, like ~lua-mode~, don't have completion support in Emacs at all. Others may
+requires additional setup to get code completion working. For instance,
+~go-mode~ requires ~guru~ to be installed on your system, and ~enh-ruby-mode~
+requires that you have a Robe server running (~M-x robe-start~).
+
+Check the relevant module's documentation for this kind of information.
+
+** No backends (or the incorrect ones) have been registered for X-mode.
+Doom expects every mode to have an explicit list of company-backends (and as
+short a list as possible). This may mean you aren't getting all the completion
+you want or any at all.
+
+Check the value of ~company-backends~ (=SPC h v company-backends=) from that
+mode to see what backends are available. Check the [[*Assigning company backend(s) to modes][Configuration section]] for
+details on changing what backends are available for that mode.
diff --git a/modules/completion/company/autoload.el b/modules/completion/company/autoload.el
index a235864e0..7d3177a54 100644
--- a/modules/completion/company/autoload.el
+++ b/modules/completion/company/autoload.el
@@ -1,14 +1,123 @@
;;; completion/company/autoload.el -*- lexical-binding: t; -*-
+;;;###autoload
+(defvar +company-backend-alist
+ '((text-mode :derived (company-dabbrev company-yasnippet company-ispell))
+ (prog-mode :derived (:separate company-capf company-yasnippet))
+ (conf-mode :derived company-capf company-dabbrev-code company-yasnippet))
+ "An alist matching modes to company backends. The backends for any mode is
+built from this.")
+
+;;;###autodef
+(defun set-company-backend! (modes &rest backends)
+ "Prepends BACKENDS (in order) to `company-backends' in MODES.
+
+MODES should be one symbol or a list of them, representing major or minor modes.
+This will overwrite backends for MODES on consecutive uses.
+
+If the car of BACKENDS is nil, unset the backends for MODES.
+
+Examples:
+
+ (set-company-backend! 'js2-mode
+ 'company-tide 'company-yasnippet)
+
+ (set-company-backend! 'sh-mode
+ '(company-shell :with company-yasnippet))
+
+ (set-company-backend! '(c-mode c++-mode)
+ '(:separate company-irony-c-headers company-irony))
+
+ (set-company-backend! 'sh-mode nil) ; unsets backends for sh-mode
+
+To have BACKENDS apply to any mode that is a parent of MODES, set MODES to
+:derived, e.g.
+
+ (set-company-backend! :derived 'text-mode 'company-dabbrev 'company-yasnippet)"
+ (declare (indent defun))
+ (let ((type :exact))
+ (when (eq modes :derived)
+ (setq type :derived
+ modes (pop backends)))
+ (dolist (mode (doom-enlist modes))
+ (if (null (car backends))
+ (setq +company-backend-alist
+ (delq (assq mode +company-backend-alist)
+ +company-backend-alist))
+ (setf (alist-get mode +company-backend-alist)
+ (cons type backends))))))
+
+
+;;
+;;; Library
+
+(defun +company--backends ()
+ (append (cl-loop for (mode . rest) in +company-backend-alist
+ for type = (car rest)
+ for backends = (cdr rest)
+ if (or (and (eq type :derived) (derived-mode-p mode)) ; parent modes
+ (and (eq type :exact)
+ (or (eq major-mode mode) ; major modes
+ (and (boundp mode)
+ (symbol-value mode))))) ; minor modes
+ append backends)
+ (default-value 'company-backends)))
+
+
+;;
+;;; Hooks
+
+;;;###autoload
+(defun +company|init-backends ()
+ "Set `company-backends' for the current buffer."
+ (unless (eq major-mode 'fundamental-mode)
+ (set (make-local-variable 'company-backends) (+company--backends)))
+ (add-hook 'after-change-major-mode-hook #'+company|init-backends nil 'local))
+
+(put '+company|init-backends 'permanent-local-hook t)
+
+
+;;
+;;; Commands
+
+;;;###autoload
+(defun +company-has-completion-p ()
+ "Return non-nil if a completion candidate exists at point."
+ (and (company-manual-begin)
+ (= company-candidates-length 1)))
+
+;;;###autoload
+(defun +company/toggle-auto-completion ()
+ "Toggle as-you-type code completion."
+ (interactive)
+ (require 'company)
+ (setq company-idle-delay (unless company-idle-delay 0.2))
+ (message "Auto completion %s"
+ (if company-idle-delay "enabled" "disabled")))
+
;;;###autoload
(defun +company/complete ()
"Bring up the completion popup. If only one result, complete it."
(interactive)
(require 'company)
+ (when (ignore-errors
+ (/= (point)
+ (cdr (bounds-of-thing-at-point 'symbol))))
+ (save-excursion (insert " ")))
(when (and (company-manual-begin)
(= company-candidates-length 1))
(company-complete-common)))
+;;;###autoload
+(defun +company/dabbrev ()
+ "Invokes `company-dabbrev-code' in prog-mode buffers and `company-dabbrev'
+everywhere else."
+ (interactive)
+ (call-interactively
+ (if (derived-mode-p 'prog-mode)
+ #'company-dabbrev-code
+ #'company-dabbrev)))
+
;;;###autoload
(defun +company/whole-lines (command &optional arg &rest ignored)
"`company-mode' completion backend that completes whole-lines, akin to vim's
@@ -16,9 +125,9 @@ C-x C-l."
(interactive (list 'interactive))
(require 'company)
(pcase command
- ('interactive (company-begin-backend '+company/whole-lines))
- ('prefix (company-grab-line "^[\t\s]*\\(.+\\)" 1))
- ('candidates
+ (`interactive (company-begin-backend '+company/whole-lines))
+ (`prefix (company-grab-line "^[\t\s]*\\(.+\\)" 1))
+ (`candidates
(all-completions
arg
(split-string
@@ -35,12 +144,13 @@ C-x C-l."
(require 'company-dict)
(require 'company-keywords)
(let ((company-backends '((company-keywords company-dict))))
- (call-interactively 'company-complete)))
+ (call-interactively #'company-complete)))
;;;###autoload
(defun +company/dabbrev-code-previous ()
+ "TODO"
(interactive)
(require 'company-dabbrev)
(let ((company-selection-wrap-around t))
- (call-interactively #'company-dabbrev-code)
+ (call-interactively #'+company/dabbrev)
(company-select-previous-or-abort)))
diff --git a/modules/completion/company/config.el b/modules/completion/company/config.el
index 6644ae417..ee673fb91 100644
--- a/modules/completion/company/config.el
+++ b/modules/completion/company/config.el
@@ -1,86 +1,124 @@
;;; completion/company/config.el -*- lexical-binding: t; -*-
-(def-setting! :company-backend (modes &rest backends)
- "Prepends BACKENDS to `company-backends' in major MODES.
-
-MODES should be one major-mode symbol or a list of them."
- `(progn
- ,@(cl-loop for mode in (doom-enlist (doom-unquote modes))
- for def-name = (intern (format "doom--init-company-%s" mode))
- collect
- `(defun ,def-name ()
- (when (and (eq major-mode ',mode)
- ,(not (eq backends '(nil))))
- (require 'company)
- (make-variable-buffer-local 'company-backends)
- (dolist (backend (list ,@(reverse backends)))
- (cl-pushnew backend company-backends :test #'equal))))
- collect `(add-hook! ,mode #',def-name))))
-
-
-;;
-;; Packages
-;;
-
(def-package! company
- :commands (company-mode global-company-mode company-complete
- company-complete-common company-manual-begin company-grab-line)
- :config
+ :commands (company-complete-common company-manual-begin company-grab-line)
+ :init
(setq company-idle-delay nil
- company-tooltip-limit 10
+ company-tooltip-limit 14
company-dabbrev-downcase nil
company-dabbrev-ignore-case nil
company-dabbrev-code-other-buffers t
company-tooltip-align-annotations t
company-require-match 'never
- company-global-modes '(not eshell-mode comint-mode erc-mode message-mode help-mode gud-mode)
- company-frontends '(company-pseudo-tooltip-frontend company-echo-metadata-frontend)
- company-backends '(company-capf company-dabbrev company-ispell)
- company-transformers '(company-sort-by-occurrence))
-
- (after! yasnippet
- (nconc company-backends '(company-yasnippet)))
-
+ company-global-modes
+ '(not erc-mode message-mode help-mode gud-mode eshell-mode)
+ company-backends '(company-capf)
+ company-frontends
+ '(company-pseudo-tooltip-frontend
+ company-echo-metadata-frontend))
+ :config
+ (add-hook 'company-mode-hook #'+company|init-backends)
+ (when (featurep! :feature evil)
+ (add-hook 'company-mode-hook #'evil-normalize-keymaps))
(global-company-mode +1))
-(def-package! company-statistics
- :after company
- :config
- (setq company-statistics-file (concat doom-cache-dir "company-stats-cache.el"))
- (quiet! (company-statistics-mode +1)))
+(def-package! company
+ :when (featurep! +auto)
+ :defer 2
+ :after-call post-self-insert-hook
+ :config (setq company-idle-delay 0.1))
-;; Looks ugly on OSX without emacs-mac build
-(def-package! company-quickhelp
- :after company
+(def-package! company-tng
+ :when (featurep! +tng)
+ :defer 2
+ :after-call post-self-insert-hook
:config
- (setq company-quickhelp-delay nil)
- (company-quickhelp-mode +1))
+ (add-to-list 'company-frontends 'company-tng-frontend)
+ (define-key! company-active-map
+ "RET" nil
+ [return] nil
+ "TAB" #'company-select-next
+ [tab] #'company-select-next
+ [backtab] #'company-select-previous))
+
+
+;;
+;; Packages
+
+(def-package! company-prescient
+ :hook (company-mode . company-prescient-mode)
+ :config
+ (setq prescient-save-file (concat doom-cache-dir "prescient-save.el"))
+ (prescient-persist-mode +1))
+
+
+(def-package! company-box
+ :when (and EMACS26+ (featurep! +childframe))
+ :hook (company-mode . company-box-mode)
+ :config
+ (setq company-box-show-single-candidate t
+ company-box-backends-colors nil
+ company-box-max-candidates 50
+ company-box-icons-alist 'company-box-icons-all-the-icons
+ company-box-icons-functions
+ '(+company-box-icons--yasnippet company-box-icons--lsp +company-box-icons--elisp company-box-icons--acphp)
+ company-box-icons-all-the-icons
+ `((Unknown . ,(all-the-icons-material "find_in_page" :height 0.8 :face 'all-the-icons-purple))
+ (Text . ,(all-the-icons-material "text_fields" :height 0.8 :face 'all-the-icons-green))
+ (Method . ,(all-the-icons-material "functions" :height 0.8 :face 'all-the-icons-red))
+ (Function . ,(all-the-icons-material "functions" :height 0.8 :face 'all-the-icons-red))
+ (Constructor . ,(all-the-icons-material "functions" :height 0.8 :face 'all-the-icons-red))
+ (Field . ,(all-the-icons-material "functions" :height 0.8 :face 'all-the-icons-red))
+ (Variable . ,(all-the-icons-material "adjust" :height 0.8 :face 'all-the-icons-blue))
+ (Class . ,(all-the-icons-material "class" :height 0.8 :face 'all-the-icons-red))
+ (Interface . ,(all-the-icons-material "settings_input_component" :height 0.8 :face 'all-the-icons-red))
+ (Module . ,(all-the-icons-material "view_module" :height 0.8 :face 'all-the-icons-red))
+ (Property . ,(all-the-icons-material "settings" :height 0.8 :face 'all-the-icons-red))
+ (Unit . ,(all-the-icons-material "straighten" :height 0.8 :face 'all-the-icons-red))
+ (Value . ,(all-the-icons-material "filter_1" :height 0.8 :face 'all-the-icons-red))
+ (Enum . ,(all-the-icons-material "plus_one" :height 0.8 :face 'all-the-icons-red))
+ (Keyword . ,(all-the-icons-material "filter_center_focus" :height 0.8 :face 'all-the-icons-red))
+ (Snippet . ,(all-the-icons-material "short_text" :height 0.8 :face 'all-the-icons-red))
+ (Color . ,(all-the-icons-material "color_lens" :height 0.8 :face 'all-the-icons-red))
+ (File . ,(all-the-icons-material "insert_drive_file" :height 0.8 :face 'all-the-icons-red))
+ (Reference . ,(all-the-icons-material "collections_bookmark" :height 0.8 :face 'all-the-icons-red))
+ (Folder . ,(all-the-icons-material "folder" :height 0.8 :face 'all-the-icons-red))
+ (EnumMember . ,(all-the-icons-material "people" :height 0.8 :face 'all-the-icons-red))
+ (Constant . ,(all-the-icons-material "pause_circle_filled" :height 0.8 :face 'all-the-icons-red))
+ (Struct . ,(all-the-icons-material "streetview" :height 0.8 :face 'all-the-icons-red))
+ (Event . ,(all-the-icons-material "event" :height 0.8 :face 'all-the-icons-red))
+ (Operator . ,(all-the-icons-material "control_point" :height 0.8 :face 'all-the-icons-red))
+ (TypeParameter . ,(all-the-icons-material "class" :height 0.8 :face 'all-the-icons-red))
+ ;; (Template . ,(company-box-icons-image "Template.png"))))
+ (Yasnippet . ,(all-the-icons-material "short_text" :height 0.8 :face 'all-the-icons-green))
+ (ElispFunction . ,(all-the-icons-material "functions" :height 0.8 :face 'all-the-icons-red))
+ (ElispVariable . ,(all-the-icons-material "check_circle" :height 0.8 :face 'all-the-icons-blue))
+ (ElispFeature . ,(all-the-icons-material "stars" :height 0.8 :face 'all-the-icons-orange))
+ (ElispFace . ,(all-the-icons-material "format_paint" :height 0.8 :face 'all-the-icons-pink))))
+
+ (defun +company-box-icons--yasnippet (candidate)
+ (when (get-text-property 0 'yas-annotation candidate)
+ 'Yasnippet))
+
+ (defun +company-box-icons--elisp (candidate)
+ (when (derived-mode-p 'emacs-lisp-mode)
+ (let ((sym (intern candidate)))
+ (cond ((fboundp sym) 'ElispFunction)
+ ((boundp sym) 'ElispVariable)
+ ((featurep sym) 'ElispFeature)
+ ((facep sym) 'ElispFace))))))
(def-package! company-dict
- :commands company-dict
+ :defer t
:config
+ (setq company-dict-dir (expand-file-name "dicts" doom-private-dir))
(defun +company|enable-project-dicts (mode &rest _)
"Enable per-project dictionaries."
(if (symbol-value mode)
- (cl-pushnew mode company-dict-minor-mode-list :test #'eq)
+ (add-to-list 'company-dict-minor-mode-list mode nil #'eq)
(setq company-dict-minor-mode-list (delq mode company-dict-minor-mode-list))))
(add-hook 'doom-project-hook #'+company|enable-project-dicts))
-
-;;
-;; Autoloads
-;;
-
-(autoload 'company-capf "company-capf")
-(autoload 'company-yasnippet "company-yasnippet")
-(autoload 'company-dabbrev "company-dabbrev")
-(autoload 'company-dabbrev-code "company-dabbrev-code")
-(autoload 'company-etags "company-etags")
-(autoload 'company-elisp "company-elisp")
-(autoload 'company-files "company-files")
-(autoload 'company-gtags "company-gtags")
-(autoload 'company-ispell "company-ispell")
-
diff --git a/modules/completion/company/packages.el b/modules/completion/company/packages.el
index 8afbdd127..1e2b7a64b 100644
--- a/modules/completion/company/packages.el
+++ b/modules/completion/company/packages.el
@@ -3,5 +3,6 @@
(package! company)
(package! company-dict)
-(package! company-quickhelp)
-(package! company-statistics)
+(package! company-prescient)
+(when (and EMACS26+ (featurep! +childframe))
+ (package! company-box))
diff --git a/modules/completion/company/test/company.el b/modules/completion/company/test/company.el
deleted file mode 100644
index 4f7244c14..000000000
--- a/modules/completion/company/test/company.el
+++ /dev/null
@@ -1,24 +0,0 @@
-;; -*- lexical-binding: t; no-byte-compile: t; -*-
-;;; completion/company/test/company.el
-
-(require! :completion company)
-(require 'company)
-
-;;
-(def-test! set-company-backend
- :minor-mode company-mode
- (let ((company-backends '(default)))
- (set! :company-backend 'emacs-lisp-mode '(backend-1))
- (set! :company-backend 'lisp-interaction-mode 'backend-1 'backend-2)
- (set! :company-backend 'text-mode 'backend-1)
- (with-temp-buffer
- (emacs-lisp-mode)
- (should (equal company-backends '((backend-1) default))))
- (with-temp-buffer
- (lisp-interaction-mode)
- (should (equal company-backends '(backend-1 backend-2 default))))
- (with-temp-buffer
- (text-mode)
- (should (equal company-backends '(backend-1 default))))
- ;; global backends shouldn't be affected
- (should (equal company-backends '(default)))))
diff --git a/modules/completion/company/test/test-company.el b/modules/completion/company/test/test-company.el
new file mode 100644
index 000000000..ed69404b0
--- /dev/null
+++ b/modules/completion/company/test/test-company.el
@@ -0,0 +1,75 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; completion/company/test/test-company.el
+
+(describe "completion/company"
+ (before-all
+ (load! "../autoload"))
+
+ (describe ":company-backend"
+ :var (a +company-backend-alist backends)
+ (before-each
+ (setq-default company-backends '(t))
+ (setq +company-backend-alist nil
+ a (get-buffer-create "x"))
+ (fset 'backends
+ (lambda (mode)
+ (let ((major-mode mode))
+ (+company--backends))))
+ (set-buffer a)
+ (spy-on 'require))
+ (after-each
+ (kill-buffer a))
+
+ ;;
+ (it "sets backends for a major mode"
+ (set-company-backend! 'text-mode 'a)
+ (expect (backends 'text-mode) :to-equal '(a t)))
+
+ (it "sets backends for a derived-mode"
+ (set-company-backend! :derived 'prog-mode 'a)
+ (expect (backends 'prog-mode) :to-equal '(a t))
+ (expect (backends 'emacs-lisp-mode) :to-equal '(a t)))
+
+ (it "sets multiple backends for exact major modes"
+ (set-company-backend! '(text-mode emacs-lisp-mode) 'a 'b)
+ (expect (backends 'text-mode) :to-equal (backends 'emacs-lisp-mode)))
+
+ (it "sets cumulative backends"
+ (set-company-backend! :derived 'prog-mode '(a b c))
+ (set-company-backend! 'emacs-lisp-mode 'd 'e)
+ (expect (backends 'emacs-lisp-mode) :to-equal '(d e (a b c) t)))
+
+ (it "sets cumulative backends with a minor mode"
+ (set-company-backend! :derived 'prog-mode '(a b c))
+ (set-company-backend! 'emacs-lisp-mode 'd 'e)
+ (set-company-backend! 'some-minor-mode 'x 'y)
+ (setq-local some-minor-mode t)
+ (expect (backends 'emacs-lisp-mode) :to-equal '(x y d e (a b c) t)))
+
+ (it "overwrites past backends"
+ (set-company-backend! 'text-mode 'old 'backends)
+ (set-company-backend! 'text-mode 'new 'backends)
+ (expect (backends 'text-mode) :to-equal '(new backends t)))
+
+ (it "unsets past backends"
+ (set-company-backend! 'text-mode 'old)
+ (set-company-backend! 'text-mode nil)
+ (expect (backends 'text-mode) :to-equal (default-value 'company-backends)))
+
+ (it "unsets past parent backends"
+ (set-company-backend! :derived 'prog-mode 'old)
+ (set-company-backend! 'emacs-lisp-mode 'child)
+ (set-company-backend! :derived 'prog-mode nil)
+ (expect (backends 'emacs-lisp-mode) :to-equal '(child t)))
+
+ (it "overwrites past cumulative backends"
+ (set-company-backend! :derived 'prog-mode 'base)
+ (set-company-backend! 'emacs-lisp-mode 'old)
+ (set-company-backend! 'emacs-lisp-mode 'new)
+ (expect (backends 'emacs-lisp-mode) :to-equal '(new base t)))
+
+ (it "overwrites past parent backends"
+ (set-company-backend! :derived 'prog-mode 'base)
+ (set-company-backend! 'emacs-lisp-mode 'child)
+ (set-company-backend! :derived 'prog-mode 'new)
+ (expect (backends 'emacs-lisp-mode) :to-equal '(child new t)))))
diff --git a/modules/completion/helm/autoload/evil.el b/modules/completion/helm/autoload/evil.el
index d1c1841bd..a5cfa3a25 100644
--- a/modules/completion/helm/autoload/evil.el
+++ b/modules/completion/helm/autoload/evil.el
@@ -1,60 +1,83 @@
;;; completion/helm/autoload/evil.el -*- lexical-binding: t; -*-
;;;###if (featurep! :feature evil)
-;;;###autoload (autoload '+helm:swoop "completion/helm/autoload/evil" nil t)
-(evil-define-command +helm:swoop (&optional search bang)
- "Invoke `swoop' with SEARCH. If BANG, do multiline search."
- (interactive "")
- (helm-swoop :$query search :$multiline bang))
+;;
+;; Project searching
-(defun +helm--file-search (beg end query &optional directory options)
- (require 'helm-ag)
- (helm-ag--init-state)
- (let ((helm-ag--default-directory (or directory (doom-project-root)))
- (query (or query
- (if (evil-visual-state-p)
- (and beg end
- (> (abs (- end beg)) 1)
- (rxt-quote-pcre (buffer-substring-no-properties beg end)))
- +helm--file-last-query)
- +helm--file-last-query))
- (helm-ag-command-option (concat helm-ag-command-option " " (string-join options " "))))
- (setq helm-ag--last-query query)
- (helm-attrset 'search-this-file nil helm-ag-source)
- (helm-attrset 'name (helm-ag--helm-header helm-ag--default-directory) helm-ag-source)
- (helm :sources '(helm-ag-source)
- :input query
- :buffer "*helm-ag*"
- :keymap helm-ag-map
- :history 'helm-ag--helm-history)))
+;;;###autoload (autoload '+helm:pt "completion/helm/autoload/evil" nil t)
+(evil-define-command +helm:pt (all-files-p query)
+ "Ex interface for `+helm/pt'"
+ (interactive "")
+ (+helm/pt all-files-p query))
+
+;;;###autoload (autoload '+helm:grep "completion/helm/autoload/evil" nil t)
+(evil-define-command +helm:grep (all-files-p query)
+ "Ex interface for `+helm/grep'"
+ (interactive "")
+ (+helm/grep all-files-p query))
-(defvar +helm--file-last-search nil)
;;;###autoload (autoload '+helm:ag "completion/helm/autoload/evil" nil t)
-(evil-define-command +helm:ag (beg end query &optional bang)
- "TODO"
- (interactive "")
- (+helm--file-search beg end query nil
- (if bang (list "-a" "--hidden"))))
-
-;;;###autoload (autoload '+helm:ag-cwd "completion/helm/autoload/evil" nil t)
-(evil-define-command +helm:ag-cwd (beg end query &optional bang)
- "TODO"
- (interactive "")
- (+helm--file-search beg end query default-directory
- (list "-n" (if bang "-a"))))
+(evil-define-command +helm:ag (all-files-p query)
+ "Ex interface for `+helm/ag'"
+ (interactive "")
+ (+helm/ag all-files-p query))
;;;###autoload (autoload '+helm:rg "completion/helm/autoload/evil" nil t)
-(evil-define-command +helm:rg (beg end query &optional bang)
- "TODO"
- (interactive "")
- (let ((helm-ag-base-command "rg --no-heading"))
- (+helm--file-search beg end query nil
- (if bang (list "-uu")))))
+(evil-define-command +helm:rg (all-files-p query)
+ "Ex interface for `+helm/rg'"
+ (interactive "")
+ (+helm/rg all-files-p query))
-;;;###autoload (autoload '+helm:rg-cwd "completion/helm/autoload/evil" nil t)
-(evil-define-command +helm:rg-cwd (beg end query &optional bang)
+
+;;;###autoload (autoload '+helm:pt-from-cwd "completion/helm/autoload/evil" nil t)
+(evil-define-command +helm:pt-from-cwd (query &optional recurse-p)
+ "Ex interface for `+helm/pt-from-cwd'."
+ (interactive "")
+ (+helm/pt-from-cwd (not recurse-p) query))
+
+;;;###autoload (autoload '+helm:grep-from-cwd "completion/helm/autoload/evil" nil t)
+(evil-define-command +helm:grep-from-cwd (query &optional recurse-p)
+ "Ex interface for `+helm/grep-from-cwd'."
+ (interactive "")
+ (+helm/grep-from-cwd (not recurse-p) query))
+
+;;;###autoload (autoload '+helm:ag-from-cwd "completion/helm/autoload/evil" nil t)
+(evil-define-command +helm:ag-from-cwd (query &optional recurse-p)
+ "Ex interface for `+helm/ag-from-cwd'."
+ (interactive "")
+ (+helm/ag-from-cwd (not recurse-p) query))
+
+;;;###autoload (autoload '+helm:rg-from-cwd "completion/helm/autoload/evil" nil t)
+(evil-define-command +helm:rg-from-cwd (query &optional recurse-p)
+ "Ex interface for `+helm/rg-from-cwd'."
+ (interactive "")
+ (+helm/rg-from-cwd (not recurse-p) query))
+
+
+;;;###autoload
+(defun +helm--set-prompt-display (pos)
"TODO"
- (interactive "")
- (let ((helm-ag-base-command "rg --no-heading --maxdepth 1"))
- (+helm--file-search beg end query default-directory
- (if bang (list "-uu")))))
+ (let (beg state region-active m)
+ (with-selected-window (minibuffer-window)
+ (setq beg (save-excursion (vertical-motion 0 (helm-window)) (point))
+ state evil-state
+ region-active (region-active-p)
+ m (mark t)))
+ (when region-active
+ (setq m (- m beg))
+ ;; Increment pos to handle the space before prompt (i.e `pref').
+ (put-text-property (1+ (min m pos)) (+ 2 (max m pos))
+ 'face
+ (list :background (face-background 'region))
+ header-line-format))
+ (put-text-property
+ ;; Increment pos to handle the space before prompt (i.e `pref').
+ (+ 1 pos) (+ 2 pos)
+ 'face
+ (if (eq state 'insert)
+ 'underline
+ ;; Don't just use 'cursor, this can hide the current character.
+ (list :inverse-video t
+ :foreground (face-background 'cursor)
+ :background (face-background 'default)))
+ header-line-format)))
diff --git a/modules/completion/helm/autoload/helm.el b/modules/completion/helm/autoload/helm.el
new file mode 100644
index 000000000..d798b167c
--- /dev/null
+++ b/modules/completion/helm/autoload/helm.el
@@ -0,0 +1,249 @@
+;;; completion/helm/autoload/helm.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +helm/tasks (&optional _arg)
+ (interactive "P")
+ ;; TODO Implement `+helm/tasks'
+ (error "Not implemented yet"))
+
+;;;###autoload
+(defun +helm/projectile-find-file ()
+ "Call `helm-find-files' if called from HOME, otherwise
+`helm-projectile-find-file'."
+ (interactive)
+ (call-interactively
+ (if (or (file-equal-p default-directory "~")
+ (if-let* ((proot (doom-project-root)))
+ (file-equal-p proot "~")
+ t))
+ #'helm-find-files
+ #'helm-projectile-find-file)))
+
+;;;###autoload
+(defun +helm/workspace-buffer-list ()
+ "A version of `helm-buffers-list' with its buffer list restricted to the
+current workspace."
+ (interactive)
+ (unless (featurep! :feature workspaces)
+ (user-error "This command requires the :feature workspaces module"))
+ (with-no-warnings
+ (with-persp-buffer-list nil (helm-buffers-list))))
+
+;;;###autoload
+(defun +helm/workspace-mini ()
+ "A version of `helm-mini' with its buffer list restricted to the current
+workspace."
+ (interactive)
+ (unless (featurep! :feature workspaces)
+ (user-error "This command requires the :feature workspaces module"))
+ (with-no-warnings
+ (with-persp-buffer-list nil (helm-mini))))
+
+
+;;
+;; Project search
+
+(defun +helm-ag-search-args (all-files-p recursive-p)
+ (list (concat "ag " (if IS-WINDOWS "--vimgrep" "--nocolor --nogroup"))
+ "-S"
+ (if all-files-p "-z -a")
+ (unless recursive-p "--depth 1")))
+
+(defun +helm-rg-search-args (all-files-p recursive-p)
+ (list "rg --no-heading --line-number --color never"
+ "-S"
+ (when all-files-p "-z -uu")
+ (unless recursive-p "--maxdepth 1")))
+
+(defun +helm-pt-search-args (all-files-p recursive-p)
+ (list "pt --nocolor --nogroup -e"
+ "-S"
+ (if all-files-p "-z -a")
+ (unless recursive-p "--depth 1")))
+
+;;
+(defun +helm--grep-source ()
+ (require 'helm-projectile)
+ (helm-build-async-source (capitalize (helm-grep-command t))
+ :header-name (lambda (_name) "Helm Projectile Grep (C-c ? Help)")
+ :candidates-process #'helm-grep-collect-candidates
+ :filter-one-by-one #'helm-grep-filter-one-by-one
+ :candidate-number-limit 9999
+ :nohighlight t
+ :keymap helm-grep-map
+ :history 'helm-grep-history
+ :action (apply #'helm-make-actions helm-projectile-grep-or-ack-actions)
+ :persistent-action 'helm-grep-persistent-action
+ :persistent-help "Jump to line (`C-u' Record in mark ring)"
+ :requires-pattern 2))
+
+(defun +helm--grep-search (directory query prompt &optional all-files-p recursive-p)
+ (let* ((default-directory directory)
+ (helm-ff-default-directory directory)
+ (helm-grep-in-recurse recursive-p)
+ (helm-grep-ignored-files
+ (unless all-files-p
+ (cl-union (projectile-ignored-files-rel) grep-find-ignored-files)))
+ (helm-grep-ignored-directories
+ (unless all-files-p
+ (cl-union (mapcar 'directory-file-name (projectile-ignored-directories-rel))
+ grep-find-ignored-directories)))
+ (helm-grep-default-command
+ (if (and nil (eq (projectile-project-vcs) 'git))
+ (format "git --no-pager grep --no-color -n%%c -e %%p %s -- %%f"
+ (if recursive-p "" "--max-depth 1 "))
+ (format "grep -si -a%s %%e -n%%cH -e %%p %%f %s"
+ (if recursive-p " -R" "")
+ (if recursive-p "." "./*"))))
+ (helm-grep-default-recurse-command helm-grep-default-command))
+ (setq helm-source-grep (+helm--grep-source))
+ (helm :sources 'helm-source-grep
+ :input query
+ :prompt prompt
+ :buffer "*helm grep*"
+ :default-directory directory
+ :keymap helm-grep-map
+ :history 'helm-grep-history
+ :truncate-lines helm-grep-truncate-lines)))
+
+;;;###autoload
+(cl-defun +helm-file-search (engine &key query in all-files (recursive t))
+ "Conduct a file search using ENGINE, which can be any of: rg, ag, pt, and
+grep. If omitted, ENGINE will default to the first one it detects, in that
+order.
+
+:query STRING
+ Determines the initial input to search for.
+:in PATH
+ Sets what directory to base the search out of. Defaults to the current
+ project's root.
+:recursive BOOL
+ Whether or not to search files recursively from the base directory."
+ (declare (indent defun))
+ (require 'helm-ag)
+ (helm-ag--init-state)
+ (let* ((project-root (or (doom-project-root) default-directory))
+ (directory (or in project-root))
+ (default-directory directory)
+ (helm-ag--default-directory directory)
+ (helm-ag--default-target (list directory))
+ (engine (or engine
+ (cl-find-if #'executable-find +helm-project-search-engines
+ :key #'symbol-name)
+ (and (or (executable-find "grep")
+ (executable-find "git"))
+ 'grep)
+ (user-error "No search engine specified (is ag, rg, pt or git installed?)")))
+ (query (or query
+ (when (use-region-p)
+ (let ((beg (or (bound-and-true-p evil-visual-beginning) (region-beginning)))
+ (end (or (bound-and-true-p evil-visual-end) (region-end))))
+ (when (> (abs (- end beg)) 1)
+ (rxt-quote-pcre (buffer-substring-no-properties beg end)))))
+ ""))
+ (prompt (format "[%s %s] "
+ (symbol-name engine)
+ (cond ((file-equal-p directory project-root)
+ (projectile-project-name))
+ ((file-equal-p directory default-directory)
+ "./")
+ ((file-relative-name directory project-root)))))
+ (command
+ (pcase engine
+ (`ag (+helm-ag-search-args all-files recursive))
+ (`rg (+helm-rg-search-args all-files recursive))
+ (`pt (+helm-pt-search-args all-files recursive))
+ ('grep (+helm--grep-search directory query prompt all-files recursive)
+ (cl-return t))))
+ (helm-ag-base-command (string-join command " ")))
+ ;; TODO Define our own sources instead
+ (helm-attrset 'name (format "[%s %s] Searching %s"
+ engine
+ (string-join (delq nil (cdr command)) " ")
+ (abbreviate-file-name directory))
+ helm-source-do-ag)
+ (helm-attrset '+helm-command command helm-source-do-ag)
+ (cl-letf (((symbol-function 'helm-do-ag--helm)
+ (lambda () (helm :sources '(helm-source-do-ag)
+ :prompt prompt
+ :buffer "*helm-ag*"
+ :keymap helm-do-ag-map
+ :input query
+ :history 'helm-ag--helm-history))))
+ (helm-do-ag directory))))
+
+(defun +helm--get-command (format)
+ (cl-loop for tool in (cl-remove-duplicates +helm-project-search-engines :from-end t)
+ if (executable-find (symbol-name tool))
+ return (intern (format format tool))))
+
+;;;###autoload
+(defun +helm/project-search (&optional arg initial-query directory)
+ "Performs a project search from the project root.
+
+Uses the first available search backend from `+helm-project-search-engines'. If
+ARG (universal argument), include all files, even hidden or compressed ones, in
+the search."
+ (interactive "P")
+ (funcall (or (+helm--get-command "+helm/%s")
+ #'+helm/grep)
+ arg
+ initial-query
+ directory))
+
+;;;###autoload
+(defun +helm/project-search-from-cwd (&optional arg initial-query)
+ "Performs a project search recursively from the current directory.
+
+Uses the first available search backend from `+helm-project-search-engines'. If
+ARG (universal argument), include all files, even hidden or compressed ones."
+ (interactive "P")
+ (funcall (or (+helm--get-command "+helm/%s-from-cwd")
+ #'+helm/grep-from-cwd)
+ arg
+ initial-query))
+
+
+;;;###autoload (autoload '+helm/rg "completion/helm/autoload/helm")
+;;;###autoload (autoload '+helm/rg-from-cwd "completion/helm/autoload/helm")
+;;;###autoload (autoload '+helm/ag "completion/helm/autoload/helm")
+;;;###autoload (autoload '+helm/ag-from-cwd "completion/helm/autoload/helm")
+;;;###autoload (autoload '+helm/pt "completion/helm/autoload/helm")
+;;;###autoload (autoload '+helm/pt-from-cwd "completion/helm/autoload/helm")
+;;;###autoload (autoload '+helm/grep "completion/helm/autoload/helm")
+;;;###autoload (autoload '+helm/grep-from-cwd "completion/helm/autoload/helm")
+
+(dolist (engine `(,@(cl-remove-duplicates +helm-project-search-engines :from-end t) grep))
+ (defalias (intern (format "+helm/%s" engine))
+ (lambda (arg &optional query directory)
+ (interactive "P")
+ (+helm-file-search engine
+ :query query
+ :in directory
+ :all-files (and (not (null arg))
+ (listp arg))))
+ (format "Perform a project file search using %s.
+
+QUERY is a regexp. If omitted, the current selection is used. If no selection is
+active, the last known search is used.
+
+ARG is the universal argument. If a number is passed through it, e.g. C-u 3, then
+
+If ALL-FILES-P, search compressed and hidden files as well."
+ engine))
+
+ (defalias (intern (format "+helm/%s-from-cwd" engine))
+ (lambda (arg &optional query)
+ (interactive "P")
+ (+helm-file-search engine
+ :query query
+ :in default-directory
+ :all-files (and (not (null arg))
+ (listp arg))))
+ (format "Perform a project file search from the current directory using %s.
+
+QUERY is a regexp. If omitted, the current selection is used. If no selection is
+active, the last known search is used.
+
+If ALL-FILES-P, search compressed and hidden files as well."
+ engine)))
diff --git a/modules/completion/helm/autoload/posframe.el b/modules/completion/helm/autoload/posframe.el
new file mode 100644
index 000000000..308679c74
--- /dev/null
+++ b/modules/completion/helm/autoload/posframe.el
@@ -0,0 +1,66 @@
+;;; completion/helm/autoload/posframe.el -*- lexical-binding: t; -*-
+
+(add-hook 'helm-cleanup-hook #'+helm|posframe-cleanup)
+
+;;;###autoload
+(defun +helm-poshandler-frame-center-near-bottom (info)
+ "Display the child frame in the center of the frame, slightly closer to the
+bottom, which is easier on the eyes on big displays."
+ (let ((parent-frame (plist-get info :parent-frame))
+ (pos (posframe-poshandler-frame-center info)))
+ (cons (car pos)
+ (truncate (/ (frame-pixel-height parent-frame)
+ 2)))))
+
+(defvar +helm--posframe-buffer nil)
+;;;###autoload
+(defun +helm-posframe-display (buffer &optional _resume)
+ "TODO"
+ (setq helm--buffer-in-new-frame-p t)
+ (let ((solaire-p (bound-and-true-p solaire-mode))
+ (params (copy-sequence +helm-posframe-parameters)))
+ (let-alist params
+ (require 'posframe)
+ (posframe-show
+ (setq +helm--posframe-buffer buffer)
+ :position (point)
+ :poshandler +helm-posframe-handler
+ :width
+ (max (cl-typecase .width
+ (integer .width)
+ (float (truncate (* (frame-width) .width)))
+ (function (funcall .width))
+ (t 0))
+ .min-width)
+ :height
+ (max (cl-typecase .height
+ (integer .height)
+ (float (truncate (* (frame-height) .height)))
+ (function (funcall .height))
+ (t 0))
+ .min-height)
+ :override-parameters
+ (dolist (p '(width height min-width min-height) params)
+ (setq params (delq (assq p params) params)))))
+ ;;
+ (unless (or (null +helm-posframe-text-scale)
+ (= +helm-posframe-text-scale 0))
+ (with-current-buffer buffer
+ (when (and (featurep 'solaire-mode)
+ (not solaire-p))
+ (solaire-mode +1))
+ (text-scale-set +helm-posframe-text-scale)))))
+
+;;;###autoload
+(defun +helm|posframe-cleanup ()
+ "TODO"
+ ;; Ensure focus is properly returned to the underlying window, by forcing a
+ ;; chance in buffer/window focus. This gives the modeline a chance to refresh.
+ (switch-to-buffer +helm--posframe-buffer t)
+ ;;
+ (posframe-delete +helm--posframe-buffer))
+
+
+;;;###autoload
+(defun +helm*fix-get-font-height (orig-fn position)
+ (ignore-errors (funcall orig-fn position)))
diff --git a/modules/completion/helm/config.el b/modules/completion/helm/config.el
index 33890f1b1..32441b5cc 100644
--- a/modules/completion/helm/config.el
+++ b/modules/completion/helm/config.el
@@ -1,124 +1,183 @@
;;; completion/helm/config.el -*- lexical-binding: t; -*-
-;; Warning: since I don't use helm, this may be out of date.
+(defvar +helm-project-search-engines '(rg ag pt)
+ "What search tools for `+helm/project-search' (and `+helm-file-search' when no
+ENGINE is specified) to try, and in what order.
-(defvar +helm-global-prompt "››› "
- "The helm text prompt prefix string is globally replaced with this string.")
+To disable a particular tool, remove it from this list. To prioritize a tool
+over others, move it to the front of the list. Later duplicates in this list are
+silently ignored.
+
+This falls back to git-grep (then grep) if none of these available.")
+
+;; Posframe (requires +childframe)
+(defvar +helm-posframe-handler
+ #'+helm-poshandler-frame-center-near-bottom
+ "The function that determines the location of the childframe. It should return
+a cons cell representing the X and Y coordinates. See
+`posframe-poshandler-frame-center' as a reference.")
+
+(defvar +helm-posframe-text-scale 1
+ "The text-scale to use in the helm childframe. Set to nil for no scaling. Can
+be negative.")
+
+(defvar +helm-posframe-parameters
+ '((internal-border-width . 8)
+ (width . 0.5)
+ (height . 0.35)
+ (min-width . 80)
+ (min-height . 16))
+ "TODO")
;;
;; Packages
-;;
+
+(def-package! helm-mode
+ :defer t
+ :after-call pre-command-hook
+ :init
+ (map! [remap apropos] #'helm-apropos
+ [remap find-library] #'helm-locate-library
+ [remap bookmark-jump] #'helm-bookmarks
+ [remap execute-extended-command] #'helm-M-x
+ [remap find-file] #'helm-find-files
+ [remap imenu-anywhere] #'helm-imenu-anywhere
+ [remap imenu] #'helm-semantic-or-imenu
+ [remap noop-show-kill-ring] #'helm-show-kill-ring
+ [remap persp-switch-to-buffer] #'+helm/workspace-mini
+ [remap switch-to-buffer] #'helm-buffers-list
+ [remap projectile-find-file] #'+helm/projectile-find-file
+ [remap projectile-recentf] #'helm-projectile-recentf
+ [remap projectile-switch-project] #'helm-projectile-switch-project
+ [remap projectile-switch-to-buffer] #'helm-projectile-switch-to-buffer
+ [remap recentf-open-files] #'helm-recentf
+ [remap yank-pop] #'helm-show-kill-ring)
+ :config
+ (helm-mode +1)
+ ;; helm is too heavy for `find-file-at-point'
+ (add-to-list 'helm-completing-read-handlers-alist (cons #'find-file-at-point nil)))
+
(def-package! helm
- :init
- (setq helm-quick-update t
- ;; Speedier without fuzzy matching
- helm-mode-fuzzy-match nil
- helm-buffers-fuzzy-matching nil
- helm-apropos-fuzzy-match nil
- helm-M-x-fuzzy-match nil
- helm-recentf-fuzzy-match nil
- helm-projectile-fuzzy-match nil
- ;; Display extraineous helm UI elements
+ :after helm-mode
+ :preface
+ (setq helm-candidate-number-limit 50
+ ;; Remove extraineous helm UI elements
helm-display-header-line nil
+ helm-mode-line-string nil
helm-ff-auto-update-initial-value nil
helm-find-files-doc-header nil
;; Don't override evil-ex's completion
helm-mode-handle-completion-in-region nil
- helm-candidate-number-limit 50
- ;; Don't wrap item cycling
- helm-move-to-line-cycle-in-source t)
+ ;; Default helm window sizes
+ helm-display-buffer-default-width nil
+ helm-display-buffer-default-height 0.25
+ ;; When calling `helm-semantic-or-imenu', don't immediately jump to
+ ;; symbol at point
+ helm-imenu-execute-action-at-once-if-one nil
+ ;; disable special behavior for left/right, M-left/right keys.
+ helm-ff-lynx-style-map nil)
+
+ (when (featurep! :feature evil +everywhere)
+ (setq helm-default-prompt-display-function #'+helm--set-prompt-display))
+
+ :init
+ (when (and EMACS26+ (featurep! +childframe))
+ (setq helm-display-function #'+helm-posframe-display)
+ ;; Fix "Specified window is not displaying the current buffer" error
+ (advice-add #'posframe--get-font-height :around #'+helm*fix-get-font-height))
+
+ (let ((fuzzy (featurep! +fuzzy)))
+ (setq helm-M-x-fuzzy-match fuzzy
+ helm-ag-fuzzy-match fuzzy
+ helm-apropos-fuzzy-match fuzzy
+ helm-apropos-fuzzy-match fuzzy
+ helm-bookmark-show-location fuzzy
+ helm-buffers-fuzzy-matching fuzzy
+ helm-completion-in-region-fuzzy-match fuzzy
+ helm-completion-in-region-fuzzy-match fuzzy
+ helm-ff-fuzzy-matching fuzzy
+ helm-file-cache-fuzzy-match fuzzy
+ helm-flx-for-helm-locate fuzzy
+ helm-imenu-fuzzy-match fuzzy
+ helm-lisp-fuzzy-completion fuzzy
+ helm-locate-fuzzy-match fuzzy
+ helm-mode-fuzzy-match fuzzy
+ helm-projectile-fuzzy-match fuzzy
+ helm-recentf-fuzzy-match fuzzy
+ helm-semantic-fuzzy-match fuzzy))
:config
- (load "helm-autoloads" nil t)
- (add-hook 'doom-init-hook #'helm-mode)
+ (set-popup-rule! "^\\*helm" :vslot -100 :size 0.22 :ttl nil)
- (defvar helm-projectile-find-file-map (make-sparse-keymap))
- (require 'helm-projectile)
- (set-keymap-parent helm-projectile-find-file-map helm-map)
+ ;; Hide the modeline
+ (defun +helm|hide-mode-line (&rest _)
+ (with-current-buffer (helm-buffer-get)
+ (unless helm-mode-line-string
+ (hide-mode-line-mode +1))))
+ (add-hook 'helm-after-initialize-hook #'+helm|hide-mode-line)
+ (advice-add #'helm-display-mode-line :override #'+helm|hide-mode-line)
+ (advice-add #'helm-ag-show-status-default-mode-line :override #'ignore)
- ;; helm is too heavy for find-file-at-point
- (after! helm-mode
- (add-to-list 'helm-completing-read-handlers-alist '(find-file-at-point . nil)))
-
- (set! :popup "\\` ?\\*[hH]elm.*?\\*\\'" :size 14 :regexp t)
- (setq projectile-completion-system 'helm)
-
- ;;; Helm hacks
- (defun +helm*replace-prompt (plist)
- "Globally replace helm prompts with `+helm-global-prompt'."
- (if (keywordp (car plist))
- (plist-put plist :prompt +helm-global-prompt)
- (setf (nth 2 plist) +helm-global-prompt)
- plist))
- (advice-add #'helm :filter-args #'+helm*replace-prompt)
-
- (defun +helm*hide-header (&rest _)
- "Hide header-line & mode-line in helm windows."
- (setq mode-line-format nil))
- (advice-add #'helm-display-mode-line :override #'+helm*hide-header)
-
- (map! :map global-map
- [remap apropos] #'helm-apropos
- [remap find-file] #'helm-find-files
- [remap recentf-open-files] #'helm-recentf
- [remap projectile-switch-to-buffer] #'helm-projectile-switch-to-buffer
- [remap projectile-recentf] #'helm-projectile-recentf
- [remap projectile-find-file] #'helm-projectile-find-file
- [remap imenu] #'helm-semantic-or-imenu
- [remap bookmark-jump] #'helm-bookmarks
- [remap noop-show-kill-ring] #'helm-show-kill-ring
- [remap projectile-switch-project] #'helm-projectile-switch-project
- [remap projectile-find-file] #'helm-projectile-find-file
- [remap imenu-anywhere] #'helm-imenu-anywhere
- [remap execute-extended-command] #'helm-M-x))
+ ;; TODO Find a better way
+ (defun +helm*use-helpful (orig-fn arg)
+ (cl-letf (((symbol-function #'describe-function)
+ (symbol-function #'helpful-callable))
+ ((symbol-function #'describe-variable)
+ (symbol-function #'helpful-variable)))
+ (funcall orig-fn arg)))
+ (advice-add #'helm-describe-variable :around #'+helm*use-helpful)
+ (advice-add #'helm-describe-function :around #'+helm*use-helpful))
-(def-package! helm-locate
- :defer t
- :init (defvar helm-generic-files-map (make-sparse-keymap))
- :config (set-keymap-parent helm-generic-files-map helm-map))
+(def-package! helm-flx
+ :when (featurep! +fuzzy)
+ :hook (helm-mode . helm-flx-mode)
+ :config (helm-flx-mode +1))
-(def-package! helm-bookmark
- :commands helm-bookmark
- :config (setq-default helm-bookmark-show-location t))
+;; `helm-ag'
+(after! helm-ag
+ (map! :map helm-ag-edit-map :n "RET" #'compile-goto-error)
+ (define-key helm-ag-edit-map [remap quit-window] #'helm-ag--edit-abort)
+ (set-popup-rule! "^\\*helm-ag-edit" :size 0.35 :ttl 0 :quit nil)
+ ;; Recenter after jumping to match
+ (advice-add #'helm-ag--find-file-action :after-while #'doom*recenter))
-(def-package! helm-files
- :defer t
- :config
+;; `helm-bookmark'
+(setq helm-bookmark-show-location t)
+
+
+;; `helm-files'
+(after! helm-files
(setq helm-boring-file-regexp-list
(append (list "\\.projects$" "\\.DS_Store$")
helm-boring-file-regexp-list)))
-(def-package! helm-ag
- :defer t
+;; `helm-locate'
+(defvar helm-generic-files-map (make-sparse-keymap))
+(after! helm-locate (set-keymap-parent helm-generic-files-map helm-map))
+
+
+;; `helm-projectile'
+(def-package! helm-projectile
+ :commands (helm-projectile-find-file
+ helm-projectile-recentf
+ helm-projectile-switch-project
+ helm-projectile-switch-to-buffer)
+ :init
+ (setq projectile-completion-system 'helm)
+ (defvar helm-projectile-find-file-map (make-sparse-keymap))
:config
- (map! :map helm-ag-edit-map
- [remap doom/kill-this-buffer] #'helm-ag--edit-abort
- [remap quit-window] #'helm-ag--edit-abort))
+ (set-keymap-parent helm-projectile-find-file-map helm-map))
-(def-package! helm-css-scss ; https://github.com/ShingoFukuyama/helm-css-scss
- :commands (helm-css-scss
- helm-css-scss-multi
- helm-css-scss-insert-close-comment)
- :config
- (setq helm-css-scss-split-direction #'split-window-vertically
- helm-css-scss-split-with-multiple-windows t))
-
-
-(def-package! helm-swoop ; https://github.com/ShingoFukuyama/helm-swoop
- :commands (helm-swoop helm-multi-swoop helm-multi-swoop-all)
- :config
- (setq helm-swoop-use-line-number-face t
- helm-swoop-candidate-number-limit 200
- helm-swoop-speed-or-color t
- helm-swoop-pre-input-function (lambda () "")))
-
-
-(def-package! helm-describe-modes :commands helm-describe-modes)
-
+;; `swiper-helm'
+(after! swiper-helm
+ (setq swiper-helm-display-function
+ (lambda (buf &optional _resume) (pop-to-buffer buf)))
+ (global-set-key [remap swiper] #'swiper-helm)
+ (add-to-list 'swiper-font-lock-exclude #'+doom-dashboard-mode nil #'eq))
diff --git a/modules/completion/helm/packages.el b/modules/completion/helm/packages.el
index ca7338972..1e8d9d178 100644
--- a/modules/completion/helm/packages.el
+++ b/modules/completion/helm/packages.el
@@ -5,7 +5,10 @@
(package! helm-ag)
(package! helm-c-yasnippet)
(package! helm-company)
-(package! helm-css-scss)
(package! helm-describe-modes :recipe (:fetcher github :repo "emacs-helm/helm-describe-modes"))
(package! helm-projectile)
-(package! helm-swoop)
+(package! swiper-helm)
+(when (featurep! +fuzzy)
+ (package! helm-flx))
+(when (and EMACS26+ (featurep! +childframe))
+ (package! posframe))
diff --git a/modules/completion/ido/config.el b/modules/completion/ido/config.el
index 5327920d3..de96fc24b 100644
--- a/modules/completion/ido/config.el
+++ b/modules/completion/ido/config.el
@@ -1,7 +1,6 @@
;;; completion/ido/config.el -*- lexical-binding: t; -*-
-(def-package! ido
- :config
+(defun +ido|init ()
(setq ido-ignore-buffers
'("\\` " "^\\*ESS\\*" "^\\*Messages\\*" "^\\*Help\\*" "^\\*Buffer"
"^\\*.*Completions\\*$" "^\\*Ediff" "^\\*tramp" "^\\*cvs-"
@@ -16,29 +15,18 @@
ido-enable-last-directory-history t
ido-save-directory-list-file (concat doom-cache-dir "ido.last"))
- (push "\\`.DS_Store$" ido-ignore-files)
- (push "Icon\\?$" ido-ignore-files)
+ (unless (member "\\`.DS_Store$" ido-ignore-files)
+ (push "\\`.DS_Store$" ido-ignore-files)
+ (push "Icon\\?$" ido-ignore-files))
- (ido-mode 1)
- (ido-everywhere 1)
- (require 'ido-ubiquitous)
- (ido-ubiquitous-mode 1)
-
- (defun +ido|init ()
- (require 'ido-vertical-mode)
- (ido-vertical-mode 1)
-
- (require 'flx-ido)
- (flx-ido-mode +1)
-
- (require 'crm-custom)
- (crm-custom-mode +1)
-
- (map! :map (ido-common-completion-map ido-completion-map ido-file-completion-map)
- "C-n" #'ido-next-match
- "C-p" #'ido-prev-match
- "C-w" #'ido-delete-backward-word-updir))
- (add-hook 'ido-setup-hook #'+ido|init)
+ (define-key! (ido-common-completion-map ido-completion-map ido-file-completion-map)
+ "\C-n" #'ido-next-match
+ "\C-p" #'ido-prev-match
+ "\C-w" #'ido-delete-backward-word-updir
+ ;; Go to $HOME with ~
+ "~" (λ! (if (looking-back "/" (point-min))
+ (insert "~/")
+ (call-interactively #'self-insert-command))))
(defun +ido*sort-mtime ()
"Sort ido filelist by mtime instead of alphabetically."
@@ -55,10 +43,16 @@
(advice-add #'ido-sort-mtime :override #'+ido*sort-mtime)
(add-hook! (ido-make-file-list ido-make-dir-list) #'+ido*sort-mtime)
- (defun +ido|setup-home-keybind ()
- "Go to $HOME with ~"
- (define-key ido-file-completion-map (kbd "~")
- (λ! (if (looking-back "/" (point-min))
- (insert "~/")
- (call-interactively #'self-insert-command)))))
- (add-hook 'ido-setup-hook #'+ido|setup-home-keybind))
+ ;;
+ (ido-mode 1)
+ (ido-everywhere 1)
+ (ido-ubiquitous-mode 1)
+ (ido-vertical-mode 1)
+ (flx-ido-mode +1)
+ (crm-custom-mode +1)
+
+ ;;
+ (remove-hook 'ido-setup-hook #'+ido|init))
+
+;;
+(add-hook 'ido-setup-hook #'+ido|init)
diff --git a/modules/completion/ido/packages.el b/modules/completion/ido/packages.el
index 7d211f7d9..368665425 100644
--- a/modules/completion/ido/packages.el
+++ b/modules/completion/ido/packages.el
@@ -2,6 +2,6 @@
;;; completion/ido/packages.el
(package! flx-ido)
-(package! ido-ubiquitous)
+(package! ido-completing-read+)
(package! ido-vertical-mode)
(package! crm-custom)
diff --git a/modules/completion/ivy/README.org b/modules/completion/ivy/README.org
index 18f399739..f4702a357 100644
--- a/modules/completion/ivy/README.org
+++ b/modules/completion/ivy/README.org
@@ -1,128 +1,57 @@
-#+TITLE: :completion ivy
+#+TITLE: completion/ivy
+#+DATE: February 13, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
-This module adds Ivy, a completion backend.
+* Table of Contents :TOC_3:noexport:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+ - [[Hacks][Hacks]]
+- [[Prerequisites][Prerequisites]]
+ - [[Install][Install]]
+ - [[MacOS][MacOS]]
+ - [[Arch Linux][Arch Linux]]
+- [[Features][Features]]
+ - [[Jump-to-file project navigation][Jump-to-file project navigation]]
+ - [[Project search & replace][Project search & replace]]
+ - [[In-buffer searching][In-buffer searching]]
+ - [[Task lookup][Task lookup]]
+ - [[Ivy integration for various completing commands][Ivy integration for various completing commands]]
+ - [[General][General]]
+ - [[Jump to files, buffers or projects)][Jump to files, buffers or projects)]]
+ - [[Search][Search]]
+- [[Configuration][Configuration]]
+ - [[Enable fuzzy/non-fuzzy search for specific commands][Enable fuzzy/non-fuzzy search for specific commands]]
+ - [[Change the position of the ivy childframe][Change the position of the ivy childframe]]
+- [[Troubleshooting][Troubleshooting]]
+
+* Description
+This module provides Ivy integration for a variety of Emacs commands, as well as
+a unified interface for project search and replace, powered by ag, rg, pt,
+git-grep & grep (whichever is available).
#+begin_quote
I prefer ivy over ido for its flexibility. I prefer ivy over helm because it's
-lighter.
+lighter, simpler and faster in many cases.
#+end_quote
-+ Project-wide search & replace powered by ~rg~ or ~ag~
-+ Project jump-to navigation ala Command-T, Sublime Text's Jump-to-anywhere or
- Vim's CtrlP plugin.
-+ Ivy integration for ~M-x~, ~imenu~, ~recentf~ and others.
-+ A powerful, interactive in-buffer search using ~swiper~.
-+ Ivy-powered TODO/FIXME navigation
+** Module Flags
++ =+fuzzy= Enables the fuzzy method for ivy searches.
++ =+childframe= Causes Ivy to display in a floating child frame, above Emacs.
+ *This requires GUI Emacs 26.1+*
-* Table of Contents :TOC:
-- [[#install][Install]]
- - [[#macos][MacOS]]
- - [[#arch-linux][Arch Linux]]
-- [[#usage][Usage]]
- - [[#project-search--replace][Project search & replace]]
- - [[#jump-to-file-project-navigation][Jump-to-file project navigation]]
- - [[#in-buffer-searching][In-buffer searching]]
- - [[#task-lookup][Task lookup]]
-- [[#appendix][Appendix]]
- - [[#commands][Commands]]
- - [[#hacks][Hacks]]
-
-* Install
-This module optionally depends on [[https://github.com/BurntSushi/ripgrep][ripgrep]] and [[https://github.com/ggreer/the_silver_searcher][the_silver_searcher]].
-
-~rg~ is faster, but its results aren't deterministic, neither does it support
-multiline search or full PCRE (at the time of writing), that's where ~ag~ is
-useful.
-
-** MacOS
-#+BEGIN_SRC sh :tangle (if (doom-system-os 'macos) "yes")
-brew install ripgrep the_silver_searcher
-#+END_SRC
-
-** Arch Linux
-#+BEGIN_SRC sh :dir /sudo:: :tangle (if (doom-system-os 'arch) "yes")
-sudo pacman --needed --noconfirm -S ripgrep the_silver_searcher
-#+END_SRC
-
-* Usage
-Here is some insight into how I use this module.
-
-** Project search & replace
-There are four Ex interfaces for the silver searcher and ripgrep. They are:
-
-+ ~:ag[!]~
-+ ~:agcwd[!]~
-+ ~:rg[!]~
-+ ~:rgcwd[!]~
-
-The optional BANG tells ag/rg to include ignored files in the search. And the
-\*cwd variant of each command will only search in the current directory
-(non-recursively).
-
-[[/../screenshots/modules/completion/ivy/ivy-search.gif]]
-
-Now, how do we do text replacements? With the ivy popup open you can press
-=S+Tab= to create an wgrep buffer out of the results.
-
-[[/../screenshots/modules/completion/ivy/ivy-search-replace.gif]]
-
-Make your modifications and press =C-c C-c= to commit them, or =C-c C-k= to
-abort.
-
-** Jump-to-file project navigation
-Inspired by Sublime Text's jump-to-anywhere, Vim's CtrlP/Unite plugins, and
-Textmate's Command-T, a marriage of ~projectile~ and ~ivy~ makes this available
-in Emacs.
-
-Invoke it with =SPC f /=, =SPC SPC= or ~M-x counsel-projectile-find-file~.
-
-[[/../screenshots/modules/completion/ivy/ivy-projectile.gif]]
-
-** In-buffer searching
-I use ~evil-search~ (invoked by pressing =/= in normal mode) when jumping
-small/moderate (or predictable) distances. However, there are occasions where I
-need more feedback, so I turn to ~swiper~ (available directly with =M-x swiper
-RET=, or via ~:sw[iper]~).
-
-[[/../screenshots/modules/completion/ivy/ivy-swiper.gif]]
-
-** Task lookup
-I sprinkle my projects with TODO's & FIXME's. You can navigate to and peruse
-them via ~M-x +ivy/tasks~ or ~:todo[!]~ (ex command).
-
-[[/../screenshots/modules/completion/ivy/ivy-todo.gif]]
-
-* Appendix
-** Commands
-Here is a list of my commonly used commands, their default keybinds (defined in
-[[../../private/default/+bindings.el][private/default/+bindings.el]]), and their corresponding ex command (defined in
-[[../../private/default/+evil-commands.el][private/default/+evil-commands.el]]).
-
-| command | key / ex command | description |
-|-------------------------------------+------------------------+------------------------------------------------------------------|
-| ~counsel-M-x~ | =M-x= | Smarter, smex-powered M-x |
-| ~counsel-bookmark~ | =SPC RET= | Find bookmark |
-| ~counsel-find-file~ | =SPC f .= or =SPC .= | Browse from current directory |
-| ~counsel-projectile-find-file~ | =SPC f /= or =SPC SPC= | Find file in project |
-| ~counsel-projectile-switch-project~ | =SPC p p= | Open another project |
-| ~counsel-recentf~ | =SPC f r= | Find recently opened file |
-| ~ivy-switch-buffer~ | =SPC b b= | Jump to buffer in current workspace |
-| ~+ivy/switch-workspace-buffer~ | =SPC b B= | Jump to buffer across workspaces |
-| ~+ivy:ag~ | ~:ag[!] [QUERY]~ | Search project (BANG = ignore gitignore) |
-| ~+ivy:ag-cwd~ | ~:agcwd[!] [QUERY]~ | Search this directory (BANG = don't recurse into subdirectories) |
-| ~+ivy:rg~ | ~:rg[!] [QUERY]~ | Search project (if BANG, ignore gitignore) |
-| ~+ivy:rg-cwd~ | ~:rgcwd[!] [QUERY]~ | Search this directory (BANG = don't recurse into subdirectories) |
-| ~+ivy:swiper~ | ~:sw[iper] [QUERY]~ | Search current buffer |
-| ~+ivy:todo~ | ~:todo[!]~ | List all TODO/FIXMEs in project (or current file if BANG) |
-
-While in a search (e.g. invoked from ~+ivy:ag~ or ~+ivy:rg~), these new
-keybindings are available to you:
-
-| key | description |
-|-------------+--------------------------------------------------------------------------------|
-| == | Perform search/replace on the search results (open occur buffer in wgrep mode) |
-| =C-SPC= | Preview the current candidate |
-| =M-RET= | Open the selected candidate in other-window |
+** Plugins
++ [[https://github.com/abo-abo/swiper][ivy]]
++ [[https://github.com/abo-abo/swiper][counsel]]
++ [[https://github.com/ericdanan/counsel-projectile][counsel-projectile]]
++ [[https://github.com/abo-abo/swiper][swiper]]
++ [[https://github.com/abo-abo/swiper][ivy-hydra]]
++ [[https://github.com/yevgnen/ivy-rich][ivy-rich]]
++ [[https://github.com/mhayashi1120/Emacs-wgrep][wgrep]]
++ [[https://github.com/DarwinAwardWinner/amx][amx]]
++ [[https://github.com/lewang/flx][flx]]* (=+fuzzy=)
++ [[https://github.com/tumashu/ivy-posframe][ivy-posframe]]* (=+childframe=)
** Hacks
+ Functions with ivy/counsel equivalents have been globally remapped (like
@@ -131,4 +60,161 @@ keybindings are available to you:
+ ~counsel-[arp]g~'s 3-character limit was reduced to 1 (mainly for the ex
command)
+* Prerequisites
+This module optionally depends on one of:
++ [[https://github.com/BurntSushi/ripgrep][ripgrep]] (rg)
++ [[https://github.com/ggreer/the_silver_searcher][the_silver_searcher]] (ag)
++ [[https://github.com/monochromegane/the_platinum_searcher][the_platinum_searcher]] (pt)
+
+Ripgrep is recommended, but the order of its results aren't deterministic and it
+doesn't support full PCRE (at the time of writing). The_silver_searcher is a
+good alternative if either of these bother you.
+
+If none of these are installed, file search commands will use git-grep (falling
+back to grep, otherwise).
+
+** Install
+*** MacOS
+#+BEGIN_SRC sh
+brew install ripgrep the_silver_searcher
+#+END_SRC
+
+*** Arch Linux
+#+BEGIN_SRC sh :dir /sudo::
+sudo pacman --needed --noconfirm -S ripgrep the_silver_searcher
+#+END_SRC
+
+* Features
+Ivy and its ilk are large plugins. Covering everything about them is outside of
+this documentation's scope, so only Doom-specific Ivy features are listed here:
+
+** Jump-to-file project navigation
+Inspired by Sublime Text's jump-to-anywhere, CtrlP/Unite in Vim, and Textmate's
+Command-T, this module provides similar functionality by bringing ~projectile~
+and ~ivy~ together.
+
+https://assets.doomemacs.org/completion/ivy/projectile.png
+
+| Keybind | Description |
+|----------------------+-------------------------------------|
+| =SPC f /=, =SPC SPC= | Jump to file in project |
+| =SPC f .=, =SPC .= | Jump to file from current directory |
+
+** Project search & replace
+This module provides interactive text search and replace using the first search
+program available on your system (rg, ag, pt, git-grep or grep).
+
+| Keybind | Description |
+|----------------------+-------------------------------------|
+| =SPC / b=, =M-f= | Search the current buffer |
+| =SPC / p= | Search project |
+| =SPC / d= | Search this directory |
+| =SPC p t= | List all TODO/FIXMEs in project |
+
+https://assets.doomemacs.org/completion/ivy/search.png
+
+The ~+ivy-project-search-engines~ variable is consulted to determine which
+underlying program to check for (and in what order). It's default value is ~'(rg
+ag pt)~. If none of these are available, it will resort to =git-grep= (falling
+back to =grep= after that).
+
+To use a specific program, the following engine-specific commands are available
+(but not bound to any key by default) for searching from the project root or the
+current directory (recursively), respectively:
+
++ ~+ivy/ag~ / ~+ivy/ag-from-cwd~
++ ~+ivy/rg~ / ~+ivy/rg-from-cwd~
++ ~+ivy/pt~ / ~+ivy/pt-from-cwd~
++ ~+ivy/grep~ / ~+ivy/grep-from-cwd~
+
+The universal argument (=SPC u= for evil users; =C-u= otherwise) changes the
+behavior of these commands, instructing the underlying search engine to include
+ignored files.
+
+This module also provides Ex Commands for evil users:
+
+| Ex command | Description |
+|-----------------------+------------------------------------------------|
+| ~:ag[!] [QUERY]~ | Search project w/ ag[fn:1] |
+| ~:rg[!] [QUERY]~ | Search project w/ rg[fn:1] |
+| ~:pt[!] [QUERY]~ | Search project w/ pt[fn:1] |
+| ~:grep[!] [QUERY]~ | Search project w/ git-grep/grep[fn:1] |
+| ~:agcwd[!] [QUERY]~ | Search this directory w/ the_silver_searcher |
+| ~:rgcwd[!] [QUERY]~ | Search this directory w/ ripgrep |
+| ~:ptcwd[!] [QUERY]~ | Search this directory w/ the_platinum_searcher |
+| ~:grepcwd[!] [QUERY]~ | Search this directory w/ git-grep/grep |
+
+The optional BANG functions is equivalent to the universal argument for the
+previous commands.
+
+-----
+
+While in a search (e.g. invoked from ~+ivy:ag~ or ~:rg~), these extra
+keybindings are available to you:
+
+| Keybind | Description |
+|---------+------------------------------------------------|
+| =S-TAB= | Open a writable buffer of your search results |
+| =C-SPC= | Preview the current candidate |
+| =M-RET= | Open the selected candidate in other-window |
+
+Changes to the resulting wgrep buffer (opened by =S-TAB=) can be committed with
+=C-c C-c= and aborted with =C-c C-k=.
+
+https://assets.doomemacs.org/completion/ivy/search-replace.png
+
+** In-buffer searching
+The =swiper= package provides an interactive buffer search powered by ivy. It
+can be invoked with:
+
++ =SPC / b=
++ =M-f=
++ ~:sw[iper] [QUERY]~
+
+https://assets.doomemacs.org/completion/ivy/swiper.png
+
+A wgrep buffer can be opened from swiper with =S-TAB=.
+
+** Task lookup
+Some projects have TODO's and FIXME's littered across them. The ~+ivy/tasks~
+command allows you to search and jump to them. It can be invoked with:
+
++ =SPC p t= (C-u = restrict search to current file)
++ ~:todo[!]~ (BANG = restrict search to current file)
+
+https://assets.doomemacs.org/completion/ivy/todo.png
+
+** Ivy integration for various completing commands
+*** General
+| Keybind | Description |
+|----------------+---------------------------|
+| =M-x=, =SPC := | Smarter, smex-powered M-x |
+| =SPC '= | Resume last ivy session |
+
+*** Jump to files, buffers or projects)
+| Keybind | Description |
+|---------------------------------+---------------------------------------|
+| =SPC RET= | Find bookmark |
+| =SPC f .=, =SPC .= | Browse from current directory |
+| =SPC f /=, =SPC p /=, =SPC SPC= | Find file in project |
+| =SPC f r= | Find recently opened file |
+| =SPC p p= | Open another project |
+| =SPC b b=, =SPC ,= | Switch to buffer in current workspace |
+| =SPC b B=, =SPC <= | Switch to buffer |
+
+*** Search
+| Keybind | Description |
+|------------------+------------------------------------------|
+| =SPC / i= | Search for symbol in current buffer |
+| =SPC / I= | Search for symbol in all similar buffers |
+| =SPC / b=, =M-f= | Search the current buffer |
+| =SPC / p= | Search project |
+| =SPC / d= | Search this directory |
+| =SPC p t= | List all TODO/FIXMEs in project |
+
+* Configuration
+** TODO Enable fuzzy/non-fuzzy search for specific commands
+** TODO Change the position of the ivy childframe
+
+* TODO Troubleshooting
diff --git a/modules/completion/ivy/autoload/evil.el b/modules/completion/ivy/autoload/evil.el
index b287a273a..28a83c11f 100644
--- a/modules/completion/ivy/autoload/evil.el
+++ b/modules/completion/ivy/autoload/evil.el
@@ -14,94 +14,55 @@
(+ivy/tasks bang))
-;; --- file searching ---------------------
+;;
+;; Project searching
-(defvar +ivy--file-last-search nil)
-(defvar +ivy--file-search-recursion-p t)
-(defvar +ivy--file-search-all-files-p nil)
+;;;###autoload (autoload '+ivy:pt "completion/ivy/autoload/evil" nil t)
+(evil-define-command +ivy:pt (all-files-p query)
+ "Ex interface for `+ivy/pt'"
+ (interactive "")
+ (+ivy/pt all-files-p query))
-(defun +ivy--file-search (engine beg end query &optional directory)
- (let* ((project-root (doom-project-root))
- (directory (or directory project-root))
- (recursion-p +ivy--file-search-recursion-p)
- (all-files-p +ivy--file-search-all-files-p)
- (engine (or engine
- (and (executable-find "rg") 'rg)
- (and (executable-find "ag") 'ag)))
- (query
- (or query
- (if (evil-visual-state-p)
- (and beg end
- (> (abs (- end beg)) 1)
- (rxt-quote-pcre (buffer-substring-no-properties beg end)))
- +ivy--file-last-search)
- +ivy--file-last-search))
- (prompt
- (format "%s%%s %s"
- (symbol-name engine)
- (cond ((equal directory default-directory)
- "./")
- ((equal directory project-root)
- (projectile-project-name))
- (t
- (file-relative-name directory project-root))))))
- (setq +ivy--file-last-search query)
- (pcase engine
- ('ag
- (let ((args (concat
- (if all-files-p " -a")
- (unless recursion-p " -n"))))
- (counsel-ag query directory args (format prompt args))))
- ('rg
- ;; smart-case instead of case-insensitive flag
- (let ((counsel-rg-base-command
- (replace-regexp-in-string " -i " " -S " counsel-rg-base-command))
- (args (concat
- (if all-files-p " -uu")
- (unless recursion-p " --maxdepth 1"))))
- (counsel-rg query directory args (format prompt args))))
- ('pt) ;; TODO pt search engine (necessary?)
- (_ (error "No search engine specified")))))
+;;;###autoload (autoload '+ivy:grep "completion/ivy/autoload/evil" nil t)
+(evil-define-command +ivy:grep (all-files-p query)
+ "Ex interface for `+ivy/grep'"
+ (interactive "")
+ (+ivy/grep all-files-p query))
;;;###autoload (autoload '+ivy:ag "completion/ivy/autoload/evil" nil t)
-(evil-define-operator +ivy:ag (beg end query &optional all-files-p directory)
- "Perform a project file search using the silver search. QUERY is a pcre
-regexp. If omitted, the current selection is used. If no selection is active,
-the last known search is used.
-
-If ALL-FILES-P, don't respect .gitignore files and search everything."
- (interactive "")
- (let ((+ivy--file-search-all-files-p all-files-p))
- (+ivy--file-search 'ag beg end query directory)))
+(evil-define-command +ivy:ag (all-files-p query)
+ "Ex interface for `+ivy/ag'"
+ (interactive "")
+ (+ivy/ag all-files-p query))
;;;###autoload (autoload '+ivy:rg "completion/ivy/autoload/evil" nil t)
-(evil-define-operator +ivy:rg (beg end query &optional all-files-p directory)
- "Perform a project file search using ripgrep. QUERY is a regexp. If omitted,
-the current selection is used. If no selection is active, the last known search
-is used.
-
-If ALL-FILES-P, don't respect .gitignore files and search everything.
-
-NOTE: ripgrep doesn't support multiline searches (yet)."
- (interactive "")
- (let ((+ivy--file-search-all-files-p all-files-p))
- (+ivy--file-search 'rg beg end query directory)))
+(evil-define-command +ivy:rg (all-files-p query)
+ "Ex interface for `+ivy/rg'"
+ (interactive "")
+ (+ivy/rg all-files-p query))
-;;;###autoload (autoload '+ivy:ag-cwd "completion/ivy/autoload/evil" nil t)
-(evil-define-operator +ivy:ag-cwd (beg end query &optional bang)
- "The same as :ag, but searches the current directory. If BANG, don't recurse
-into sub-directories."
- (interactive "")
- (let ((+ivy--file-search-recursion-p (not bang)))
- (+ivy:ag beg end query t default-directory)))
+;;;###autoload (autoload '+ivy:pt-from-cwd "completion/ivy/autoload/evil" nil t)
+(evil-define-command +ivy:pt-from-cwd (query &optional recurse-p)
+ "Ex interface for `+ivy/pt-from-cwd'."
+ (interactive "")
+ (+ivy/pt-from-cwd (not recurse-p) query))
-;;;###autoload (autoload '+ivy:rg-cwd "completion/ivy/autoload/evil" nil t)
-(evil-define-operator +ivy:rg-cwd (beg end query &optional bang)
- "The same as :rg, but only searches the current directory. If BANG, don't
-recurse into sub-directories.
+;;;###autoload (autoload '+ivy:grep-from-cwd "completion/ivy/autoload/evil" nil t)
+(evil-define-command +ivy:grep-from-cwd (query &optional recurse-p)
+ "Ex interface for `+ivy/grep-from-cwd'."
+ (interactive "")
+ (+ivy/grep-from-cwd (not recurse-p) query))
+
+;;;###autoload (autoload '+ivy:ag-from-cwd "completion/ivy/autoload/evil" nil t)
+(evil-define-command +ivy:ag-from-cwd (query &optional recurse-p)
+ "Ex interface for `+ivy/ag-from-cwd'."
+ (interactive "")
+ (+ivy/ag-from-cwd (not recurse-p) query))
+
+;;;###autoload (autoload '+ivy:rg-from-cwd "completion/ivy/autoload/evil" nil t)
+(evil-define-command +ivy:rg-from-cwd (query &optional recurse-p)
+ "Ex interface for `+ivy/rg-from-cwd'."
+ (interactive "")
+ (+ivy/rg-from-cwd (not recurse-p) query))
-NOTE: ripgrep doesn't support multiline searches (yet)."
- (interactive "")
- (let ((+ivy--file-search-recursion-p (not bang)))
- (+ivy:rg beg end query t default-directory)))
diff --git a/modules/completion/ivy/autoload/hydras.el b/modules/completion/ivy/autoload/hydras.el
new file mode 100644
index 000000000..afe09e393
--- /dev/null
+++ b/modules/completion/ivy/autoload/hydras.el
@@ -0,0 +1,31 @@
+;;; completion/ivy/autoload/hydras.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(after! ivy-hydra
+ (defhydra+ hydra-ivy (:hint nil :color pink)
+ "
+ Move ^^^^^^^^^^ | Call ^^^^ | Cancel^^ | Options^^ | Action _w_/_s_/_a_: %s(ivy-action-name)
+----------^^^^^^^^^^-+--------------^^^^-+-------^^-+--------^^-+---------------------------------
+ _g_ ^ ^ _k_ ^ ^ _u_ | _f_orward _o_ccur | _i_nsert | _c_alling: %-7s(if ivy-calling \"on\" \"off\") _C_ase-fold: %-10`ivy-case-fold-search
+ ^↨^ _h_ ^+^ _l_ ^↕^ | _RET_ done ^^ | _q_uit | _m_atcher: %-7s(ivy--matcher-desc) _t_runcate: %-11`truncate-lines
+ _G_ ^ ^ _j_ ^ ^ _d_ | _TAB_ alt-done ^^ | ^ ^ | _<_/_>_: shrink/grow
+"
+ ;; arrows
+ ("l" ivy-alt-done)
+ ("h" ivy-backward-delete-char)
+ ("g" ivy-beginning-of-buffer)
+ ("G" ivy-end-of-buffer)
+ ("d" ivy-scroll-up-command)
+ ("u" ivy-scroll-down-command)
+ ("e" ivy-scroll-down-command)
+ ;; actions
+ ("q" keyboard-escape-quit :exit t)
+ ("" keyboard-escape-quit :exit t)
+ ("TAB" ivy-alt-done :exit nil)
+ ("RET" ivy-done :exit t)
+ ("C-SPC" ivy-call-and-recenter :exit nil)
+ ("f" ivy-call)
+ ("c" ivy-toggle-calling)
+ ("m" ivy-toggle-fuzzy)
+ ("t" (setq truncate-lines (not truncate-lines)))
+ ("o" ivy-occur :exit t)))
diff --git a/modules/completion/ivy/autoload/ivy.el b/modules/completion/ivy/autoload/ivy.el
index 8a008348d..9d5eb107b 100644
--- a/modules/completion/ivy/autoload/ivy.el
+++ b/modules/completion/ivy/autoload/ivy.el
@@ -1,36 +1,91 @@
;;; completion/ivy/autoload/ivy.el -*- lexical-binding: t; -*-
-(defsubst +ivy--icon-for-mode (mode)
- "Apply `all-the-icons-for-mode' on MODE but either return an icon or nil."
- (let ((icon (all-the-icons-icon-for-mode mode)))
- (unless (symbolp icon) icon)))
+(defun +ivy--is-workspace-buffer-p (buffer)
+ (let ((buffer (car buffer)))
+ (when (stringp buffer)
+ (setq buffer (get-buffer buffer)))
+ (+workspace-contains-buffer-p buffer)))
+
+(defun +ivy--is-workspace-other-buffer-p (buffer)
+ (let ((buffer (car buffer)))
+ (when (stringp buffer)
+ (setq buffer (get-buffer buffer)))
+ (and (not (eq buffer (current-buffer)))
+ (+workspace-contains-buffer-p buffer))))
;;;###autoload
-(defun +ivy-buffer-transformer (str)
- (let* ((buf (get-buffer str))
- (path (buffer-file-name buf))
- (mode (buffer-local-value 'major-mode buf))
- (faces
- (with-current-buffer buf
- (cond ((string-match-p "^ ?\\*" (buffer-name buf))
- 'font-lock-comment-face)
- ((buffer-modified-p buf)
- 'doom-modeline-buffer-modified)
- (buffer-read-only
- 'error)))))
- (propertize
- (format "%-40s %s%-20s %s"
- str
- (if +ivy-buffer-icons
- (concat (propertize " " 'display
- (or (+ivy--icon-for-mode mode)
- (+ivy--icon-for-mode (get mode 'derived-mode-parent))))
- "\t")
- "")
- mode
- (or (and path (abbreviate-file-name (file-name-directory (file-truename path))))
- ""))
- 'face faces)))
+(defun +ivy-rich-buffer-name (candidate)
+ "Display the buffer name.
+
+Buffers that are considered unreal (see `doom-real-buffer-p') are dimmed with
+`+ivy-buffer-unreal-face'."
+ (let ((b (get-buffer candidate)))
+ (cond ((ignore-errors
+ (file-remote-p
+ (buffer-local-value 'default-directory b)))
+ (ivy-append-face candidate 'ivy-remote))
+ ((doom-unreal-buffer-p b)
+ (ivy-append-face candidate +ivy-buffer-unreal-face))
+ ((not (buffer-file-name b))
+ (ivy-append-face candidate 'ivy-subdir))
+ ((buffer-modified-p b)
+ (ivy-append-face candidate 'ivy-modified-buffer))
+ (candidate))))
+
+;;;###autoload
+(defun +ivy-rich-buffer-icon (candidate)
+ "Display the icon for CANDIDATE buffer.
+
+Otherwise show the fundamental-mode icon."
+ (with-current-buffer candidate
+ (let ((icon (all-the-icons-icon-for-mode major-mode)))
+ (if (symbolp icon)
+ (all-the-icons-icon-for-mode 'fundamental-mode)
+ icon))))
+
+
+;;
+;; Library
+
+(defun +ivy--switch-buffer-preview ()
+ (let (ivy-use-virtual-buffers ivy--virtual-buffers)
+ (counsel--switch-buffer-update-fn)))
+
+(defalias '+ivy--switch-buffer-preview-all #'counsel--switch-buffer-update-fn)
+(defalias '+ivy--switch-buffer-unwind #'counsel--switch-buffer-unwind)
+
+(defun +ivy--switch-buffer (workspace other)
+ (let ((current (not other))
+ prompt action filter update unwind)
+ (cond ((and workspace current)
+ (setq prompt "Switch to workspace buffer: "
+ action #'ivy--switch-buffer-action
+ filter #'+ivy--is-workspace-other-buffer-p))
+ (workspace
+ (setq prompt "Switch to workspace buffer in other window: "
+ action #'ivy--switch-buffer-other-window-action
+ filter #'+ivy--is-workspace-buffer-p))
+ (current
+ (setq prompt "Switch to buffer: "
+ action #'ivy--switch-buffer-action))
+ ((setq prompt "Switch to buffer in other window: "
+ action #'ivy--switch-buffer-other-window-action)))
+ (when +ivy-buffer-preview
+ (cond ((not (and ivy-use-virtual-buffers
+ (eq +ivy-buffer-preview 'everything)))
+ (setq update #'+ivy--switch-buffer-preview
+ unwind #'+ivy--switch-buffer-unwind))
+ ((setq update #'+ivy--switch-buffer-preview-all
+ unwind #'+ivy--switch-buffer-unwind))))
+ (ivy-read prompt 'internal-complete-buffer
+ :action action
+ :predicate filter
+ :update-fn update
+ :unwind unwind
+ :preselect (buffer-name (other-buffer (current-buffer)))
+ :matcher #'ivy--switch-buffer-matcher
+ :keymap ivy-switch-buffer-map
+ :caller #'+ivy--switch-buffer)))
;;;###autoload
(defun +ivy/switch-workspace-buffer (&optional arg)
@@ -38,14 +93,25 @@
If ARG (universal argument), open selection in other-window."
(interactive "P")
- (ivy-read "Switch to workspace buffer: "
- (mapcar #'buffer-name (delq (current-buffer) (doom-buffer-list)))
- :action (if arg
- #'ivy--switch-buffer-other-window-action
- #'ivy--switch-buffer-action)
- :matcher #'ivy--switch-buffer-matcher
- :keymap ivy-switch-buffer-map
- :caller #'+ivy/switch-workspace-buffer))
+ (+ivy--switch-buffer t arg))
+
+;;;###autoload
+(defun +ivy/switch-workspace-buffer-other-window ()
+ "Switch another window to a buffer within the current workspace."
+ (interactive)
+ (+ivy--switch-buffer t t))
+
+;;;###autoload
+(defun +ivy/switch-buffer ()
+ "Switch to another buffer."
+ (interactive)
+ (+ivy--switch-buffer nil nil))
+
+;;;###autoload
+(defun +ivy/switch-buffer-other-window ()
+ "Switch to another buffer in another window."
+ (interactive)
+ (+ivy--switch-buffer nil t))
(defun +ivy--tasks-candidates (tasks)
"Generate a list of task tags (specified by `+ivy-task-tags') for
@@ -93,9 +159,9 @@ If ARG (universal argument), open selection in other-window."
"\\):?\\s-*\\(.+\\)")
x)
(error
- (message! (red "Error matching task in file: (%s) %s"
- (error-message-string ex)
- (car (split-string x ":"))))
+ (print! (red "Error matching task in file: (%s) %s")
+ (error-message-string ex)
+ (car (split-string x ":")))
nil))
collect `((type . ,(match-string 3 x))
(desc . ,(match-string 4 x))
@@ -124,44 +190,13 @@ search current file. See `+ivy-task-tags' to customize what this searches for."
(if arg
(concat "in: " (file-relative-name buffer-file-name))
"project"))
- (+ivy--tasks-candidates
- (+ivy--tasks (if arg buffer-file-name (doom-project-root))))
+ (let ((tasks (+ivy--tasks (if arg buffer-file-name (doom-project-root)))))
+ (unless tasks
+ (user-error "No tasks in your project! Good job!"))
+ (+ivy--tasks-candidates tasks))
:action #'+ivy--tasks-open-action
:caller '+ivy/tasks))
-;;;###autoload
-(defun +ivy*counsel-ag-function (string base-cmd extra-ag-args)
- "Advice to 1) get rid of the character limit from `counsel-ag-function' and 2)
-disable ivy's over-zealous parentheses quoting behavior (if i want literal
-parentheses, I'll escape them myself).
-
-NOTE This may need to be updated frequently, to meet changes upstream (in
-counsel-rg)."
- (when (null extra-ag-args)
- (setq extra-ag-args ""))
- (if (< (length string) 1) ;; #1
- (counsel-more-chars 1)
- (let ((default-directory counsel--git-dir)
- (regex (counsel-unquote-regex-parens
- (setq ivy--old-re
- (ivy--regex string)))))
- (let* ((args-end (string-match " -- " extra-ag-args))
- (file (if args-end
- (substring-no-properties extra-ag-args (+ args-end 3))
- ""))
- (extra-ag-args (if args-end
- (substring-no-properties extra-ag-args 0 args-end)
- extra-ag-args))
- (ag-cmd (format base-cmd
- (concat extra-ag-args
- " -- "
- (shell-quote-argument regex)
- file))))
- (if (file-remote-p default-directory)
- (split-string (shell-command-to-string ag-cmd) "\n" t)
- (counsel--async-command ag-cmd)
- nil)))))
-
;;;###autoload
(defun +ivy/wgrep-occur ()
"Invoke the search+replace wgrep buffer on the current ag/rg search results."
@@ -200,7 +235,7 @@ counsel-rg)."
(with-ivy-window
(let ((file-name (match-string-no-properties 1 x))
(line-number (match-string-no-properties 2 x)))
- (find-file-other-window (expand-file-name file-name counsel--git-dir))
+ (find-file-other-window (expand-file-name file-name (ivy-state-directory ivy-last)))
(goto-char (point-min))
(forward-line (1- (string-to-number line-number)))
(re-search-forward (ivy--regex ivy-text t) (line-end-position) t)
@@ -208,10 +243,169 @@ counsel-rg)."
(selected-window))))))
;;;###autoload
-(defun +ivy-quit-and-resume ()
- "Close the current popup window and resume ivy."
- (interactive)
- (when (doom-popup-p)
- (doom/popup-close))
- (ivy-resume))
+(defun +ivy-confirm-delete-file (x)
+ (dired-delete-file x 'confirm-each-subdirectory))
+
+;;
+;; File searching
+
+;;;###autoload
+(defun +ivy/projectile-find-file ()
+ "A more sensible `counsel-projectile-find-file', which will revert to
+`counsel-find-file' if invoked from $HOME, `counsel-file-jump' if invoked from a
+non-project, `projectile-find-file' if in a big project (more than
+`ivy-sort-max-size' files), or `counsel-projectile-find-file' otherwise.
+
+The point of this is to avoid Emacs locking up indexing massive file trees."
+ (interactive)
+ (call-interactively
+ (cond ((or (file-equal-p default-directory "~")
+ (when-let* ((proot (doom-project-root)))
+ (file-equal-p proot "~")))
+ #'counsel-find-file)
+
+ ((doom-project-p)
+ (let ((files (projectile-current-project-files)))
+ (if (<= (length files) ivy-sort-max-size)
+ #'counsel-projectile-find-file
+ #'projectile-find-file)))
+
+ (#'counsel-file-jump))))
+
+;;;###autoload
+(cl-defun +ivy-file-search (engine &key query in all-files (recursive t))
+ "Conduct a file search using ENGINE, which can be any of: rg, ag, pt, and
+grep. If omitted, ENGINE will default to the first one it detects, in that
+order.
+
+:query STRING
+ Determines the initial input to search for.
+:in PATH
+ Sets what directory to base the search out of. Defaults to the current
+ project's root.
+:recursive BOOL
+ Whether or not to search files recursively from the base directory."
+ (declare (indent defun))
+ (let* ((project-root (or (doom-project-root) default-directory))
+ (directory (or in project-root))
+ (default-directory directory)
+ (engine (or engine
+ (cl-loop for tool in +ivy-project-search-engines
+ if (executable-find (symbol-name tool))
+ return tool)
+ (and (or (executable-find "grep")
+ (executable-find "git"))
+ 'grep)
+ (error "No search engine specified (is ag, rg, pt or git installed?)")))
+ (query
+ (or (if query (rxt-quote-pcre query))
+ (when (use-region-p)
+ (let ((beg (or (bound-and-true-p evil-visual-beginning) (region-beginning)))
+ (end (or (bound-and-true-p evil-visual-end) (region-end))))
+ (when (> (abs (- end beg)) 1)
+ (rxt-quote-pcre (buffer-substring-no-properties beg end)))))))
+ (prompt
+ (format "%s%%s %s"
+ (symbol-name engine)
+ (cond ((equal directory default-directory)
+ "./")
+ ((equal directory project-root)
+ (projectile-project-name))
+ ((file-relative-name directory project-root))))))
+ (require 'counsel)
+ (let ((ivy-more-chars-alist
+ (if query '((t . 1)) ivy-more-chars-alist)))
+ (pcase engine
+ ('grep
+ (let ((args (if recursive " -R"))
+ (counsel-projectile-grep-initial-input query))
+ (if all-files
+ (cl-letf (((symbol-function #'projectile-ignored-directories-rel)
+ (symbol-function #'ignore))
+ ((symbol-function #'projectile-ignored-files-rel)
+ (symbol-function #'ignore)))
+ (counsel-projectile-grep args))
+ (counsel-projectile-grep args))))
+ ('ag
+ (let ((args (concat (if all-files " -a")
+ (unless recursive " --depth 1"))))
+ (counsel-ag query directory args (format prompt args))))
+ ('rg
+ (let ((args (concat (if all-files " -uu")
+ (unless recursive " --maxdepth 1"))))
+ (counsel-rg query directory args (format prompt args))))
+ ('pt
+ (let ((counsel-pt-base-command
+ (concat counsel-pt-base-command
+ (if all-files " -U")
+ (unless recursive " --depth=1")))
+ (default-directory directory))
+ (counsel-pt query)))
+ (_ (error "No search engine specified"))))))
+
+(defun +ivy--get-command (format)
+ (cl-loop for tool in (cl-remove-duplicates +ivy-project-search-engines :from-end t)
+ if (executable-find (symbol-name tool))
+ return (intern (format format tool))))
+
+;;;###autoload
+(defun +ivy/project-search (&optional arg initial-query directory)
+ "Performs a project search from the project root.
+
+Uses the first available search backend from `+ivy-project-search-engines'. If
+ARG (universal argument), include all files, even hidden or compressed ones, in
+the search."
+ (interactive "P")
+ (funcall (or (+ivy--get-command "+ivy/%s")
+ #'+ivy/grep)
+ arg
+ initial-query
+ directory))
+
+;;;###autoload
+(defun +ivy/project-search-from-cwd (&optional arg initial-query)
+ "Performs a project search recursively from the current directory.
+
+Uses the first available search backend from `+ivy-project-search-engines'. If
+ARG (universal argument), include all files, even hidden or compressed ones."
+ (interactive "P")
+ (funcall (or (+ivy--get-command "+ivy/%s-from-cwd")
+ #'+ivy/grep-from-cwd)
+ arg
+ initial-query))
+
+
+;;;###autoload (autoload '+ivy/rg "completion/ivy/autoload/ivy")
+;;;###autoload (autoload '+ivy/rg-from-cwd "completion/ivy/autoload/ivy")
+;;;###autoload (autoload '+ivy/ag "completion/ivy/autoload/ivy")
+;;;###autoload (autoload '+ivy/ag-from-cwd "completion/ivy/autoload/ivy")
+;;;###autoload (autoload '+ivy/pt "completion/ivy/autoload/ivy")
+;;;###autoload (autoload '+ivy/pt-from-cwd "completion/ivy/autoload/ivy")
+;;;###autoload (autoload '+ivy/grep "completion/ivy/autoload/ivy")
+;;;###autoload (autoload '+ivy/grep-from-cwd "completion/ivy/autoload/ivy")
+
+(dolist (engine `(,@(cl-remove-duplicates +ivy-project-search-engines :from-end t) grep))
+ (defalias (intern (format "+ivy/%s" engine))
+ (lambda (all-files-p &optional query directory)
+ (interactive "P")
+ (+ivy-file-search engine :query query :in directory :all-files all-files-p))
+ (format "Perform a project file search using %s.
+
+QUERY is a regexp. If omitted, the current selection is used. If no selection is
+active, the last known search is used.
+
+If ALL-FILES-P, search compressed and hidden files as well."
+ engine))
+
+ (defalias (intern (format "+ivy/%s-from-cwd" engine))
+ (lambda (all-files-p &optional query)
+ (interactive "P")
+ (+ivy-file-search engine :query query :in default-directory :all-files all-files-p))
+ (format "Perform a project file search from the current directory using %s.
+
+QUERY is a regexp. If omitted, the current selection is used. If no selection is
+active, the last known search is used.
+
+If ALL-FILES-P, search compressed and hidden files as well."
+ engine)))
diff --git a/modules/completion/ivy/autoload/posframe.el b/modules/completion/ivy/autoload/posframe.el
new file mode 100644
index 000000000..0b31f9a6b
--- /dev/null
+++ b/modules/completion/ivy/autoload/posframe.el
@@ -0,0 +1,16 @@
+;;; completion/ivy/autoload/posframe.el -*- lexical-binding: t; -*-
+;;;###if (featurep! +childframe)
+
+;;;###autoload
+(defun +ivy-display-at-frame-center-near-bottom (str)
+ "TODO"
+ (ivy-posframe--display str #'+ivy-poshandler-frame-center-near-bottom))
+
+;;;###autoload
+(defun +ivy-poshandler-frame-center-near-bottom (info)
+ "TODO"
+ (let ((parent-frame (plist-get info :parent-frame))
+ (pos (posframe-poshandler-frame-center info)))
+ (cons (car pos)
+ (truncate (/ (frame-pixel-height parent-frame) 2)))))
+
diff --git a/modules/completion/ivy/config.el b/modules/completion/ivy/config.el
index fa707c5ea..992b8aac2 100644
--- a/modules/completion/ivy/config.el
+++ b/modules/completion/ivy/config.el
@@ -3,12 +3,32 @@
(defvar +ivy-buffer-icons nil
"If non-nil, show buffer mode icons in `ivy-switch-buffer' and the like.")
+(defvar +ivy-buffer-preview nil
+ "If non-nil, preview buffers while switching, à la `counsel-switch-buffer'.
+
+When nil, don't preview anything.
+When non-nil, preview non-virtual buffers.
+When 'everything, also preview virtual buffers")
+
(defvar +ivy-task-tags
'(("TODO" . warning)
("FIXME" . error))
"An alist of tags for `+ivy/tasks' to include in its search, whose CDR is the
face to render it with.")
+(defvar +ivy-project-search-engines '(rg ag pt)
+ "What search tools for `+ivy/project-search' (and `+ivy-file-search' when no
+ENGINE is specified) to try, and in what order.
+
+To disable a particular tool, remove it from this list. To prioritize a tool
+over others, move it to the front of the list. Later duplicates in this list are
+silently ignored.
+
+If you want to already use git-grep or grep, set this to nil.")
+
+(defvar +ivy-buffer-unreal-face 'font-lock-comment-face
+ "The face for unreal buffers in `ivy-switch-to-buffer'.")
+
(defmacro +ivy-do-action! (action)
"Returns an interactive lambda that sets the current ivy action and
immediately runs it on the current candidate (ending the ivy session)."
@@ -20,133 +40,274 @@ immediately runs it on the current candidate (ending the ivy session)."
;;
-;; Packages
-;;
+;;; Packages
(def-package! ivy
- :init
- (add-hook 'doom-post-init-hook #'ivy-mode)
+ :defer 1
+ :after-call pre-command-hook
:config
- (setq ivy-height 12
- ivy-do-completion-in-region nil
+ (setq ivy-height 15
ivy-wrap t
ivy-fixed-height-minibuffer t
projectile-completion-system 'ivy
- smex-completion-method 'ivy
;; Don't use ^ as initial input
ivy-initial-inputs-alist nil
;; highlight til EOL
ivy-format-function #'ivy-format-function-line
;; disable magic slash on non-match
- ivy-magic-slash-non-match-action nil)
+ ivy-magic-slash-non-match-action nil
+ ;; don't show recent files in switch-buffer
+ ivy-use-virtual-buffers nil
+ ;; ...but if that ever changes, show their full path
+ ivy-virtual-abbreviate 'full
+ ;; don't quit minibuffer on delete-error
+ ivy-on-del-error-function nil
+ ;; enable ability to select prompt (alternative to `ivy-immediate-done')
+ ivy-use-selectable-prompt t)
- (after! magit (setq magit-completing-read-function #'ivy-completing-read))
- (after! yasnippet (push #'+ivy-yas-prompt yas-prompt-functions))
+ (after! yasnippet
+ (add-to-list 'yas-prompt-functions #'+ivy-yas-prompt nil #'eq))
- (map! [remap apropos] #'counsel-apropos
- [remap describe-face] #'counsel-describe-face
- [remap find-file] #'counsel-find-file
- [remap switch-to-buffer] #'ivy-switch-buffer
- [remap persp-switch-to-buffer] #'+ivy/switch-workspace-buffer
- [remap recentf-open-files] #'counsel-recentf
- [remap imenu] #'counsel-imenu
- [remap bookmark-jump] #'counsel-bookmark
- [remap projectile-find-file] #'counsel-projectile-find-file
- [remap imenu-anywhere] #'ivy-imenu-anywhere
- [remap execute-extended-command] #'counsel-M-x
- [remap describe-face] #'counsel-describe-face)
+ (define-key! ivy-mode-map
+ [remap switch-to-buffer] #'+ivy/switch-buffer
+ [remap switch-to-buffer-other-window] #'+ivy/switch-buffer-other-window
+ [remap persp-switch-to-buffer] #'+ivy/switch-workspace-buffer
+ [remap imenu-anywhere] #'ivy-imenu-anywhere)
- ;; Show more buffer information in switch-buffer commands
- (ivy-set-display-transformer #'ivy-switch-buffer #'+ivy-buffer-transformer)
- (ivy-set-display-transformer #'ivy-switch-buffer-other-window #'+ivy-buffer-transformer)
- (ivy-set-display-transformer #'+ivy/switch-workspace-buffer #'+ivy-buffer-transformer)
- (ivy-set-display-transformer #'counsel-recentf #'abbreviate-file-name)
+ (ivy-mode +1)
- (nconc ivy-sort-functions-alist
- '((persp-kill-buffer . nil)
- (persp-remove-buffer . nil)
- (persp-add-buffer . nil)
- (persp-switch . nil)
- (persp-window-switch . nil)
- (persp-frame-switch . nil)
- (+workspace/switch-to . nil)
- (+workspace/delete . nil))))
+ (def-package! ivy-hydra
+ :commands (ivy-dispatching-done-hydra ivy--matcher-desc ivy-hydra/body)
+ :init
+ (define-key! ivy-minibuffer-map
+ "C-o" #'ivy-dispatching-done-hydra
+ "M-o" #'hydra-ivy/body)
+ :config
+ ;; ivy-hydra rebinds this, so we have to do so again
+ (define-key ivy-minibuffer-map (kbd "M-o") #'hydra-ivy/body)))
-(def-package! swiper :commands (swiper swiper-all))
+(def-package! ivy-rich
+ :hook (ivy-mode . ivy-rich-mode)
+ :config
+ (when +ivy-buffer-icons
+ (cl-pushnew '(+ivy-rich-buffer-icon (:width 2 :align right))
+ (cadr (plist-get ivy-rich-display-transformers-list
+ 'ivy-switch-buffer)))
+ (after! counsel-projectile
+ (setq ivy-rich-display-transformers-list
+ (plist-put ivy-rich-display-transformers-list
+ 'counsel-projectile-switch-project
+ '(:columns
+ (((lambda (_) (all-the-icons-octicon "file-directory"))
+ (:width 2 :align right))
+ (ivy-rich-candidate)))))
+ (setq ivy-rich-display-transformers-list
+ (plist-put ivy-rich-display-transformers-list
+ 'counsel-projectile-find-file
+ '(:columns
+ ((all-the-icons-icon-for-file (:width 2 :align right))
+ (ivy-rich-candidate)))))))
+
+ ;; Remove built-in coloring of buffer list; we do our own
+ (setq ivy-switch-buffer-faces-alist nil)
+ (ivy-set-display-transformer 'internal-complete-buffer nil)
+
+ ;; Highlight buffers differently based on whether they're in the same project
+ ;; as the current project or not.
+ (let* ((plist (plist-get ivy-rich-display-transformers-list 'ivy-switch-buffer))
+ (switch-buffer-alist (assq 'ivy-rich-candidate (plist-get plist :columns))))
+ (when switch-buffer-alist
+ (setcar switch-buffer-alist '+ivy-rich-buffer-name)))
+
+ ;; Allow these transformers to apply to more switch-buffer commands
+ (let ((ivy-switch-buffer-transformer (plist-get ivy-rich-display-transformers-list 'ivy-switch-buffer)))
+ (dolist (cmd '(+ivy--switch-buffer counsel-projectile-switch-to-buffer))
+ (setq ivy-rich-display-transformers-list
+ (plist-put ivy-rich-display-transformers-list
+ cmd ivy-switch-buffer-transformer)))))
(def-package! counsel
- :requires ivy
+ :commands counsel-describe-face
+ :init
+ (map! [remap apropos] #'counsel-apropos
+ [remap bookmark-jump] #'counsel-bookmark
+ [remap describe-face] #'counsel-faces
+ [remap describe-function] #'counsel-describe-function
+ [remap describe-variable] #'counsel-describe-variable
+ [remap describe-bindings] #'counsel-descbinds
+ [remap set-variable] #'counsel-set-variable
+ [remap execute-extended-command] #'counsel-M-x
+ [remap find-file] #'counsel-find-file
+ [remap find-library] #'counsel-find-library
+ [remap info-lookup-symbol] #'counsel-info-lookup-symbol
+ [remap imenu] #'counsel-imenu
+ [remap recentf-open-files] #'counsel-recentf
+ [remap org-capture] #'counsel-org-capture
+ [remap swiper] #'counsel-grep-or-swiper
+ [remap evil-ex-registers] #'counsel-evil-registers
+ [remap yank-pop] #'counsel-yank-pop)
:config
- (require 'counsel-projectile)
- (setq counsel-find-file-ignore-regexp "\\(?:^[#.]\\)\\|\\(?:[#~]$\\)\\|\\(?:^Icon?\\)")
+ (set-popup-rule! "^\\*ivy-occur" :size 0.35 :ttl 0 :quit nil)
- ;; Configure `counsel-rg', `counsel-ag' & `counsel-pt'
- (set! :popup 'ivy-occur-grep-mode :size (+ 2 ivy-height) :regexp t :autokill t)
- (dolist (cmd '(counsel-ag counsel-rg counsel-pt))
- (ivy-add-actions
- cmd
- '(("O" +ivy-git-grep-other-window-action "open in other window"))))
+ (setq counsel-find-file-ignore-regexp "\\(?:^[#.]\\)\\|\\(?:[#~]$\\)\\|\\(?:^Icon?\\)"
+ counsel-describe-function-function #'helpful-callable
+ counsel-describe-variable-function #'helpful-variable
+ ;; Add smart-casing (-S) to default command arguments:
+ counsel-rg-base-command "rg -S --no-heading --line-number --color never %s ."
+ counsel-ag-base-command "ag -S --nocolor --nogroup %s"
+ counsel-pt-base-command "pt -S --nocolor --nogroup -e %s")
- ;; 1. Remove character limit from `counsel-ag-function'
- ;; 2. This may need to be updated frequently, to meet changes upstream
- ;; 3. counsel-ag, counsel-rg and counsel-pt all use this function
- (advice-add #'counsel-ag-function :override #'+ivy*counsel-ag-function))
+ (add-to-list 'swiper-font-lock-exclude #'+doom-dashboard-mode nil #'eq)
+
+ ;; Factories
+ (defun +ivy-action-reloading (cmd)
+ (lambda (x)
+ (funcall cmd x)
+ (ivy--reset-state ivy-last)))
+
+ (defun +ivy-action-given-file (cmd prompt)
+ (lambda (source)
+ (let* ((enable-recursive-minibuffers t)
+ (target (read-file-name (format "%s %s to:" prompt source))))
+ (funcall cmd source target 1))))
+
+ ;; Configure `counsel-find-file'
+ (ivy-add-actions
+ 'counsel-find-file
+ `(("b" counsel-find-file-cd-bookmark-action "cd bookmark")
+ ("s" counsel-find-file-as-root "open as root")
+ ("m" counsel-find-file-mkdir-action "mkdir")
+ ("c" ,(+ivy-action-given-file #'copy-file "Copy file") "copy file")
+ ("d" ,(+ivy-action-reloading #'+ivy-confirm-delete-file) "delete")
+ ("r" (lambda (path) (rename-file path (read-string "New name: "))) "rename")
+ ("R" ,(+ivy-action-reloading (+ivy-action-given-file #'rename-file "Move")) "move")
+ ("f" find-file-other-window "other window")
+ ("F" find-file-other-frame "other frame")
+ ("p" (lambda (path) (with-ivy-window (insert (file-relative-name path default-directory)))) "insert relative path")
+ ("P" (lambda (path) (with-ivy-window (insert path))) "insert absolute path")
+ ("l" (lambda (path) "Insert org-link with relative path"
+ (with-ivy-window (insert (format "[[./%s]]" (file-relative-name path default-directory))))) "insert org-link (rel. path)")
+ ("L" (lambda (path) "Insert org-link with absolute path"
+ (with-ivy-window (insert (format "[[%s]]" path)))) "insert org-link (abs. path)")))
+
+ (ivy-add-actions
+ 'counsel-ag ; also applies to `counsel-rg' & `counsel-pt'
+ '(("O" +ivy-git-grep-other-window-action "open in other window"))))
+
+
+(def-package! counsel-projectile
+ :commands (counsel-projectile-find-file counsel-projectile-find-dir counsel-projectile-switch-to-buffer
+ counsel-projectile-grep counsel-projectile-ag counsel-projectile-switch-project)
+ :init
+ (map! [remap projectile-find-file] #'+ivy/projectile-find-file
+ [remap projectile-find-dir] #'counsel-projectile-find-dir
+ [remap projectile-switch-to-buffer] #'counsel-projectile-switch-to-buffer
+ [remap projectile-grep] #'counsel-projectile-grep
+ [remap projectile-ag] #'counsel-projectile-ag
+ [remap projectile-switch-project] #'counsel-projectile-switch-project)
+ :config
+ ;; no highlighting visited files; slows down the filtering
+ (ivy-set-display-transformer #'counsel-projectile-find-file nil))
+
+
+(def-package! wgrep
+ :commands wgrep-change-to-wgrep-mode
+ :config (setq wgrep-auto-save-buffer t))
+
+
+(def-package! ivy-posframe
+ :when (and EMACS26+ (featurep! +childframe))
+ :hook (ivy-mode . ivy-posframe-enable)
+ :preface
+ ;; This function searches the entire `obarray' just to populate
+ ;; `ivy-display-functions-props'. There are 15k entries in mine! This is
+ ;; wasteful, so...
+ (advice-add #'ivy-posframe-setup :override #'ignore)
+ :config
+ (setq ivy-fixed-height-minibuffer nil
+ ivy-posframe-parameters
+ `((min-width . 90)
+ (min-height . ,ivy-height)
+ (internal-border-width . 10)))
+
+ ;; ... let's do it manually instead
+ (unless (assq 'ivy-posframe-display-at-frame-bottom-left ivy-display-functions-props)
+ (dolist (fn (list 'ivy-posframe-display-at-frame-bottom-left
+ 'ivy-posframe-display-at-frame-center
+ 'ivy-posframe-display-at-point
+ 'ivy-posframe-display-at-frame-bottom-window-center
+ 'ivy-posframe-display
+ 'ivy-posframe-display-at-window-bottom-left
+ 'ivy-posframe-display-at-window-center
+ '+ivy-display-at-frame-center-near-bottom))
+ (push (cons fn '(:cleanup ivy-posframe-cleanup)) ivy-display-functions-props)))
+ ;; default to posframe display function
+ (setf (alist-get t ivy-display-functions-alist) #'+ivy-display-at-frame-center-near-bottom)
+
+ ;; Fix #1017: stop session persistence from restoring a broken posframe
+ (defun +workspace|delete-all-posframes (&rest _) (posframe-delete-all))
+ (add-hook 'persp-after-load-state-functions #'+workspace|delete-all-posframes)
+
+ ;; posframe doesn't work well with async sources
+ (dolist (fn '(swiper counsel-ag counsel-grep counsel-git-grep))
+ (setf (alist-get fn ivy-display-functions-alist) #'ivy-display-function-fallback)))
+
+
+(def-package! flx
+ :when (featurep! +fuzzy)
+ :defer t ; is loaded by ivy
+ :init
+ (setq ivy-re-builders-alist
+ '((counsel-ag . ivy--regex-plus)
+ (counsel-rg . ivy--regex-plus)
+ (counsel-grep . ivy--regex-plus)
+ (swiper . ivy--regex-plus)
+ (swiper-isearch . ivy--regex-plus)
+ (t . ivy--regex-fuzzy))
+ ivy-initial-inputs-alist nil))
;; Used by `counsel-M-x'
-(def-package! smex
- :commands (smex smex-major-mode-commands)
- :config
- (setq smex-save-file (concat doom-cache-dir "/smex-items"))
- (smex-initialize))
+(setq amx-save-file (concat doom-cache-dir "amx-items"))
-(def-package! ivy-hydra
- :commands (+ivy@coo/body ivy-dispatching-done-hydra)
- :init
- (map! :map ivy-minibuffer-map
- "C-o" #'+ivy@coo/body
- "M-o" #'ivy-dispatching-done-hydra)
- :config
- (def-hydra! +ivy@coo (:hint nil :color pink)
- "
- Move ^^^^^^^^^^ | Call ^^^^ | Cancel^^ | Options^^ | Action _w_/_s_/_a_: %s(ivy-action-name)
-----------^^^^^^^^^^-+--------------^^^^-+-------^^-+--------^^-+---------------------------------
- _g_ ^ ^ _k_ ^ ^ _u_ | _f_orward _o_ccur | _i_nsert | _c_alling: %-7s(if ivy-calling \"on\" \"off\") _C_ase-fold: %-10`ivy-case-fold-search
- ^↨^ _h_ ^+^ _l_ ^↕^ | _RET_ done ^^ | _q_uit | _m_atcher: %-7s(ivy--matcher-desc) _t_runcate: %-11`truncate-lines
- _G_ ^ ^ _j_ ^ ^ _d_ | _TAB_ alt-done ^^ | ^ ^ | _<_/_>_: shrink/grow
-"
- ;; arrows
- ("j" ivy-next-line)
- ("k" ivy-previous-line)
- ("l" ivy-alt-done)
- ("h" ivy-backward-delete-char)
- ("g" ivy-beginning-of-buffer)
- ("G" ivy-end-of-buffer)
- ("d" ivy-scroll-up-command)
- ("u" ivy-scroll-down-command)
- ("e" ivy-scroll-down-command)
- ;; actions
- ("q" keyboard-escape-quit :exit t)
- ("C-g" keyboard-escape-quit :exit t)
- ("" keyboard-escape-quit :exit t)
- ("C-o" nil)
- ("i" nil)
- ("TAB" ivy-alt-done :exit nil)
- ("C-j" ivy-alt-done :exit nil)
- ("RET" ivy-done :exit t)
- ("C-m" ivy-done :exit t)
- ("C-SPC" ivy-call-and-recenter :exit nil)
- ("f" ivy-call)
- ("c" ivy-toggle-calling)
- ("m" ivy-toggle-fuzzy)
- (">" ivy-minibuffer-grow)
- ("<" ivy-minibuffer-shrink)
- ("w" ivy-prev-action)
- ("s" ivy-next-action)
- ("a" ivy-read-action)
- ("t" (setq truncate-lines (not truncate-lines)))
- ("C" ivy-toggle-case-fold)
- ("o" ivy-occur :exit t)))
+;;
+;; Evil key fixes
+
+(map! :when (featurep! :feature evil +everywhere)
+ :after ivy
+ :map (ivy-occur-mode-map ivy-occur-grep-mode-map)
+ :m "j" #'ivy-occur-next-line
+ :m "k" #'ivy-occur-previous-line
+ :m "h" #'evil-backward-char
+ :m "l" #'evil-forward-char
+ :m "g" nil
+ :m "gg" #'evil-goto-first-line
+ :map ivy-occur-mode-map
+ :n [mouse-1] #'ivy-occur-click
+ :n [return] #'ivy-occur-press-and-switch
+ :n "gf" #'ivy-occur-press
+ :n "ga" #'ivy-occur-read-action
+ :n "go" #'ivy-occur-dispatch
+ :n "gc" #'ivy-occur-toggle-calling
+ :n "gr" #'ivy-occur-revert-buffer
+ :n "q" #'quit-window
+ :map ivy-occur-grep-mode-map
+ :v "j" #'evil-next-line
+ :v "k" #'evil-previous-line
+ :n "D" #'ivy-occur-delete-candidate
+ :n "C-d" #'evil-scroll-down
+ :n "d" #'ivy-occur-delete-candidate
+ :n "C-x C-q" #'ivy-wgrep-change-to-wgrep-mode
+ :n "i" #'ivy-wgrep-change-to-wgrep-mode
+ :n "gd" #'ivy-occur-delete-candidate
+ :n [mouse-1] #'ivy-occur-click
+ :n [return] #'ivy-occur-press-and-switch
+ :n "gf" #'ivy-occur-press
+ :n "gr" #'ivy-occur-revert-buffer
+ :n "ga" #'ivy-occur-read-action
+ :n "go" #'ivy-occur-dispatch
+ :n "gc" #'ivy-occur-toggle-calling
+ :n "q" #'quit-window)
diff --git a/modules/completion/ivy/doctor.el b/modules/completion/ivy/doctor.el
new file mode 100644
index 000000000..d730f1552
--- /dev/null
+++ b/modules/completion/ivy/doctor.el
@@ -0,0 +1,5 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; completion/ivy/doctor.el
+
+(when (and (not EMACS26+) (featurep! +childframe))
+ (error! "The +childframe feature requires Emacs 26+"))
diff --git a/modules/completion/ivy/packages.el b/modules/completion/ivy/packages.el
index 8ec066214..ace65cf1b 100644
--- a/modules/completion/ivy/packages.el
+++ b/modules/completion/ivy/packages.el
@@ -1,9 +1,17 @@
;; -*- no-byte-compile: t; -*-
;;; completion/ivy/packages.el
+(package! amx)
(package! ivy)
(package! counsel)
(package! counsel-projectile)
-(package! smex)
(package! swiper)
(package! ivy-hydra)
+(package! ivy-rich)
+(package! wgrep)
+
+(when (featurep! +fuzzy)
+ (package! flx))
+
+(when (and EMACS26+ (featurep! +childframe))
+ (package! ivy-posframe))
diff --git a/modules/config/default/+emacs-bindings.el b/modules/config/default/+emacs-bindings.el
new file mode 100644
index 000000000..201a55890
--- /dev/null
+++ b/modules/config/default/+emacs-bindings.el
@@ -0,0 +1,353 @@
+;;; config/default/+emacs-bindings.el -*- lexical-binding: t; -*-
+
+;; Sensible deafult key bindings for non-evil users
+(setq doom-leader-alt-key "C-c"
+ doom-localleader-alt-key "C-c l")
+
+;; persp-mode and projectile in different prefixes
+(setq persp-keymap-prefix (kbd "C-c w"))
+(after! projectile
+ (define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map))
+
+(after! which-key
+ (which-key-add-key-based-replacements "C-c !" "checking")
+ (which-key-add-key-based-replacements "C-c l" ""))
+
+
+;;
+;;; Global keybinds
+
+(map! "C-'" #'imenu
+ ;; Text scaling
+ "" #'text-scale-increase
+ "" #'text-scale-decrease
+ "" (λ! (text-scale-set 0))
+ "M-+" (λ! (text-scale-set 0))
+ "M-=" #'text-scale-increase
+ "M--" #'text-scale-decrease
+ ;; Editor related bindings
+ [remap newline] #'newline-and-indent
+ "C-j" #'+default/newline
+ (:when (featurep! :completion ivy)
+ "C-S-s" #'swiper
+ "C-S-r" #'ivy-resume)
+ (:when (featurep! :completion helm)
+ "C-S-s" #'swiper-helm
+ "C-S-r" #'helm-resume)
+ ;; Buffer related bindings
+ "C-x b" #'persp-switch-to-buffer
+ (:when (featurep! :completion ivy)
+ "C-x 4 b" #'+ivy/switch-workspace-buffer-other-window)
+ "C-x C-b" #'ibuffer-list-buffers
+ "C-x B" #'switch-to-buffer
+ "C-x 4 B" #'switch-to-buffer-other-window
+ "C-x k" #'doom/kill-this-buffer-in-all-windows
+ ;; Popup bindigns
+ "C-x p" #'+popup/other
+ "C-`" #'+popup/toggle
+ "C-~" #'+popup/raise)
+
+
+;;
+;;; Leader keys
+
+(map! :leader
+ :desc "Find file in project" "C-f" #'projectile-find-file
+ :desc "Evaluate line/region" "e" #'+eval/line-or-region
+ :desc "Open scratch buffer" "x" #'doom/open-scratch-buffer
+ :desc "Open project scratch buffer" "X" #'doom/open-project-scratch-buffer
+
+ (:when (featurep! :emacs term)
+ :desc "Terminal" "`" #'+term/open
+ :desc "Terminal in popup" "~" #'+term/open-popup-in-project)
+ (:when (featurep! :tools vterm)
+ :desc "Terminal" "`" #'+vterm/open
+ :desc "Terminal in popup" "~" #'+vterm/open-popup-in-project)
+ (:when (featurep! :emacs eshell)
+ :desc "Eshell" "`" #'+eshell/open
+ :desc "Eshell in popup" "~" #'+eshell/open-popup)
+
+ ;; Add labels to prefixes defined elsewhere
+ :desc "project" "p" nil
+
+ (:prefix ("f" . "file")
+ :desc "Find other file" "a" #'projectile-find-other-file
+ :desc "Browse private config" "c" #'doom/open-private-config
+ :desc "Find file in private config" "C" #'doom/find-file-in-private-config
+ :desc "Open project editorconfig" "." #'editorconfig-find-current-editorconfig
+ :desc "Find directory" "d" #'dired
+ :desc "Find file in emacs.d" "e" #'+default/find-in-emacsd
+ :desc "Browse emacs.d" "E" #'+default/browse-emacsd
+ :desc "Find file from here" "f" (if (fboundp 'counsel-file-jump) #'counsel-file-jump #'find-file)
+ :desc "Find file in other project" "F" #'doom/browse-in-other-project
+ :desc "Find file in project" "p" #'projectile-find-file
+ :desc "Find file in other project" "P" #'doom/find-file-in-other-project
+ :desc "Recent files" "r" #'recentf-open-files
+ :desc "Recent project files" "R" #'projectile-recentf
+ :desc "Sudo this file" "s" #'doom/sudo-this-file
+ :desc "Sudo find file" "S" #'doom/sudo-find-file
+ :desc "Delete this file" "X" #'doom/delete-this-file
+ :desc "Yank filename" "y" #'+default/yank-buffer-filename)
+
+ "o" nil ; we need to unbind it first as Org claims this
+ (:prefix ("o". "org")
+ (:prefix ("a" . "org agenda")
+ :desc "Agenda" "a" #'org-agenda
+ :desc "Todo list" "t" #'org-todo-list
+ :desc "Tags view" "m" #'org-tags-view
+ :desc "View search" "v" #'org-search-view)
+ :desc "Switch org buffers" "b" #'org-switchb
+ :desc "Capture" "c" #'org-capture
+ :desc "Goto capture" "C" (λ! (require 'org-capture) (call-interactively #'org-capture-goto-target))
+ :desc "Link store" "l" #'org-store-link
+ :desc "Sync org caldav" "s" #'org-caldav-sync)
+
+ (:prefix ("q" . "quit/restart")
+ :desc "Quit Emacs" "q" #'kill-emacs
+ :desc "Save and quit Emacs" "Q" #'save-buffers-kill-terminal
+ (:when (featurep! :feature workspaces)
+ :desc "Quit Emacs & forget session" "X" #'+workspace/kill-session-and-quit)
+ :desc "Restart & restore Emacs" "r" #'doom/restart-and-restore
+ :desc "Restart Emacs" "R" #'doom/restart)
+
+ (:prefix ("&" . "snippets")
+ :desc "New snippet" "n" #'yas-new-snippet
+ :desc "Insert snippet" "i" #'yas-insert-snippet
+ :desc "Find global snippet" "/" #'yas-visit-snippet-file
+ :desc "Reload snippets" "r" #'yas-reload-all
+ :desc "Create Temp Template" "c" #'aya-create
+ :desc "Use Temp Template" "e" #'aya-expand)
+
+ (:prefix ("v" . "versioning")
+ :desc "Git revert file" "R" #'vc-revert
+ (:when (featurep! :ui vc-gutter)
+ :desc "Git revert hunk" "r" #'git-gutter:revert-hunk
+ :desc "Git stage hunk" "s" #'git-gutter:stage-hunk
+ :desc "Git time machine" "t" #'git-timemachine-toggle
+ :desc "Jump to next hunk" "n" #'git-gutter:next-hunk
+ :desc "Jump to previous hunk" "p" #'git-gutter:previous-hunk)
+ (:when (featurep! :tools magit)
+ :desc "Magit dispatch" "/" #'magit-dispatch
+ :desc "Forge dispatch" "'" #'forge-dispatch
+ :desc "Magit status" "g" #'magit-status
+ :desc "Magit file delete" "x" #'magit-file-delete
+ :desc "Magit blame" "B" #'magit-blame-addition
+ :desc "Magit clone" "C" #'+magit/clone
+ :desc "Magit fetch" "F" #'magit-fetch
+ :desc "Magit buffer log" "L" #'magit-log
+ :desc "Git stage file" "S" #'magit-stage-file
+ :desc "Git unstage file" "U" #'magit-unstage-file
+ (:prefix ("f" . "find")
+ :desc "Find file" "f" #'magit-find-file
+ :desc "Find gitconfig file" "g" #'magit-find-git-config-file
+ :desc "Find commit" "c" #'magit-show-commit
+ :desc "Find issue" "i" #'forge-visit-issue
+ :desc "Find pull request" "p" #'forge-visit-pullreq)
+ (:prefix ("o" . "open in browser")
+ :desc "Browse region or line" "." #'+vc/git-browse-region-or-line
+ :desc "Browse remote" "r" #'forge-browse-remote
+ :desc "Browse commit" "c" #'forge-browse-commit
+ :desc "Browse an issue" "i" #'forge-browse-issue
+ :desc "Browse a pull request" "p" #'forge-browse-pullreq
+ :desc "Browse issues" "I" #'forge-browse-issues
+ :desc "Browse pull requests" "P" #'forge-browse-pullreqs)
+ (:prefix ("l" . "list")
+ (:when (featurep! :tools gist)
+ :desc "List gists" "g" #'+gist:list)
+ :desc "List repositories" "r" #'magit-list-repositories
+ :desc "List submodules" "s" #'magit-list-submodules
+ :desc "List issues" "i" #'forge-list-issues
+ :desc "List pull requests" "p" #'forge-list-pullreqs
+ :desc "List notifications" "n" #'forge-list-notifications)
+ (:prefix ("c" . "create")
+ :desc "Initialize repo" "r" #'magit-init
+ :desc "Clone repo" "R" #'+magit/clone
+ :desc "Commit" "c" #'magit-commit-create
+ :desc "Issue" "i" #'forge-create-issue
+ :desc "Pull request" "p" #'forge-create-pullreq)))
+
+ (:prefix ("w" . "workspaces/windows")
+ :desc "Autosave session" "a" #'doom/quicksave-session
+ :desc "Display workspaces" "d" #'+workspace/display
+ :desc "Rename workspace" "r" #'+workspace/rename
+ :desc "Create workspace" "c" #'+workspace/new
+ :desc "Delete workspace" "k" #'+workspace/delete
+ :desc "Save session" "s" #'doom/save-session
+ :desc "Save workspace" "S" #'+workspace/save
+ :desc "Load session" "l" #'doom/load-session
+ :desc "Load last autosaved session" "L" #'doom/quickload-session
+ :desc "Kill other buffers" "o" #'doom/kill-other-buffers
+ :desc "Undo window config" "u" #'winner-undo
+ :desc "Redo window config" "U" #'winner-redo
+ :desc "Switch to left workspace" "p" #'+workspace/switch-left
+ :desc "Switch to right workspace" "n" #'+workspace/switch-right
+ :desc "Switch to" "w" #'+workspace/switch-to
+ :desc "Switch to workspace 1" "1" (λ! (+workspace/switch-to 0))
+ :desc "Switch to workspace 2" "2" (λ! (+workspace/switch-to 1))
+ :desc "Switch to workspace 3" "3" (λ! (+workspace/switch-to 2))
+ :desc "Switch to workspace 4" "4" (λ! (+workspace/switch-to 3))
+ :desc "Switch to workspace 5" "5" (λ! (+workspace/switch-to 4))
+ :desc "Switch to workspace 6" "6" (λ! (+workspace/switch-to 5))
+ :desc "Switch to workspace 7" "7" (λ! (+workspace/switch-to 6))
+ :desc "Switch to workspace 8" "8" (λ! (+workspace/switch-to 7))
+ :desc "Switch to workspace 9" "9" (λ! (+workspace/switch-to 8))
+ :desc "Switch to last workspace" "0" #'+workspace/switch-to-last)
+
+ (:when (featurep! :editor multiple-cursors)
+ (:prefix ("m" . "multiple cursors")
+ :desc "Edit lines" "l" #'mc/edit-lines
+ :desc "Mark next" "n" #'mc/mark-next-like-this
+ :desc "Unmark next" "N" #'mc/unmark-next-like-this
+ :desc "Mark previous" "p" #'mc/mark-previous-like-this
+ :desc "Unmark previous" "P" #'mc/unmark-previous-like-this
+ :desc "Mark all" "t" #'mc/mark-all-like-this
+ :desc "Mark all DWIM" "m" #'mc/mark-all-like-this-dwim
+ :desc "Edit line endings" "e" #'mc/edit-ends-of-lines
+ :desc "Edit line starts" "a" #'mc/edit-beginnings-of-lines
+ :desc "Mark tag" "s" #'mc/mark-sgml-tag-pair
+ :desc "Mark in defun" "d" #'mc/mark-all-like-this-in-defun
+ :desc "Add cursor w/mouse" "" #'mc/add-cursor-on-click))
+
+ ;; APPs
+ (:when (featurep! :app email)
+ (:prefix ("M" . "email")
+ :desc "Open email app" "M" #'=email
+ :desc "Compose email" "c" #'+email/compose))
+
+ (:when (featurep! :app irc)
+ (:prefix ("I" . "irc")
+ :desc "Open irc app" "I" #'=irc
+ :desc "Next unread buffer" "a" #'tracking-next-buffer
+ :desc "Quit irc" "q" #'+irc/quit
+ :desc "Reconnect all" "r" #'circe-reconnect-all
+ :desc "Send message" "s" #'+irc/send-message
+ (:when (featurep! :completion ivy)
+ :desc "Jump to channel" "j" #'irc/ivy-jump-to-channel)))
+
+ (:when (featurep! :app twitter)
+ (:prefix ("T" . "twitter")
+ :desc "Open twitter app" "T" #'=twitter
+ :desc "Quit twitter" "q" #'+twitter/quit
+ :desc "Rerender twits" "r" #'+twitter/rerender-all
+ :desc "Ace link" "l" #'+twitter/ace-link)))
+
+
+;;
+;;; Plugins
+
+(map! "C-=" #'er/expand-region
+ "C--" #'er/contract-region
+ (:when (featurep! :ui neotree)
+ "" #'+neotree/open
+ "" #'+neotree/find-this-file)
+ (:when (featurep! :ui treemacs)
+ "" #'+treemacs/open
+ "" #'+treemacs/find-file)
+ ;; smartparens
+ (:after smartparens
+ :map smartparens-mode-map
+ "C-M-a" #'sp-beginning-of-sexp
+ "C-M-e" #'sp-end-of-sexp
+ "C-M-f" #'sp-forward-sexp
+ "C-M-b" #'sp-backward-sexp
+ "C-M-d" #'sp-splice-sexp
+ "C-M-k" #'sp-kill-sexp
+ "C-M-t" #'sp-transpose-sexp
+ "C-" #'sp-forward-slurp-sexp
+ "M-" #'sp-forward-barf-sexp
+ "C-" #'sp-backward-slurp-sexp
+ "M-" #'sp-backward-barf-sexp)
+ ;; company mode
+ "C-;" #'+company/complete
+ ;; Counsel
+ (:when (featurep! :completion ivy)
+ (:after counsel
+ :map counsel-ag-map
+ "C-c C-e" #'+ivy/wgrep-occur ; search/replace on results
+ [backtab] #'+ivy/wgrep-occur ; search/replace on results
+ "C-SPC" #'ivy-call-and-recenter ; preview
+ "M-RET" (+ivy-do-action! #'+ivy-git-grep-other-window-action))
+ "C-M-y" #'counsel-yank-pop)
+ ;; repl toggle
+ "C-c C-z" #'+eval/open-repl-other-window
+ ;; company mode
+ (:after company
+ :map company-active-map
+ "C-o" #'company-search-kill-others
+ "C-n" #'company-select-next
+ "C-p" #'company-select-previous
+ "C-h" #'company-quickhelp-manual-begin
+ "C-S-h" #'company-show-doc-buffer
+ "C-s" #'company-search-candidates
+ "M-s" #'company-filter-candidates
+ "" #'company-complete-common-or-cycle
+ [tab] #'company-complete-common-or-cycle
+ [backtab] #'company-select-previous
+ "C-RET" #'counsel-company
+ :map company-search-map
+ "C-n" #'company-search-repeat-forward
+ "C-p" #'company-search-repeat-backward
+ "C-s" (λ! (company-search-abort) (company-filter-candidates)))
+ ;; neotree bindings
+ (:after neotree
+ :map neotree-mode-map
+ "q" #'neotree-hide
+ "RET" #'neotree-enter
+ "SPC" #'neotree-quick-look
+ "v" #'neotree-enter-vertical-split
+ "s" #'neotree-enter-horizontal-split
+ "c" #'neotree-create-node
+ "D" #'neotree-delete-node
+ "g" #'neotree-refresh
+ "r" #'neotree-rename-node
+ "R" #'neotree-refresh
+ "h" #'+neotree/collapse-or-up
+ "l" #'+neotree/expand-or-open
+ "n" #'neotree-next-line
+ "p" #'neotree-previous-line
+ "N" #'neotree-select-next-sibling-node
+ "P" #'neotree-select-previous-sibling-node)
+ ;; help and info
+ (:after help-mode
+ :map help-mode-map
+ "o" #'ace-link-help
+ ">" #'help-go-forward
+ "<" #'help-go-back
+ "n" #'forward-button
+ "p" #'backward-button)
+ (:after helpful
+ :map helpful-mode-map
+ "o" #'ace-link-help)
+ (:after apropos
+ :map apropos-mode-map
+ "o" #'ace-link-help
+ "n" #'forward-button
+ "p" #'backward-button)
+ (:after info
+ :map Info-mode-map
+ "o" #'ace-link-info)
+ ;; yasnippet
+ (:after yasnippet
+ ;; keymap while editing an inserted snippet
+ :map yas-keymap
+ "C-e" #'+snippets/goto-end-of-field
+ "C-a" #'+snippets/goto-start-of-field
+ "" #'yas-prev-field
+ "" #'+snippets/delete-to-start-of-field
+ [backspace] #'+snippets/delete-backward-char
+ [delete] #'+snippets/delete-forward-char-or-field)
+ ;; flycheck
+ (:after flycheck
+ :map flycheck-error-list-mode-map
+ "C-n" #'flycheck-error-list-next-error
+ "C-p" #'flycheck-error-list-previous-error
+ "RET" #'flycheck-error-list-goto-error)
+ ;; ivy
+ (:after ivy
+ :map ivy-minibuffer-map
+ "TAB" #'ivy-alt-done
+ "C-g" #'keyboard-escape-quit)
+ ;; ein notebokks
+ (:after ein:notebook-multilang
+ :map ein:notebook-multilang-mode-map
+ "C-c h" #'+ein/hydra/body))
diff --git a/modules/config/default/+emacs.el b/modules/config/default/+emacs.el
new file mode 100644
index 000000000..c9188f3d5
--- /dev/null
+++ b/modules/config/default/+emacs.el
@@ -0,0 +1,32 @@
+;;; config/default/+emacs.el -*- lexical-binding: t; -*-
+
+(require 'projectile) ; we need its keybinds immediately
+
+
+;;
+;;; Reasonable defaults
+
+(setq shift-select-mode t)
+(delete-selection-mode +1)
+
+(def-package! expand-region
+ :commands (er/contract-region er/mark-symbol er/mark-word)
+ :config
+ (defun doom*quit-expand-region ()
+ "Properly abort an expand-region region."
+ (when (memq last-command '(er/expand-region er/contract-region))
+ (er/contract-region 0)))
+ (advice-add #'evil-escape :before #'doom*quit-expand-region)
+ (advice-add #'doom/escape :before #'doom*quit-expand-region))
+
+
+(def-package! winum
+ :after-call (doom-switch-window-hook)
+ :config (winum-mode +1))
+
+
+;;
+;;; Keybinds
+
+(when (featurep! +bindings)
+ (load! "+emacs-bindings"))
diff --git a/modules/config/default/+evil-bindings.el b/modules/config/default/+evil-bindings.el
new file mode 100644
index 000000000..6c9097944
--- /dev/null
+++ b/modules/config/default/+evil-bindings.el
@@ -0,0 +1,871 @@
+;;; config/default/+bindings.el -*- lexical-binding: t; -*-
+
+;; This file defines a Spacemacs-esque keybinding scheme
+
+;; Don't let evil-collection interfere with certain keys
+(setq evil-collection-key-blacklist
+ (list "C-j" "C-k" "gd" "gf" "K" "[" "]" "gz"
+ doom-leader-key doom-localleader-key
+ doom-leader-alt-key doom-localleader-alt-key))
+
+
+;;
+;;; Global keybindings
+
+(map! (:map override
+ ;; A little sandbox to run code in
+ "M-;" #'eval-expression
+ "A-;" #'eval-expression)
+
+ ;; Smart tab
+ :i [tab] (general-predicate-dispatch nil ; fall back to nearest keymap
+ (and (featurep! :feature snippets)
+ (bound-and-true-p yas-minor-mode)
+ (yas-maybe-expand-abbrev-key-filter 'yas-expand))
+ 'yas-expand
+ (and (featurep! :completion company +tng)
+ (+company-has-completion-p))
+ '+company/complete)
+ :n [tab] (general-predicate-dispatch nil
+ (and (featurep! :editor fold)
+ (save-excursion (end-of-line) (invisible-p (point))))
+ '+fold/toggle
+ (fboundp 'evilmi-jump-items)
+ 'evilmi-jump-items)
+ :v [tab] (general-predicate-dispatch nil
+ (and (bound-and-true-p yas-minor-mode)
+ (or (eq evil-visual-selection 'line)
+ (and (fboundp 'evilmi-jump-items)
+ (save-excursion
+ (/= (point)
+ (progn (evilmi-jump-items nil)
+ (point)))))))
+ 'yas-insert-snippet
+ (fboundp 'evilmi-jump-items)
+ 'evilmi-jump-items)
+
+ ;; Smarter newlines
+ :i [remap newline] #'newline-and-indent ; auto-indent on newline
+ :i "C-j" #'+default/newline ; default behavior
+
+ (:after vc-annotate
+ :map vc-annotate-mode-map
+ [remap quit-window] #'kill-this-buffer)
+
+ (:map (help-mode-map helpful-mode-map)
+ :n "o" 'ace-link-help)
+
+ ;; misc
+ :n "C-S-f" #'toggle-frame-fullscreen
+
+ ;; Global evil keybinds
+ :m "]a" #'evil-forward-arg
+ :m "[a" #'evil-backward-arg
+ :m "]o" #'outline-next-visible-heading
+ :m "[o" #'outline-previous-visible-heading
+ :n "]b" #'next-buffer
+ :n "[b" #'previous-buffer
+ :n "zx" #'kill-this-buffer
+ :n "ZX" #'bury-buffer
+ :n "gp" #'+evil/reselect-paste
+ :n "g=" #'widen
+ :v "g=" #'+evil:narrow-buffer
+ :nv "z=" #'flyspell-correct-word-generic
+ :nv "g@" #'+evil:apply-macro
+ :nv "gc" #'evil-commentary
+ :nv "gx" #'evil-exchange
+ :nv "C-a" #'evil-numbers/inc-at-pt
+ :nv "C-S-a" #'evil-numbers/dec-at-pt
+ :v "gp" #'+evil/paste-preserve-register
+ :v "@" #'+evil:apply-macro
+ ;; repeat in visual mode (FIXME buggy)
+ :v "." #'+evil:apply-macro
+ ;; don't leave visual mode after shifting
+ :v "<" #'+evil/visual-dedent ; vnoremap < " #'+evil/visual-indent ; vnoremap > >gv
+
+ ;; window management (prefix "C-w")
+ (:map evil-window-map
+ ;; Navigation
+ "C-h" #'evil-window-left
+ "C-j" #'evil-window-down
+ "C-k" #'evil-window-up
+ "C-l" #'evil-window-right
+ "C-w" #'other-window
+ ;; Swapping windows
+ "H" #'+evil/window-move-left
+ "J" #'+evil/window-move-down
+ "K" #'+evil/window-move-up
+ "L" #'+evil/window-move-right
+ "C-S-w" #'ace-swap-window
+ ;; Window undo/redo
+ (:prefix "m"
+ "m" #'doom/window-maximize-buffer
+ "v" #'doom/window-maximize-vertically
+ "s" #'doom/window-maximize-horizontally)
+ "u" #'winner-undo
+ "C-u" #'winner-undo
+ "C-r" #'winner-redo
+ "o" #'doom/window-enlargen
+ ;; Delete window
+ "c" #'+workspace/close-window-or-workspace
+ "C-C" #'ace-delete-window)
+
+ ;; Plugins
+ ;; evil-easymotion
+ :m "gs" #'+evil/easymotion ; lazy-load `evil-easymotion'
+ (:after evil-easymotion
+ :map evilem-map
+ "a" (evilem-create #'evil-forward-arg)
+ "A" (evilem-create #'evil-backward-arg)
+ "s" (evilem-create #'evil-snipe-repeat
+ :name 'evil-easymotion-snipe-forward
+ :pre-hook (save-excursion (call-interactively #'evil-snipe-s))
+ :bind ((evil-snipe-scope 'buffer)
+ (evil-snipe-enable-highlight)
+ (evil-snipe-enable-incremental-highlight)))
+ "S" (evilem-create #'evil-snipe-repeat
+ :name 'evil-easymotion-snipe-backward
+ :pre-hook (save-excursion (call-interactively #'evil-snipe-S))
+ :bind ((evil-snipe-scope 'buffer)
+ (evil-snipe-enable-highlight)
+ (evil-snipe-enable-incremental-highlight)))
+ "SPC" #'avy-goto-char-timer
+ "/" (evilem-create #'evil-ex-search-next
+ :pre-hook (save-excursion (call-interactively #'evil-ex-search-forward))
+ :bind ((evil-search-wrap)))
+ "?" (evilem-create #'evil-ex-search-previous
+ :pre-hook (save-excursion (call-interactively #'evil-ex-search-backward))
+ :bind ((evil-search-wrap))))
+
+ ;; text object plugins
+ :textobj "x" #'evil-inner-xml-attr #'evil-outer-xml-attr
+ :textobj "a" #'evil-inner-arg #'evil-outer-arg
+ :textobj "B" #'evil-textobj-anyblock-inner-block #'evil-textobj-anyblock-a-block
+ :textobj "i" #'evil-indent-plus-i-indent #'evil-indent-plus-a-indent
+ :textobj "k" #'evil-indent-plus-i-indent-up #'evil-indent-plus-a-indent-up
+ :textobj "j" #'evil-indent-plus-i-indent-up-down #'evil-indent-plus-a-indent-up-down
+
+ ;; evil-snipe
+ (:after evil-snipe
+ :map evil-snipe-parent-transient-map
+ "C-;" (λ! (require 'evil-easymotion)
+ (call-interactively
+ (evilem-create #'evil-snipe-repeat
+ :bind ((evil-snipe-scope 'whole-buffer)
+ (evil-snipe-enable-highlight)
+ (evil-snipe-enable-incremental-highlight))))))
+
+ ;; evil-surround
+ :v "S" #'evil-surround-region
+ :o "s" #'evil-surround-edit
+ :o "S" #'evil-Surround-edit)
+
+
+;;
+;;; Module keybinds
+
+;;; :feature
+(map! (:when (featurep! :feature debugger)
+ :after realgud
+ :map realgud:shortkey-mode-map
+ :n "j" #'evil-next-line
+ :n "k" #'evil-previous-line
+ :n "h" #'evil-backward-char
+ :n "l" #'evil-forward-char
+ :n "c" #'realgud:cmd-continue
+ :m "n" #'realgud:cmd-next
+ :m "b" #'realgud:cmd-break
+ :m "B" #'realgud:cmd-clear)
+
+ (:when (featurep! :feature eval)
+ :g "M-r" #'+eval/buffer
+ :nv "gr" #'+eval:region
+ :n "gR" #'+eval/buffer
+ :v "gR" #'+eval:replace-region)
+
+ (:when (featurep! :feature lookup)
+ :nv "K" #'+lookup/documentation
+ :nv "gd" #'+lookup/definition
+ :nv "gD" #'+lookup/references
+ :nv "gf" #'+lookup/file)
+
+ (:when (featurep! :feature snippets)
+ ;; auto-yasnippet
+ :i [C-tab] #'aya-expand
+ :nv [C-tab] #'aya-create
+ ;; yasnippet
+ (:after yasnippet
+ (:map yas-keymap
+ "C-e" #'+snippets/goto-end-of-field
+ "C-a" #'+snippets/goto-start-of-field
+ [M-right] #'+snippets/goto-end-of-field
+ [M-left] #'+snippets/goto-start-of-field
+ [M-backspace] #'+snippets/delete-to-start-of-field
+ [backspace] #'+snippets/delete-backward-char
+ [delete] #'+snippets/delete-forward-char-or-field)))
+
+ (:when (featurep! :tools flyspell)
+ ;; Keybinds that have no Emacs+evil analogues (i.e. don't exist):
+ ;; zq - mark word at point as good word
+ ;; zw - mark word at point as bad
+ ;; zu{q,w} - undo last marking
+ ;; Keybinds that evil define:
+ ;; z= - correct flyspell word at point
+ ;; ]s - jump to previous spelling error
+ ;; [s - jump to next spelling error
+ (:map flyspell-mouse-map
+ "RET" #'flyspell-correct-word-generic
+ [return] #'flyspell-correct-word-generic
+ [mouse-1] #'flyspell-correct-word-generic))
+
+ (:when (featurep! :tools flycheck)
+ :m "]e" #'next-error
+ :m "[e" #'previous-error
+ (:after flycheck
+ :map flycheck-error-list-mode-map
+ :n "C-n" #'flycheck-error-list-next-error
+ :n "C-p" #'flycheck-error-list-previous-error
+ :n "j" #'flycheck-error-list-next-error
+ :n "k" #'flycheck-error-list-previous-error
+ :n "RET" #'flycheck-error-list-goto-error
+ :n [return] #'flycheck-error-list-goto-error))
+
+ (:when (featurep! :feature workspaces)
+ :n "gt" #'+workspace/switch-right
+ :n "gT" #'+workspace/switch-left
+ :n "]w" #'+workspace/switch-right
+ :n "[w" #'+workspace/switch-left
+ :g "M-1" (λ! (+workspace/switch-to 0))
+ :g "M-2" (λ! (+workspace/switch-to 1))
+ :g "M-3" (λ! (+workspace/switch-to 2))
+ :g "M-4" (λ! (+workspace/switch-to 3))
+ :g "M-5" (λ! (+workspace/switch-to 4))
+ :g "M-6" (λ! (+workspace/switch-to 5))
+ :g "M-7" (λ! (+workspace/switch-to 6))
+ :g "M-8" (λ! (+workspace/switch-to 7))
+ :g "M-9" (λ! (+workspace/switch-to 8))
+ :g "M-0" #'+workspace/switch-to-last
+ :g "M-t" #'+workspace/new
+ :g "M-T" #'+workspace/display))
+
+;;; :completion
+(map! (:when (featurep! :completion company)
+ :i "C-@" #'+company/complete
+ :i "C-SPC" #'+company/complete
+ (:prefix "C-x"
+ :i "C-l" #'+company/whole-lines
+ :i "C-k" #'+company/dict-or-keywords
+ :i "C-f" #'company-files
+ :i "C-]" #'company-etags
+ :i "s" #'company-ispell
+ :i "C-s" #'company-yasnippet
+ :i "C-o" #'company-capf
+ :i "C-n" #'+company/dabbrev
+ :i "C-p" #'+company/dabbrev-code-previous)
+ (:after company
+ (:map company-active-map
+ "C-w" nil ; don't interfere with `evil-delete-backward-word'
+ "C-n" #'company-select-next
+ "C-p" #'company-select-previous
+ "C-j" #'company-select-next
+ "C-k" #'company-select-previous
+ "C-h" #'company-show-doc-buffer
+ "C-u" #'company-previous-page
+ "C-d" #'company-next-page
+ "C-s" #'company-filter-candidates
+ "C-S-s" (cond ((featurep! :completion helm) #'helm-company)
+ ((featurep! :completion ivy) #'counsel-company))
+ "C-SPC" #'company-complete-common
+ "TAB" #'company-complete-common-or-cycle
+ [tab] #'company-complete-common-or-cycle
+ [backtab] #'company-select-previous)
+ (:map company-search-map ; applies to `company-filter-map' too
+ "C-n" #'company-select-next-or-abort
+ "C-p" #'company-select-previous-or-abort
+ "C-j" #'company-select-next-or-abort
+ "C-k" #'company-select-previous-or-abort
+ "C-s" (λ! (company-search-abort) (company-filter-candidates))
+ "ESC" #'company-search-abort)
+ ;; TAB auto-completion in term buffers
+ (:map comint-mode-map
+ "TAB" #'company-complete
+ [tab] #'company-complete)))
+
+ (:when (featurep! :completion ivy)
+ (:map (help-mode-map helpful-mode-map)
+ :n "Q" #'ivy-resume)
+ (:after ivy
+ :map ivy-minibuffer-map
+ "C-SPC" #'ivy-call-and-recenter ; preview file
+ "C-l" #'ivy-alt-done
+ "C-v" #'yank)
+ (:after counsel
+ :map counsel-ag-map
+ "C-SPC" #'ivy-call-and-recenter ; preview
+ "C-l" #'ivy-done
+ "C-c C-e" #'+ivy/wgrep-occur ; search/replace on results
+ [backtab] #'+ivy/wgrep-occur ; search/replace on results
+ [C-return] (+ivy-do-action! #'+ivy-git-grep-other-window-action))
+ (:after swiper
+ :map swiper-map
+ [backtab] #'+ivy/wgrep-occur))
+
+ (:when (featurep! :completion helm)
+ (:after helm
+ (:map helm-map
+ [left] #'left-char
+ [right] #'right-char
+ "C-S-n" #'helm-next-source
+ "C-S-p" #'helm-previous-source
+ "C-j" #'helm-next-line
+ "C-k" #'helm-previous-line
+ "C-S-j" #'helm-next-source
+ "C-S-k" #'helm-previous-source
+ "C-f" #'helm-next-page
+ "C-S-f" #'helm-previous-page
+ "C-u" #'helm-delete-minibuffer-contents
+ "C-w" #'backward-kill-word
+ "C-r" #'evil-paste-from-register ; Evil registers in helm! Glorious!
+ "C-s" #'helm-minibuffer-history
+ "C-b" #'backward-word
+ ;; Swap TAB and C-z
+ "TAB" #'helm-execute-persistent-action
+ [tab] #'helm-execute-persistent-action
+ "C-z" #'helm-select-action)
+ (:after swiper-helm
+ :map swiper-helm-keymap [backtab] #'helm-ag-edit)
+ (:after helm-ag
+ :map helm-ag-map
+ "C--" #'+helm-do-ag-decrease-context
+ "C-=" #'+helm-do-ag-increase-context
+ [backtab] #'helm-ag-edit
+ [left] nil
+ [right] nil)
+ (:after helm-files
+ :map (helm-find-files-map helm-read-file-map)
+ [C-return] #'helm-ff-run-switch-other-window
+ "C-w" #'helm-find-files-up-one-level)
+ (:after helm-locate
+ :map helm-generic-files-map
+ [C-return] #'helm-ff-run-switch-other-window)
+ (:after helm-buffers
+ :map helm-buffer-map
+ [C-return] #'helm-buffer-switch-other-window)
+ (:after helm-occur
+ :map helm-occur-map
+ [C-return] #'helm-occur-run-goto-line-ow)
+ (:after helm-grep
+ :map helm-grep-map
+ [C-return] #'helm-grep-run-other-window-action))))
+
+;;; :ui
+(map! (:when (featurep! :ui hl-todo)
+ :m "]t" #'hl-todo-next
+ :m "[t" #'hl-todo-previous)
+
+ (:when (featurep! :ui neotree)
+ :after neotree
+ :map neotree-mode-map
+ :n "g" nil
+ :n "TAB" #'neotree-quick-look
+ :n "RET" #'neotree-enter
+ :n [tab] #'neotree-quick-look
+ :n [return] #'neotree-enter
+ :n "DEL" #'evil-window-prev
+ :n "c" #'neotree-create-node
+ :n "r" #'neotree-rename-node
+ :n "d" #'neotree-delete-node
+ :n "j" #'neotree-next-line
+ :n "k" #'neotree-previous-line
+ :n "n" #'neotree-next-line
+ :n "p" #'neotree-previous-line
+ :n "h" #'+neotree/collapse-or-up
+ :n "l" #'+neotree/expand-or-open
+ :n "J" #'neotree-select-next-sibling-node
+ :n "K" #'neotree-select-previous-sibling-node
+ :n "H" #'neotree-select-up-node
+ :n "L" #'neotree-select-down-node
+ :n "G" #'evil-goto-line
+ :n "gg" #'evil-goto-first-line
+ :n "v" #'neotree-enter-vertical-split
+ :n "s" #'neotree-enter-horizontal-split
+ :n "q" #'neotree-hide
+ :n "R" #'neotree-refresh)
+
+ (:when (featurep! :ui popup)
+ :n "C-`" #'+popup/toggle
+ :n "C-~" #'+popup/raise
+ :g "C-x p" #'+popup/other)
+
+ (:when (featurep! :ui vc-gutter)
+ :m "]d" #'git-gutter:next-hunk
+ :m "[d" #'git-gutter:previous-hunk))
+
+;;; :editor
+(map! (:when (featurep! :editor fold)
+ :nv "C-SPC" #'+fold/toggle)
+
+ (:when (featurep! :editor format)
+ :n "gQ" #'+format:region)
+
+ (:when (featurep! :editor multiple-cursors)
+ ;; evil-mc
+ (:prefix "gz"
+ :nv "d" #'evil-mc-make-and-goto-next-match
+ :nv "D" #'evil-mc-make-and-goto-prev-match
+ :nv "j" #'evil-mc-make-cursor-move-next-line
+ :nv "k" #'evil-mc-make-cursor-move-prev-line
+ :nv "m" #'evil-mc-make-all-cursors
+ :nv "n" #'evil-mc-make-and-goto-next-cursor
+ :nv "N" #'evil-mc-make-and-goto-last-cursor
+ :nv "p" #'evil-mc-make-and-goto-prev-cursor
+ :nv "P" #'evil-mc-make-and-goto-first-cursor
+ :nv "q" #'evil-mc-undo-all-cursors
+ :nv "t" #'+multiple-cursors/evil-mc-toggle-cursors
+ :nv "u" #'evil-mc-undo-last-added-cursor
+ :nv "z" #'+multiple-cursors/evil-mc-make-cursor-here)
+ (:after evil-mc
+ :map evil-mc-key-map
+ :nv "C-n" #'evil-mc-make-and-goto-next-cursor
+ :nv "C-N" #'evil-mc-make-and-goto-last-cursor
+ :nv "C-p" #'evil-mc-make-and-goto-prev-cursor
+ :nv "C-P" #'evil-mc-make-and-goto-first-cursor)
+ ;; evil-multiedit
+ :v "R" #'evil-multiedit-match-all
+ :n "M-d" #'evil-multiedit-match-symbol-and-next
+ :n "M-D" #'evil-multiedit-match-symbol-and-prev
+ :v "M-d" #'evil-multiedit-match-and-next
+ :v "M-D" #'evil-multiedit-match-and-prev
+ :nv "C-M-d" #'evil-multiedit-restore
+ (:after evil-multiedit
+ (:map evil-multiedit-state-map
+ "M-d" #'evil-multiedit-match-and-next
+ "M-D" #'evil-multiedit-match-and-prev
+ "RET" #'evil-multiedit-toggle-or-restrict-region
+ [return] #'evil-multiedit-toggle-or-restrict-region)
+ (:map (evil-multiedit-state-map evil-multiedit-insert-state-map)
+ "C-n" #'evil-multiedit-next
+ "C-p" #'evil-multiedit-prev)))
+
+ (:when (featurep! :editor rotate-text)
+ :n "!" #'rotate-text))
+
+;;; :emacs
+(map! (:when (featurep! :emacs vc)
+ :after git-timemachine
+ :map git-timemachine-mode-map
+ :n "C-p" #'git-timemachine-show-previous-revision
+ :n "C-n" #'git-timemachine-show-next-revision
+ :n "[[" #'git-timemachine-show-previous-revision
+ :n "]]" #'git-timemachine-show-next-revision
+ :n "q" #'git-timemachine-quit
+ :n "gb" #'git-timemachine-blame))
+
+;;; :tools
+(map! (:when (featurep! :tools magit)
+ (:after evil-magit
+ ;; fix conflicts with private bindings
+ :map (magit-status-mode-map magit-revision-mode-map)
+ "C-j" nil
+ "C-k" nil)
+ (:map transient-map
+ "q" #'transient-quit-one))
+
+ (:when (featurep! :tools gist)
+ :after gist
+ :map gist-list-menu-mode-map
+ :n "go" #'gist-browse-current-url
+ :n "gr" #'gist-list-reload
+ :n "c" #'gist-add-buffer
+ :n "d" #'gist-kill-current
+ :n "e" #'gist-edit-current-description
+ :n "f" #'gist-fork
+ :n "q" #'kill-this-buffer
+ :n "s" #'gist-star
+ :n "S" #'gist-unstar
+ :n "y" #'gist-print-current-url))
+
+;;; :lang
+(map! (:when (featurep! :lang markdown)
+ :after markdown-mode
+ :map markdown-mode-map
+ ;; fix conflicts with private bindings
+ [backspace] nil))
+
+
+;;
+;;;
+
+(map! :leader
+ :desc "Eval expression" ";" #'eval-expression
+ :desc "M-x" ":" #'execute-extended-command
+ :desc "Pop up scratch buffer" "x" #'doom/open-scratch-buffer
+ :desc "Org Capture" "X" #'org-capture
+
+ ;; C-u is used by evil
+ :desc "Universal argument" "u" #'universal-argument
+ :desc "window" "w" evil-window-map
+ :desc "help" "h" help-map
+
+ :desc "Toggle last popup" "~" #'+popup/toggle
+ :desc "Find file" "." #'find-file
+
+ :desc "Switch buffer" "," #'switch-to-buffer
+ (:when (featurep! :feature workspaces)
+ :desc "Switch workspace buffer" "," #'persp-switch-to-buffer
+ :desc "Switch buffer" "<" #'switch-to-buffer)
+
+ :desc "Resume last search" "'"
+ (cond ((featurep! :completion ivy) #'ivy-resume)
+ ((featurep! :completion helm) #'helm-resume))
+
+ :desc "Search for symbol in project" "*" #'+default/search-project-for-symbol-at-point
+
+ :desc "Find file in project" "SPC" #'projectile-find-file
+ :desc "Blink cursor line" "DEL" #'+nav-flash/blink-cursor
+ :desc "Jump to bookmark" "RET" #'bookmark-jump
+
+ ;; Prefixed key groups
+ (:prefix ("/" . "search")
+ :desc "Search buffer" "b" #'swiper
+ :desc "Search current directory" "d" #'+default/search-from-cwd
+ :desc "Jump to symbol" "i" #'imenu
+ :desc "Jump to symbol across buffers" "I" #'imenu-anywhere
+ :desc "Jump to link" "l" #'ace-link
+ :desc "Look up online" "o" #'+lookup/online-select
+ :desc "Search project" "p" #'+default/search-project)
+
+ (:when (featurep! :feature workspaces)
+ (:prefix ("TAB" . "workspace")
+ :desc "Display tab bar" "TAB" #'+workspace/display
+ :desc "Switch workspace" "." #'+workspace/switch-to
+ :desc "New workspace" "n" #'+workspace/new
+ :desc "Load workspace from file" "l" #'+workspace/load
+ :desc "Save workspace to file" "s" #'+workspace/save
+ :desc "Delete session" "x" #'+workspace/kill-session
+ :desc "Delete this workspace" "d" #'+workspace/delete
+ :desc "Rename workspace" "r" #'+workspace/rename
+ :desc "Restore last session" "R" #'+workspace/restore-last-session
+ :desc "Next workspace" "]" #'+workspace/switch-right
+ :desc "Previous workspace" "[" #'+workspace/switch-left
+ :desc "Switch to 1st workspace" "1" (λ! (+workspace/switch-to 0))
+ :desc "Switch to 2nd workspace" "2" (λ! (+workspace/switch-to 1))
+ :desc "Switch to 3rd workspace" "3" (λ! (+workspace/switch-to 2))
+ :desc "Switch to 4th workspace" "4" (λ! (+workspace/switch-to 3))
+ :desc "Switch to 5th workspace" "5" (λ! (+workspace/switch-to 4))
+ :desc "Switch to 6th workspace" "6" (λ! (+workspace/switch-to 5))
+ :desc "Switch to 7th workspace" "7" (λ! (+workspace/switch-to 6))
+ :desc "Switch to 8th workspace" "8" (λ! (+workspace/switch-to 7))
+ :desc "Switch to 9th workspace" "9" (λ! (+workspace/switch-to 8))
+ :desc "Switch to last workspace" "0" #'+workspace/switch-to-last))
+
+ (:prefix ("b" . "buffer")
+ :desc "Toggle narrowing" "-" #'doom/clone-and-narrow-buffer
+ :desc "Previous buffer" "[" #'previous-buffer
+ :desc "Next buffer" "]" #'next-buffer
+ (:when (featurep! :feature workspaces)
+ :desc "Switch workspace buffer" "b" #'persp-switch-to-buffer
+ :desc "Switch buffer" "B" #'switch-to-buffer)
+ (:unless (featurep! :feature workspaces)
+ :desc "Switch buffer" "b" #'switch-to-buffer)
+ :desc "Kill buffer" "k" #'kill-this-buffer
+ :desc "Next buffer" "n" #'next-buffer
+ :desc "New empty buffer" "N" #'evil-buffer-new
+ :desc "Kill other buffers" "o" #'doom/kill-other-buffers
+ :desc "Previous buffer" "p" #'previous-buffer
+ :desc "Save buffer" "s" #'save-buffer
+ :desc "Sudo edit this file" "S" #'doom/sudo-this-file
+ :desc "Pop scratch buffer" "x" #'doom/open-scratch-buffer
+ :desc "Bury buffer" "z" #'bury-buffer)
+
+ (:prefix ("c" . "code")
+ :desc "Compile" "c" #'compile
+ :desc "Jump to definition" "d" #'+lookup/definition
+ :desc "Jump to references" "D" #'+lookup/references
+ :desc "Evaluate buffer/region" "e" #'+eval/buffer-or-region
+ :desc "Evaluate & replace region" "E" #'+eval:replace-region
+ :desc "Format buffer/region" "f" #'+format/region-or-buffer
+ :desc "Open REPL" "r" #'+eval/open-repl-other-window
+ :desc "Delete trailing whitespace" "w" #'delete-trailing-whitespace
+ :desc "Delete trailing newlines" "W" #'doom/delete-trailing-newlines
+ :desc "List errors" "x" #'flycheck-list-errors)
+
+ (:prefix ("f" . "file")
+ :desc "Find file" "." #'find-file
+ :desc "Find file from here" "/"
+ (if (featurep! :completion ivy)
+ #'counsel-file-jump
+ (λ! (doom-project-find-file default-directory)))
+ :desc "Open project editorconfig" "c" #'editorconfig-find-current-editorconfig
+ :desc "Find directory" "d" #'dired
+ :desc "Find file in emacs.d" "e" #'+default/find-in-emacsd
+ :desc "Browse emacs.d" "E" #'+default/browse-emacsd
+ :desc "Find file from here" "f" #'find-file
+ :desc "Find file in private config" "p" #'doom/find-file-in-private-config
+ :desc "Browse private config" "P" #'doom/open-private-config
+ :desc "Recent files" "r" #'recentf-open-files
+ :desc "Recent project files" "R" #'projectile-recentf
+ :desc "Save file" "s" #'save-buffer
+ :desc "Sudo find file" "S" #'doom/sudo-find-file
+ :desc "Delete this file" "X" #'doom/delete-this-file
+ :desc "Yank filename" "y" #'+default/yank-buffer-filename)
+
+ (:prefix ("g" . "git")
+ :desc "Git revert file" "R" #'vc-revert
+ (:when (featurep! :ui vc-gutter)
+ :desc "Git revert hunk" "r" #'git-gutter:revert-hunk
+ :desc "Git stage hunk" "s" #'git-gutter:stage-hunk
+ :desc "Git time machine" "t" #'git-timemachine-toggle
+ :desc "Jump to next hunk" "]" #'git-gutter:next-hunk
+ :desc "Jump to previous hunk" "[" #'git-gutter:previous-hunk)
+ (:when (featurep! :tools magit)
+ :desc "Magit dispatch" "/" #'magit-dispatch
+ :desc "Forge dispatch" "'" #'forge-dispatch
+ :desc "Magit status" "g" #'magit-status
+ :desc "Magit file delete" "x" #'magit-file-delete
+ :desc "Magit blame" "B" #'magit-blame-addition
+ :desc "Magit clone" "C" #'+magit/clone
+ :desc "Magit fetch" "F" #'magit-fetch
+ :desc "Magit buffer log" "L" #'magit-log
+ :desc "Git stage file" "S" #'magit-stage-file
+ :desc "Git unstage file" "U" #'magit-unstage-file
+ (:prefix ("f" . "find")
+ :desc "Find file" "f" #'magit-find-file
+ :desc "Find gitconfig file" "g" #'magit-find-git-config-file
+ :desc "Find commit" "c" #'magit-show-commit
+ :desc "Find issue" "i" #'forge-visit-issue
+ :desc "Find pull request" "p" #'forge-visit-pullreq)
+ (:prefix ("o" . "open in browser")
+ :desc "Browse region or line" "." #'+vc/git-browse-region-or-line
+ :desc "Browse remote" "r" #'forge-browse-remote
+ :desc "Browse commit" "c" #'forge-browse-commit
+ :desc "Browse an issue" "i" #'forge-browse-issue
+ :desc "Browse a pull request" "p" #'forge-browse-pullreq
+ :desc "Browse issues" "I" #'forge-browse-issues
+ :desc "Browse pull requests" "P" #'forge-browse-pullreqs)
+ (:prefix ("l" . "list")
+ (:when (featurep! :tools gist)
+ :desc "List gists" "g" #'+gist:list)
+ :desc "List repositories" "r" #'magit-list-repositories
+ :desc "List submodules" "s" #'magit-list-submodules
+ :desc "List issues" "i" #'forge-list-issues
+ :desc "List pull requests" "p" #'forge-list-pullreqs
+ :desc "List notifications" "n" #'forge-list-notifications)
+ (:prefix ("c" . "create")
+ :desc "Initialize repo" "r" #'magit-init
+ :desc "Clone repo" "R" #'+magit/clone
+ :desc "Commit" "c" #'magit-commit-create
+ :desc "Issue" "i" #'forge-create-issue
+ :desc "Pull request" "p" #'forge-create-pullreq)))
+
+ (:prefix ("i" . "insert")
+ :desc "Insert from clipboard" "y" #'+default/yank-pop
+ :desc "Insert from evil register" "r" #'evil-ex-registers
+ :desc "Insert snippet" "s" #'yas-insert-snippet)
+
+ (:prefix ("n" . "notes")
+ :desc "Open deft" "d" #'deft
+ :desc "Find file in notes" "n" #'+default/find-in-notes
+ :desc "Browse notes" "N" #'+default/browse-notes
+ :desc "Pop scratch buffer" "s" #'doom/open-scratch-buffer
+ :desc "Org capture" "x" #'org-capture
+ :desc "Org store link" "l" #'org-store-link)
+
+ (:prefix ("o" . "open")
+ :desc "Org agenda" "A" #'org-agenda
+ (:prefix ("a" . "org agenda")
+ :desc "Agenda" "a" #'org-agenda
+ :desc "Todo list" "t" #'org-todo-list
+ :desc "Tags search" "m" #'org-tags-view
+ :desc "View search" "v" #'org-search-view)
+ :desc "Default browser" "b" #'browse-url-of-file
+ :desc "Debugger" "d" #'+debug/open
+ :desc "REPL" "r" #'+eval/open-repl-other-window
+ :desc "REPL (same window)" "R" #'+eval/open-repl-same-window
+ :desc "Dired" "-" #'dired-jump
+ (:when (featurep! :ui neotree)
+ :desc "Project sidebar" "p" #'+neotree/open
+ :desc "Find file in project sidebar" "P" #'+neotree/find-this-file)
+ (:when (featurep! :ui treemacs)
+ :desc "Project sidebar" "p" #'+treemacs/toggle
+ :desc "Find file in project sidebar" "P" #'+treemacs/find-file)
+ (:when (featurep! :emacs imenu)
+ :desc "Imenu sidebar" "i" #'imenu-list-smart-toggle)
+ (:when (featurep! :emacs term)
+ :desc "Terminal" "t" #'+term/open
+ :desc "Terminal in popup" "T" #'+term/open-popup-in-project)
+ (:when (featurep! :tools vterm)
+ :desc "Terminal" "t" #'+vterm/open
+ :desc "Terminal in popup" "T" #'+vterm/open-popup-in-project)
+ (:when (featurep! :emacs eshell)
+ :desc "Eshell" "e" #'+eshell/open
+ :desc "Eshell in popup" "E" #'+eshell/open-popup)
+ (:when (featurep! :collab floobits)
+ (:prefix ("f" . "floobits")
+ "c" #'floobits-clear-highlights
+ "f" #'floobits-follow-user
+ "j" #'floobits-join-workspace
+ "l" #'floobits-leave-workspace
+ "R" #'floobits-share-dir-private
+ "s" #'floobits-summon
+ "t" #'floobits-follow-mode-toggle
+ "U" #'floobits-share-dir-public))
+ (:when (featurep! :tools macos)
+ :desc "Reveal in Finder" "o" #'+macos/reveal-in-finder
+ :desc "Reveal project in Finder" "O" #'+macos/reveal-project-in-finder
+ :desc "Send to Transmit" "u" #'+macos/send-to-transmit
+ :desc "Send project to Transmit" "U" #'+macos/send-project-to-transmit
+ :desc "Send to Launchbar" "l" #'+macos/send-to-launchbar
+ :desc "Send project to Launchbar" "L" #'+macos/send-project-to-launchbar)
+ (:when (featurep! :tools docker)
+ :desc "Docker" "D" #'docker))
+
+ (:prefix ("p" . "project")
+ :desc "Browse project" "." #'+default/browse-project
+ :desc "Find file in other project" ">" #'doom/find-file-in-other-project
+ :desc "Find file in project" "/" #'projectile-find-file
+ :desc "Browse other project" "?" #'doom/browse-in-other-project
+ :desc "Run cmd in project root" "!" #'projectile-run-shell-command-in-root
+ :desc "Add new project" "a" #'projectile-add-known-project
+ :desc "Switch to project buffer" "b" #'projectile-switch-to-buffer
+ :desc "Compile in project" "c" #'projectile-compile-project
+ :desc "Remove known project" "d" #'projectile-remove-known-project
+ :desc "Kill project buffers" "k" #'projectile-kill-buffers
+ :desc "Invalidate project cache" "i" #'projectile-invalidate-cache
+ :desc "Find other file" "o" #'projectile-find-other-file
+ :desc "Switch project" "p" #'projectile-switch-project
+ :desc "Find recent project files" "r" #'projectile-recentf
+ :desc "Scratch buffer" "s" #'doom/open-project-scratch-buffer
+ :desc "List project tasks" "t" #'+default/project-tasks
+ (:prefix ("x" . "terminal")
+ :desc "Open eshell in project" "e" #'projectile-run-eshell
+ :desc "Open ielm in project" "i" #'projectile-run-ielm
+ :desc "Open term in project" "t" #'projectile-run-term
+ :desc "Open shell in project" "s" #'projectile-run-shell))
+
+ (:prefix ("q" . "session")
+ :desc "Quit Emacs" "q" #'save-buffers-kill-terminal
+ :desc "Quit Emacs without saving" "Q" #'evil-quit-all-with-error-code
+ :desc "Quick save current session" "s" #'doom/quicksave-session
+ :desc "Restore last session" "l" #'doom/quickload-session
+ :desc "Save session to file" "S" #'doom/save-session
+ :desc "Restore session from file" "L" #'doom/load-session
+ :desc "Restart & restore Emacs" "r" #'doom/restart-and-restore
+ :desc "Restart Emacs" "R" #'doom/restart)
+
+ (:when (featurep! :tools upload)
+ (:prefix ("r" . "remote")
+ :desc "Upload local" "u" #'ssh-deploy-upload-handler
+ :desc "Upload local (force)" "U" #'ssh-deploy-upload-handler-forced
+ :desc "Download remote" "d" #'ssh-deploy-download-handler
+ :desc "Diff local & remote" "D" #'ssh-deploy-diff-handler
+ :desc "Browse remote files" "." #'ssh-deploy-browse-remote-handler
+ :desc "Detect remote changes" ">" #'ssh-deploy-remote-changes-handler))
+
+ (:when (featurep! :feature snippets)
+ (:prefix ("s" . "snippets")
+ :desc "New snippet" "n" #'yas-new-snippet
+ :desc "Insert snippet" "i" #'yas-insert-snippet
+ :desc "Jump to mode snippet" "/" #'yas-visit-snippet-file
+ :desc "Jump to snippet" "s" #'+snippets/find-file
+ :desc "Browse snippets" "S" #'+snippets/browse
+ :desc "Reload snippets" "r" #'yas-reload-all
+ :desc "Create temporary snippet" "c" #'aya-create
+ :desc "Use temporary snippet" "e" #'aya-expand))
+
+ (:prefix ("t" . "toggle")
+ :desc "Flyspell" "s" #'flyspell-mode
+ :desc "Flycheck" "f" #'flycheck-mode
+ :desc "Line numbers" "l" #'doom/toggle-line-numbers
+ :desc "Frame fullscreen" "F" #'toggle-frame-fullscreen
+ :desc "Indent guides" "i" #'highlight-indent-guides-mode
+ :desc "Impatient mode" "h" #'+impatient-mode/toggle
+ :desc "Big mode" "b" #'doom-big-font-mode
+ :desc "Evil goggles" "g" #'evil-goggles-mode
+ :desc "org-tree-slide mode" "p" #'+org-present/start))
+
+
+;;
+;;; Universal motion repeating keys
+
+(defvar +default-repeat-keys (cons ";" ",")
+ "The keys to use for repeating motions.
+
+This is a cons cell whose CAR is the key for repeating a motion forward, and
+whose CDR is for repeating backward. They should both be kbd-able strings.")
+
+(when +default-repeat-keys
+ (defmacro do-repeat! (command next-func prev-func)
+ "Makes ; and , the universal repeat-keys in evil-mode.
+To change these keys see `+default-repeat-keys'."
+ (let ((fn-sym (intern (format "+default*repeat-%s" (doom-unquote command)))))
+ `(progn
+ (defun ,fn-sym (&rest _)
+ (define-key! :states 'motion
+ (car +default-repeat-keys) #',next-func
+ (cdr +default-repeat-keys) #',prev-func))
+ (advice-add #',command :before #',fn-sym))))
+
+ ;; n/N
+ (do-repeat! evil-ex-search-next evil-ex-search-next evil-ex-search-previous)
+ (do-repeat! evil-ex-search-previous evil-ex-search-next evil-ex-search-previous)
+ (do-repeat! evil-ex-search-forward evil-ex-search-next evil-ex-search-previous)
+ (do-repeat! evil-ex-search-backward evil-ex-search-next evil-ex-search-previous)
+
+ ;; f/F/t/T/s/S
+ (setq evil-snipe-repeat-keys nil
+ evil-snipe-override-evil-repeat-keys nil) ; causes problems with remapped ;
+ (do-repeat! evil-snipe-f evil-snipe-repeat evil-snipe-repeat-reverse)
+ (do-repeat! evil-snipe-F evil-snipe-repeat evil-snipe-repeat-reverse)
+ (do-repeat! evil-snipe-t evil-snipe-repeat evil-snipe-repeat-reverse)
+ (do-repeat! evil-snipe-T evil-snipe-repeat evil-snipe-repeat-reverse)
+ (do-repeat! evil-snipe-s evil-snipe-repeat evil-snipe-repeat-reverse)
+ (do-repeat! evil-snipe-S evil-snipe-repeat evil-snipe-repeat-reverse)
+ (do-repeat! evil-snipe-x evil-snipe-repeat evil-snipe-repeat-reverse)
+ (do-repeat! evil-snipe-X evil-snipe-repeat evil-snipe-repeat-reverse)
+
+ ;; */#
+ (do-repeat! evil-visualstar/begin-search-forward
+ evil-ex-search-next evil-ex-search-previous)
+ (do-repeat! evil-visualstar/begin-search-backward
+ evil-ex-search-previous evil-ex-search-next))
+
+
+;;
+;;; Universal evil integration
+
+(when (featurep! :feature evil +everywhere)
+ ;; Have C-u behave similarly to `doom/backward-to-bol-or-indent'.
+ ;; NOTE SPC u replaces C-u as the universal argument.
+ (map! :gi "C-u" #'doom/backward-kill-to-bol-and-indent
+ :gi "C-w" #'backward-kill-word
+ ;; Vimmish ex motion keys
+ :gi "C-b" #'backward-word
+ :gi "C-f" #'forward-word)
+
+ (after! view
+ (define-key view-mode-map [escape] #'View-quit-all))
+ (after! man
+ (evil-define-key* 'normal Man-mode-map "q" #'kill-this-buffer))
+
+ ;; Minibuffer
+ (define-key! evil-ex-completion-map
+ "C-a" #'move-beginning-of-line
+ "C-b" #'backward-word
+ "C-s" (if (featurep! :completion ivy)
+ #'counsel-minibuffer-history
+ #'helm-minibuffer-history))
+
+ (define-key! :keymaps +default-minibuffer-maps
+ [escape] #'abort-recursive-edit
+ "C-v" #'yank
+ "C-z" (λ! (ignore-errors (call-interactively #'undo)))
+ "C-a" #'move-beginning-of-line
+ "C-b" #'backward-word
+ "C-r" #'evil-paste-from-register
+ ;; Scrolling lines
+ "C-j" #'next-line
+ "C-k" #'previous-line
+ "C-S-j" #'scroll-up-command
+ "C-S-k" #'scroll-down-command))
diff --git a/modules/config/default/+evil.el b/modules/config/default/+evil.el
new file mode 100644
index 000000000..e1c7eddb6
--- /dev/null
+++ b/modules/config/default/+evil.el
@@ -0,0 +1,21 @@
+;;; config/default/+evil.el -*- lexical-binding: t; -*-
+
+(defun +default|disable-delete-selection-mode ()
+ (delete-selection-mode -1))
+(add-hook 'evil-insert-state-entry-hook #'delete-selection-mode)
+(add-hook 'evil-insert-state-exit-hook #'+default|disable-delete-selection-mode)
+
+
+;;
+;;; Keybindings
+
+;; This section is dedicated to "fixing" certain keys so that they behave
+;; sensibly (and consistently with similar contexts).
+
+;; Make SPC u SPC u [...] possible (#747)
+(map! :map universal-argument-map
+ :prefix doom-leader-key "u" #'universal-argument-more
+ :prefix doom-leader-alt-key "u" #'universal-argument-more)
+
+(when (featurep! +bindings)
+ (load! "+evil-bindings"))
diff --git a/modules/config/default/README.org b/modules/config/default/README.org
new file mode 100644
index 000000000..aa282ffb0
--- /dev/null
+++ b/modules/config/default/README.org
@@ -0,0 +1,60 @@
+#+TITLE: :config default
+
+This module provides a set of reasonable defaults, including:
+
++ A Spacemacs-esque keybinding scheme
++ Extra Ex commands for evil-mode users
++ A yasnippet snippets library tailored to Doom emacs
++ A configuration for (almost) universally repeating searches with =;= and =,=
+
+#+begin_quote
+The defaults module is intended as a "reasonable-defaults" module, but also as a
+reference for your own private modules. You'll find [[https://github.com/hlissner/doom-emacs-private][my private module in a
+separate repo]].
+
+Refer to the [[https://github.com/hlissner/doom-emacs/wiki/Customization][Customization page]] on the wiki for details on starting your own
+private module.
+#+end_quote
+
+* Table of Contents :TOC:
+- [[#install][Install]]
+- [[#configuration][Configuration]]
+ - [[#using-another-snippets-library][Using another snippets library]]
+ - [[#im-not-an-evil-user][I'm not an evil user...]]
+- [[#appendix][Appendix]]
+ - [[#commands][Commands]]
+ - [[#hacks][Hacks]]
+
+* Install
+This module has no external dependencies.
+
+* Configuration
+** Using another snippets library
+Don't want to use provided one? Then add this to your private module,
+
+#+BEGIN_SRC emacs-lisp
+;; in config/$USER/packages.el
+(package! emacs-snippets :ignore t)
+
+;; in config/$USER/config.el
+(def-package-hook! emacs-snippets :disabled t)
+(after! yasnippet
+ (push "~/path/to/my/private/snippets" yas-snippet-dirs))
+#+END_SRC
+
+** I'm not an evil user...
+That's fine. All evil configuration is ignored if =:feature evil= is disabled.
+
+* Appendix
+** Commands
++ ~+default/browse-project~
++ ~+default/browse-templates~
++ ~+default/find-in-templates~
++ ~+default/browse-emacsd~
++ ~+default/find-in-emacsd~
++ ~+default/browse-notes~
++ ~+default/find-in-notes~
++ ~+default/find-in-snippets~
+** Hacks
++ ~epa-pinentry-mode~ is set to ~'loopback~, forcing gpg-agent to use the Emacs
+ minibuffer when prompting for your passphrase. *Only works with GPG 2.1+!*
diff --git a/modules/config/default/autoload/default.el b/modules/config/default/autoload/default.el
new file mode 100644
index 000000000..f98f7ff7d
--- /dev/null
+++ b/modules/config/default/autoload/default.el
@@ -0,0 +1,271 @@
+;; config/default/autoload/default.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +default/yank-buffer-filename ()
+ "Copy the current buffer's path to the kill ring."
+ (interactive)
+ (if-let* ((filename (or buffer-file-name (bound-and-true-p list-buffers-directory))))
+ (message (kill-new (abbreviate-file-name filename)))
+ (error "Couldn't find filename in current buffer")))
+
+;;;###autoload
+(defun +default/browse-project ()
+ (interactive) (doom-project-browse (doom-project-root)))
+;; NOTE No need for find-in-project, use `projectile-find-file'
+
+;;;###autoload
+(defun +default/browse-templates ()
+ (interactive) (doom-project-browse +file-templates-dir))
+;;;###autoload
+(defun +default/find-in-templates ()
+ (interactive) (doom-project-find-file +file-templates-dir))
+
+;;;###autoload
+(defun +default/browse-emacsd ()
+ (interactive) (doom-project-browse doom-emacs-dir))
+;;;###autoload
+(defun +default/find-in-emacsd ()
+ (interactive) (doom-project-find-file doom-emacs-dir))
+
+;;;###autoload
+(defun +default/browse-notes ()
+ (interactive) (doom-project-browse org-directory))
+;;;###autoload
+(defun +default/find-in-notes ()
+ (interactive) (doom-project-find-file org-directory))
+
+;;;###autoload
+(defun +default/compile (arg)
+ "Runs `compile' from the root of the current project.
+
+If a compilation window is already open, recompile that instead.
+
+If ARG (universal argument), runs `compile' from the current directory."
+ (interactive "P")
+ (if (and (bound-and-true-p compilation-in-progress)
+ (buffer-live-p compilation-last-buffer))
+ (recompile)
+ (call-interactively
+ (if arg
+ #'projectile-compile-project
+ #'compile))))
+
+;;;###autoload
+(defun +default/man-or-woman ()
+ "Invoke `man' if man is installed, otherwise use `woman'."
+ (interactive)
+ (call-interactively
+ (if (executable-find "man")
+ #'man
+ #'woman)))
+
+;;;###autoload
+(defalias '+default/newline #'newline)
+
+;;;###autoload
+(defun +default/new-buffer ()
+ "TODO"
+ (interactive)
+ (if (featurep! 'evil)
+ (call-interactively #'evil-buffer-new)
+ (let ((buffer (generate-new-buffer "*new*")))
+ (set-window-buffer nil buffer)
+ (with-current-buffer buffer
+ (funcall (default-value 'major-mode))))))
+
+;;;###autoload
+(defun +default/project-tasks ()
+ "Invokes `+ivy/tasks' or `+helm/tasks', depending on which is available."
+ (interactive)
+ (cond ((featurep! :completion ivy) (+ivy/tasks))
+ ((featurep! :completion helm) (+helm/tasks))))
+
+;;;###autoload
+(defun +default/newline-above ()
+ "Insert an indented new line before the current one."
+ (interactive)
+ (if (featurep 'evil)
+ (call-interactively 'evil-open-above)
+ (beginning-of-line)
+ (save-excursion (newline))
+ (indent-according-to-mode)))
+
+;;;###autoload
+(defun +default/newline-below ()
+ "Insert an indented new line after the current one."
+ (interactive)
+ (if (featurep 'evil)
+ (call-interactively 'evil-open-below)
+ (end-of-line)
+ (newline-and-indent)))
+
+;;;###autoload
+(defun +default/yank-pop ()
+ "Interactively select what text to insert from the kill ring."
+ (interactive)
+ (call-interactively
+ (cond ((fboundp 'counsel-yank-pop) #'counsel-yank-pop)
+ ((fboundp 'helm-show-kill-ring) #'helm-show-kill-ring)
+ ((error "No kill-ring search backend available. Enable ivy or helm!")))))
+
+;;;###autoload
+(defun +default*newline-indent-and-continue-comments (_orig-fn)
+ "Inserts a newline and possibly indents it. Also continues comments if
+executed from a commented line; handling special cases for certain languages
+with weak native support."
+ (interactive)
+ (cond ((sp-point-in-string) (newline))
+ ((and (sp-point-in-comment)
+ comment-line-break-function)
+ (funcall comment-line-break-function))
+ (t
+ (newline nil t)
+ (indent-according-to-mode))))
+
+(defun doom--backward-delete-whitespace-to-column ()
+ "Delete back to the previous column of whitespace, or as much whitespace as
+possible, or just one char if that's not possible."
+ (interactive)
+ (let* ((context (ignore-errors (sp-get-thing)))
+ (op (plist-get context :op))
+ (cl (plist-get context :cl))
+ open-len close-len)
+ (cond ;; When in strings (sp acts weird with quotes; this is the fix)
+ ;; Also, skip closing delimiters
+ ((and op cl
+ (string= op cl)
+ (and (string= (char-to-string (or (char-before) 0)) op)
+ (setq open-len (length op)))
+ (and (string= (char-to-string (or (char-after) 0)) cl)
+ (setq close-len (length cl))))
+ (delete-char (- open-len))
+ (delete-char close-len))
+
+ ;; Delete up to the nearest tab column IF only whitespace between
+ ;; point and bol.
+ ((and (not indent-tabs-mode)
+ (not (bolp))
+ (not (sp-point-in-string))
+ (save-excursion (>= (- (skip-chars-backward " \t")) tab-width)))
+ (let ((movement (% (current-column) tab-width)))
+ (when (= movement 0)
+ (setq movement tab-width))
+ (delete-char (- movement)))
+ (unless (memq (char-before) (list ?\n ?\ ))
+ (insert " ")))
+
+ ;; Otherwise do a regular delete
+ ((delete-char -1)))))
+
+;;;###autoload
+(defun +default*delete-backward-char (n &optional killflag)
+ "Same as `delete-backward-char', but preforms these additional checks:
+
++ If point is surrounded by (balanced) whitespace and a brace delimiter ({} []
+ ()), delete a space on either side of the cursor.
++ If point is at BOL and surrounded by braces on adjacent lines, collapse
+ newlines:
+ {
+ |
+ } => {|}
++ Otherwise, resort to `doom--backward-delete-whitespace-to-column'.
++ Resorts to `delete-char' if n > 1"
+ (interactive "p\nP")
+ (or (integerp n)
+ (signal 'wrong-type-argument (list 'integerp n)))
+ (cond ((and (use-region-p)
+ delete-active-region
+ (= n 1))
+ ;; If a region is active, kill or delete it.
+ (if (eq delete-active-region 'kill)
+ (kill-region (region-beginning) (region-end) 'region)
+ (funcall region-extract-function 'delete-only)))
+ ;; In Overwrite mode, maybe untabify while deleting
+ ((null (or (null overwrite-mode)
+ (<= n 0)
+ (memq (char-before) '(?\t ?\n))
+ (eobp)
+ (eq (char-after) ?\n)))
+ (let ((ocol (current-column)))
+ (delete-char (- n) killflag)
+ (save-excursion
+ (insert-char ?\s (- ocol (current-column)) nil))))
+ ;;
+ ((and (= n 1) (bound-and-true-p smartparens-mode))
+ (cond ((and (memq (char-before) (list ?\ ?\t))
+ (save-excursion
+ (and (/= (skip-chars-backward " \t" (line-beginning-position)) 0)
+ (bolp))))
+ (doom--backward-delete-whitespace-to-column))
+ ((let* ((pair (ignore-errors (sp-get-thing)))
+ (op (plist-get pair :op))
+ (cl (plist-get pair :cl))
+ (beg (plist-get pair :beg))
+ (end (plist-get pair :end)))
+ (cond ((and end beg (= end (+ beg (length op) (length cl))))
+ (sp-backward-delete-char 1))
+ ((doom-surrounded-p pair 'inline 'balanced)
+ (delete-char -1 killflag)
+ (delete-char 1)
+ (when (= (point) (+ (length cl) beg))
+ (sp-backward-delete-char 1)
+ (sp-insert-pair op)))
+ ((and (bolp) (doom-surrounded-p pair nil 'balanced))
+ (delete-region beg end)
+ (sp-insert-pair op)
+ t)
+ ((run-hook-with-args-until-success 'doom-delete-backward-functions))
+ ((doom--backward-delete-whitespace-to-column)))))))
+ ;; Otherwise, do simple deletion.
+ ((delete-char (- n) killflag))))
+
+;;;###autoload
+(defun +default/search-from-cwd (&optional arg)
+ "Conduct a text search in files under the current folder.
+If prefix ARG is set, prompt for a directory to search from."
+ (interactive "P")
+ (let ((default-directory
+ (if arg
+ (read-directory-name "Switch to project: " default-directory)
+ default-directory)))
+ (call-interactively
+ (cond ((featurep! :completion ivy) #'+ivy/project-search-from-cwd)
+ ((featurep! :completion helm) #'+helm/project-search-from-cwd)
+ (#'projectile-grep)))))
+
+;;;###autoload
+(defun +default/search-project (&optional arg)
+ "Conduct a text search in the current project root.
+If prefix ARG is set, prompt for a known project to search from."
+ (interactive "P")
+ (let ((default-directory
+ (if arg
+ (if-let* ((projects (projectile-relevant-known-projects)))
+ (completing-read "Switch to project: " projects
+ nil t nil nil (doom-project-root))
+ (user-error "There are no known projects"))
+ default-directory)))
+ (call-interactively
+ (cond ((featurep! :completion ivy) #'+ivy/project-search)
+ ((featurep! :completion helm) #'+helm/project-search)
+ (#'rgrep)))))
+
+;;;###autoload
+(defun +default/search-project-for-symbol-at-point (&optional arg symbol)
+ "Conduct a text search in the current project for symbol at point.
+If prefix ARG is set, prompt for a known project to search from."
+ (interactive
+ (list current-prefix-arg
+ (thing-at-point 'symbol t)))
+ (let ((default-directory
+ (if arg
+ (if-let* ((projects (projectile-relevant-known-projects)))
+ (completing-read "Switch to project: " projects
+ nil t nil nil (doom-project-root))
+ (user-error "There are no known projects"))
+ default-directory)))
+ (cond ((featurep! :completion ivy)
+ (+ivy/project-search nil (rxt-quote-pcre symbol)))
+ ((featurep! :completion helm)
+ (+helm/project-search nil (rxt-quote-pcre symbol)))
+ ((rgrep (regexp-quote symbol))))))
diff --git a/modules/config/default/config.el b/modules/config/default/config.el
new file mode 100644
index 000000000..47faefde4
--- /dev/null
+++ b/modules/config/default/config.el
@@ -0,0 +1,265 @@
+;;; config/default/config.el -*- lexical-binding: t; -*-
+
+(defvar +default-minibuffer-maps
+ `(minibuffer-local-map
+ minibuffer-local-ns-map
+ minibuffer-local-completion-map
+ minibuffer-local-must-match-map
+ minibuffer-local-isearch-map
+ read-expression-map
+ ,@(if (featurep! :completion ivy) '(ivy-minibuffer-map)))
+ "A list of all the keymaps used for the minibuffer.")
+
+
+;;
+;;; Reasonable defaults
+
+(after! epa
+ (setq epa-file-encrypt-to
+ (or epa-file-encrypt-to
+ ;; Collect all public key IDs with your username
+ (unless (string-empty-p user-full-name)
+ (cl-loop for key in (ignore-errors (epg-list-keys (epg-make-context) user-full-name))
+ collect (epg-sub-key-id (car (epg-key-sub-key-list key)))))
+ user-mail-address)
+ ;; With GPG 2.1, this forces gpg-agent to use the Emacs minibuffer to
+ ;; prompt for the key passphrase.
+ epa-pinentry-mode 'loopback))
+
+
+;;
+;;; Smartparens config
+
+(when (featurep! +smartparens)
+ ;; You can disable :unless predicates with (sp-pair "'" nil :unless nil)
+ ;; And disable :post-handlers with (sp-pair "{" nil :post-handlers nil)
+ ;; or specific :post-handlers with:
+ ;; (sp-pair "{" nil :post-handlers '(:rem ("| " "SPC")))
+ (after! smartparens
+ ;; Autopair quotes more conservatively; if I'm next to a word/before another
+ ;; quote, I likely don't want to open a new pair.
+ (let ((unless-list '(sp-point-before-word-p
+ sp-point-after-word-p
+ sp-point-before-same-p)))
+ (sp-pair "'" nil :unless unless-list)
+ (sp-pair "\"" nil :unless unless-list))
+
+ ;; Expand {|} => { | }
+ ;; Expand {|} => {
+ ;; |
+ ;; }
+ (dolist (brace '("(" "{" "["))
+ (sp-pair brace nil
+ :post-handlers '(("||\n[i]" "RET") ("| " "SPC"))
+ ;; I likely don't want a new pair if adjacent to a word or opening brace
+ :unless '(sp-point-before-word-p sp-point-before-same-p)))
+
+ ;; Major-mode specific fixes
+ (sp-local-pair '(ruby-mode enh-ruby-mode) "{" "}"
+ :pre-handlers '(:rem sp-ruby-pre-handler)
+ :post-handlers '(:rem sp-ruby-post-handler))
+
+ ;; Don't do square-bracket space-expansion where it doesn't make sense to
+ (sp-local-pair '(emacs-lisp-mode org-mode markdown-mode gfm-mode)
+ "[" nil :post-handlers '(:rem ("| " "SPC")))
+
+ ;; Reasonable default pairs for HTML-style comments
+ (sp-local-pair (append sp--html-modes '(markdown-mode gfm-mode))
+ ""
+ :unless '(sp-point-before-word-p sp-point-before-same-p)
+ :actions '(insert) :post-handlers '(("| " "SPC")))
+
+ ;; Disable electric keys in C modes because it interferes with smartparens
+ ;; and custom bindings. We'll do it ourselves (mostly).
+ (after! cc-mode
+ (c-toggle-electric-state -1)
+ (c-toggle-auto-newline -1)
+ (setq c-electric-flag nil)
+ (dolist (key '("#" "{" "}" "/" "*" ";" "," ":" "(" ")" "\177"))
+ (define-key c-mode-base-map key nil)))
+
+ ;; Expand C-style doc comment blocks. Must be done manually because some of
+ ;; these languages use specialized (and deferred) parsers, whose state we
+ ;; can't access while smartparens is doing its thing.
+ (defun +default-expand-doc-comment-block (&rest _ignored)
+ (let ((indent (current-indentation)))
+ (newline-and-indent)
+ (save-excursion
+ (newline)
+ (insert (make-string indent 32) " */")
+ (delete-char 2))))
+ (sp-local-pair
+ '(js2-mode typescript-mode rjsx-mode rust-mode c-mode c++-mode objc-mode
+ csharp-mode java-mode php-mode css-mode scss-mode less-css-mode
+ stylus-mode)
+ "/*" "*/"
+ :actions '(insert)
+ :post-handlers '(("| " "SPC") ("|\n*/[i][d-2]" "RET") (+default-expand-doc-comment-block "*")))
+
+ ;; Highjacks backspace to:
+ ;; a) balance spaces inside brackets/parentheses ( | ) -> (|)
+ ;; b) delete space-indented `tab-width' steps at a time
+ ;; c) close empty multiline brace blocks in one step:
+ ;; {
+ ;; |
+ ;; }
+ ;; becomes {|}
+ ;; d) refresh smartparens' :post-handlers, so SPC and RET expansions work
+ ;; even after a backspace.
+ ;; e) properly delete smartparen pairs when they are encountered, without
+ ;; the need for strict mode.
+ ;; f) do none of this when inside a string
+ (advice-add #'delete-backward-char :override #'+default*delete-backward-char)
+
+ ;; Makes `newline-and-indent' continue comments (and more reliably)
+ (advice-add #'newline-and-indent :around #'+default*newline-indent-and-continue-comments)))
+
+
+;;
+;;; Keybinding fixes
+
+;; This section is dedicated to "fixing" certain keys so that they behave
+;; sensibly (and consistently with similar contexts).
+
+;; Consistently use q to quit windows
+(after! tabulated-list
+ (define-key tabulated-list-mode-map "q" #'quit-window))
+
+;; OS specific fixes
+(when IS-MAC
+ ;; Fix MacOS shift+tab
+ (define-key input-decode-map [S-iso-lefttab] [backtab])
+ ;; Fix conventional OS keys in Emacs
+ (map! "s-`" #'other-frame ; fix frame-switching
+ ;; fix OS window/frame navigation/manipulation keys
+ "s-w" #'delete-window
+ "s-W" #'delete-frame
+ "s-n" #'+default/new-buffer
+ "s-N" #'make-frame
+ "s-q" (if (daemonp) #'delete-frame #'save-buffers-kill-terminal)
+ "C-s-f" #'toggle-frame-fullscreen
+ ;; Restore somewhat common navigation
+ "s-l" #'goto-line
+ ;; Restore OS undo, save, copy, & paste keys (without cua-mode, because
+ ;; it imposes some other functionality and overhead we don't need)
+ "s-f" #'swiper
+ "s-z" #'undo
+ "s-Z" #'redo
+ "s-c" (if (featurep 'evil) #'evil-yank #'copy-region-as-kill)
+ "s-v" #'yank
+ "s-s" #'save-buffer
+ ;; Buffer-local font scaling
+ "s-+" (λ! (text-scale-set 0))
+ "s-=" #'text-scale-increase
+ "s--" #'text-scale-decrease
+ ;; Conventional text-editing keys & motions
+ "s-a" #'mark-whole-buffer
+ :g "s-/" (λ! (save-excursion (comment-line 1)))
+ :n "s-/" #'evil-commentary-line
+ :v "s-/" #'evil-commentary
+ :gni [s-return] #'+default/newline-below
+ :gni [S-s-return] #'+default/newline-above
+ :gi [s-backspace] #'doom/backward-kill-to-bol-and-indent
+ :gi [s-left] #'doom/backward-to-bol-or-indent
+ :gi [s-right] #'doom/forward-to-last-non-comment-or-eol
+ :gi [M-backspace] #'backward-kill-word
+ :gi [M-left] #'backward-word
+ :gi [M-right] #'forward-word))
+
+
+;;
+;;; Keybind schemes
+
+;; Custom help keys -- these aren't under `+bindings' because they ought to be
+;; universal.
+(define-key! help-map
+ ;; new keybinds
+ "'" #'describe-char
+ "A" #'doom/describe-autodefs
+ "B" #'doom/open-bug-report
+ "D" #'doom/open-manual
+ "E" #'doom/open-vanilla-sandbox
+ "M" #'doom/describe-active-minor-mode
+ "O" #'+lookup/online
+ "T" #'doom/toggle-profiler
+ "V" #'set-variable
+ "W" #'+default/man-or-woman
+ "C-k" #'describe-key-briefly
+ "C-l" #'describe-language-environment
+ "C-m" #'info-emacs-manual
+ "C-v" #'doom/version
+
+ ;; Unbind `help-for-help'. Conflicts with which-key's help command for the
+ ;; h prefix. It's already on ? and F1 anyway.
+ "C-h" nil
+
+ ;; replacement keybinds
+ ;; replaces `info-emacs-manual' b/c it's on C-m now
+ "r" nil
+ "rr" #'doom/reload
+ "rt" #'doom/reload-theme
+ "rp" #'doom/reload-packages
+ "rf" #'doom/reload-font
+ "re" #'doom/reload-env
+
+ ;; replaces `apropos-command'
+ "a" #'apropos
+ ;; replaces `describe-copying' b/c not useful
+ "C-c" #'describe-coding-system
+ ;; replaces `apropos-documentation' b/c `apropos' covers this
+ "d" #'doom/describe-module
+ ;; replaces `Info-got-emacs-command-node' b/c redundant w/ `Info-goto-node'
+ "F" #'describe-face
+ ;; replaces `view-hello-file' b/c annoying
+ "h" #'doom/describe-symbol
+ ;; replaces `describe-language-environment' b/c remapped to C-l
+ "L" #'global-command-log-mode
+ ;; replaces `view-emacs-news' b/c it's on C-n too
+ "n" #'doom/open-news
+ ;; replaces `finder-by-keyword'
+ ;; "p" #'doom/describe-package
+ ;; replaces `describe-package' b/c redundant w/ `doom/describe-package'
+ "P" #'find-library)
+
+(after! which-key
+ (which-key-add-key-based-replacements doom-leader-key "")
+ (which-key-add-key-based-replacements doom-localleader-key "")
+
+ (which-key-add-key-based-replacements "C-h r" "reload")
+ (when (featurep 'evil)
+ (which-key-add-key-based-replacements (concat doom-leader-key " r") "reload")
+ (which-key-add-key-based-replacements (concat doom-leader-alt-key " r") "reload")))
+
+
+(when (featurep! +bindings)
+ ;; Make M-x harder to miss
+ (define-key! 'override
+ "M-x" #'execute-extended-command
+ "A-x" #'execute-extended-command)
+
+ ;; A Doom convention where C-s on popups and interactive searches will invoke
+ ;; ivy/helm for their superior filtering.
+ (define-key! :keymaps +default-minibuffer-maps
+ "C-s" (if (featurep! :completion ivy)
+ #'counsel-minibuffer-history
+ #'helm-minibuffer-history))
+
+ ;; Smarter C-a/C-e for both Emacs and Evil. C-a will jump to indentation.
+ ;; Pressing it again will send you to the true bol. Same goes for C-e, except
+ ;; it will ignore comments+trailing whitespace before jumping to eol.
+ (map! :gi "C-a" #'doom/backward-to-bol-or-indent
+ :gi "C-e" #'doom/forward-to-last-non-comment-or-eol
+ ;; Standardize the behavior of M-RET/M-S-RET as a "add new item
+ ;; below/above" key.
+ :gni [M-return] #'+default/newline-below
+ :gni [M-S-return] #'+default/newline-above
+ :gni [C-return] #'+default/newline-below
+ :gni [C-S-return] #'+default/newline-above))
+
+
+;;
+;;; Bootstrap configs
+
+(if (featurep 'evil)
+ (load! "+evil")
+ (load! "+emacs"))
diff --git a/modules/config/default/packages.el b/modules/config/default/packages.el
new file mode 100644
index 000000000..54cfa79b1
--- /dev/null
+++ b/modules/config/default/packages.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; config/default/packages.el
+
+(unless (featurep! :feature evil)
+ (package! winum)
+ (package! expand-region))
diff --git a/modules/config/literate/autoload.el b/modules/config/literate/autoload.el
new file mode 100644
index 000000000..93880736a
--- /dev/null
+++ b/modules/config/literate/autoload.el
@@ -0,0 +1,14 @@
+;;; config/literate/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defalias '+literate/reload #'doom/reload)
+
+;;;###autoload
+(defun +literate|recompile-maybe ()
+ "Recompile config.org if we're editing an org file in our DOOMDIR.
+
+We assume any org file in `doom-private-dir' is connected to your literate
+config, and should trigger a recompile if changed."
+ (when (and (eq major-mode 'org-mode)
+ (file-in-directory-p buffer-file-name doom-private-dir))
+ (+literate-tangle 'force)))
diff --git a/modules/config/literate/init.el b/modules/config/literate/init.el
new file mode 100644
index 000000000..4eb3b8d18
--- /dev/null
+++ b/modules/config/literate/init.el
@@ -0,0 +1,49 @@
+;;; config/literate/init.el -*- lexical-binding: t; -*-
+
+(defvar +literate-config-file
+ (expand-file-name "config.org" doom-private-dir)
+ "The file path of your literate config file.")
+
+(defvar +literate-config-cache-file
+ (expand-file-name "literate-last-compile" doom-cache-dir)
+ "The file path that `+literate-config-file' will be tangled to, then
+byte-compiled from.")
+
+
+;;
+(defun +literate-tangle (&optional force-p)
+ "Tangles `+literate-config-file' if it has changed."
+ (let ((default-directory doom-private-dir)
+ (org +literate-config-file))
+ (when (or force-p (file-newer-than-file-p org +literate-config-cache-file))
+ (message "Compiling your literate config...")
+
+ (let* ((org (file-truename +literate-config-file))
+ (dest (concat (file-name-sans-extension org) ".el")))
+ (or (and (if (fboundp 'org-babel-tangle-file)
+ (org-babel-tangle-file org dest "emacs-lisp")
+ ;; We tangle in a separate, blank process because loading it
+ ;; here would load all of :lang org (very expensive!).
+ (zerop (call-process
+ "emacs" nil nil nil
+ "-q" "--batch" "-l" "ob-tangle" "--eval"
+ (format "(org-babel-tangle-file %S %S \"emacs-lisp\")"
+ org dest))))
+ ;; Write the cache file to serve as our mtime cache
+ (with-temp-file +literate-config-cache-file
+ (message "Done!")))
+ (warn "There was a problem tangling your literate config!"))))))
+
+
+;; Let 'er rip!
+(when noninteractive
+ (require 'ob-tangle nil t))
+
+(+literate-tangle (or doom-reloading-p noninteractive))
+;; No need to load the resulting file. Doom will do this for us after all
+;; modules have finished loading.
+
+
+;; Recompile our literate config if we modify it
+(after! org
+ (add-hook 'after-save-hook #'+literate|recompile-maybe))
diff --git a/modules/editor/fold/README.org b/modules/editor/fold/README.org
new file mode 100644
index 000000000..55c93ff78
--- /dev/null
+++ b/modules/editor/fold/README.org
@@ -0,0 +1,32 @@
+#+TITLE: editor/fold
+#+DATE: February 17, 2019
+#+SINCE: v2.1
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC_3:noexport:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Prerequisites][Prerequisites]]
+- [[Features][Features]]
+- [[Configuration][Configuration]]
+- [[Troubleshooting][Troubleshooting]]
+
+* Description
+This module marries hideshow, vimish-fold and outline-minor-mode to bring you
+marker, indent and syntax-based code folding for as many languages as possible.
+
+** Module Flags
+This module provides no flags.
+
+** Plugins
++ evil-vimish-fold*
+
+* Prerequisites
+This module has no prerequisites.
+
+* TODO Features
+
+* TODO Configuration
+
+* TODO Troubleshooting
diff --git a/modules/editor/fold/autoload/evil.el b/modules/editor/fold/autoload/evil.el
new file mode 100644
index 000000000..3a16c060e
--- /dev/null
+++ b/modules/editor/fold/autoload/evil.el
@@ -0,0 +1,150 @@
+;;; editor/fold/autoload/evil.el -*- lexical-binding: t; -*-
+;;;###if (featurep! :feature evil)
+
+(require 'hideshow)
+
+;; `hideshow' is a decent code folding implementation, but it won't let you
+;; create custom folds. `vimish-fold' offers custom folds, but essentially
+;; ignores any other type of folding (indent or custom markers, which
+;; hs-minor-mode and `outline-mode' give you).
+;;
+;; So this is my effort to combine them.
+
+(defun +fold--vimish-fold-p ()
+ (and (featurep 'vimish-fold)
+ (cl-some #'vimish-fold--vimish-overlay-p
+ (overlays-at (point)))))
+
+(defun +fold--outline-fold-p ()
+ (and (or (bound-and-true-p outline-minor-mode)
+ (derived-mode-p 'outline-mode))
+ (outline-on-heading-p)))
+
+(defun +fold--hideshow-fold-p ()
+ (hs-minor-mode +1)
+ (save-excursion
+ (ignore-errors
+ (or (hs-looking-at-block-start-p)
+ (hs-find-block-beginning)))))
+
+
+;;
+;; Code folding
+
+(defmacro +fold-from-eol (&rest body)
+ "Perform action after moving to the end of the line."
+ `(save-excursion
+ (end-of-line)
+ ,@body))
+
+;;;###autoload
+(defun +fold/toggle ()
+ "Toggle the fold at point.
+
+Targets `vimmish-fold', `hideshow' and `outline' folds."
+ (interactive)
+ (save-excursion
+ (cond ((+fold--vimish-fold-p) (vimish-fold-toggle))
+ ((+fold--outline-fold-p)
+ (cl-letf (((symbol-function #'outline-hide-subtree)
+ (symbol-function #'outline-hide-entry)))
+ (outline-toggle-children)))
+ ((+fold--hideshow-fold-p) (+fold-from-eol (hs-toggle-hiding))))))
+
+;;;###autoload
+(defun +fold/open ()
+ "Open the folded region at point.
+
+Targets `vimmish-fold', `hideshow' and `outline' folds."
+ (interactive)
+ (save-excursion
+ (cond ((+fold--vimish-fold-p) (vimish-fold-unfold))
+ ((+fold--outline-fold-p)
+ (outline-show-children)
+ (outline-show-entry))
+ ((+fold--hideshow-fold-p) (+fold-from-eol (hs-show-block))))))
+
+;;;###autoload
+(defun +fold/close ()
+ "Close the folded region at point.
+
+Targets `vimmish-fold', `hideshow' and `outline' folds."
+ (interactive)
+ (save-excursion
+ (cond ((+fold--vimish-fold-p) (vimish-fold-refold))
+ ((+fold--hideshow-fold-p) (+fold-from-eol (hs-hide-block)))
+ ((+fold--outline-fold-p) (outline-hide-subtree)))))
+
+;;;###autoload
+(defun +fold/open-all (&optional level)
+ "Open folds at LEVEL (or all folds if LEVEL is nil)."
+ (interactive
+ (list (if current-prefix-arg (prefix-numeric-value current-prefix-arg))))
+ (when (featurep 'vimish-fold)
+ (vimish-fold-unfold-all))
+ (save-excursion
+ (if (integerp level)
+ (progn
+ (outline-hide-sublevels (max 1 (1- level)))
+ (hs-life-goes-on
+ (hs-hide-level-recursive (1- level) (point-min) (point-max))))
+ (hs-show-all)
+ (when (fboundp 'outline-show-all)
+ (outline-show-all)))))
+
+;;;###autoload
+(defun +fold/close-all (&optional level)
+ "Close folds at LEVEL (or all folds if LEVEL is nil)."
+ (interactive
+ (list (if current-prefix-arg (prefix-numeric-value current-prefix-arg))))
+ (save-excursion
+ (when (featurep 'vimish-fold)
+ (vimish-fold-refold-all))
+ (hs-life-goes-on
+ (if (integerp level)
+ (hs-hide-level-recursive (1- level) (point-min) (point-max))
+ (hs-hide-all)))))
+
+(defun +fold--invisible-points (count)
+ (let (points)
+ (save-excursion
+ (catch 'abort
+ (if (< count 0) (beginning-of-line))
+ (while (re-search-forward hs-block-start-regexp nil t
+ (if (> count 0) 1 -1))
+ (unless (invisible-p (point))
+ (end-of-line)
+ (when (hs-already-hidden-p)
+ (push (point) points)
+ (when (>= (length points) count)
+ (throw 'abort nil))))
+ (forward-line (if (> count 0) 1 -1)))))
+ points))
+
+;;;###autoload
+(defun +fold/next (count)
+ "Jump to the next vimish fold, outline heading or folded region."
+ (interactive "p")
+ (cl-loop with orig-pt = (point)
+ for fn
+ in (list (lambda ()
+ (when hs-block-start-regexp
+ (car (+fold--invisible-points count))))
+ (lambda ()
+ (if (> count 0)
+ (evil-vimish-fold/next-fold count)
+ (evil-vimish-fold/previous-fold (- count)))
+ (if (/= (point) orig-pt) (point))))
+ if (save-excursion (funcall fn))
+ collect it into points
+ finally do
+ (if-let* ((pt (car (sort points (if (> count 0) #'< #'>)))))
+ (goto-char pt)
+ (message "No more folds %s point" (if (> count 0) "after" "before"))
+ (goto-char orig-pt))))
+
+;;;###autoload
+(defun +fold/previous (count)
+ "Jump to the previous vimish fold, outline heading or folded region."
+ (interactive "p")
+ (+fold/next (- count)))
diff --git a/modules/editor/fold/autoload/fold.el b/modules/editor/fold/autoload/fold.el
new file mode 100644
index 000000000..e631c31c5
--- /dev/null
+++ b/modules/editor/fold/autoload/fold.el
@@ -0,0 +1,87 @@
+;;; editor/fold/autoload/fold.el -*- lexical-binding: t; -*-
+
+(defface +fold-hideshow-folded-face
+ `((t (:inherit font-lock-comment-face :weight light)))
+ "Face to hightlight `hideshow' overlays."
+ :group 'doom-themes)
+
+;;;###autoload
+(defun +fold-hideshow*ensure-mode (&rest _)
+ "Ensure hs-minor-mode is enabled."
+ (unless (bound-and-true-p hs-minor-mode)
+ (hs-minor-mode +1)))
+
+;;;###autoload
+(defun +fold-hideshow-haml-forward-sexp (arg)
+ (haml-forward-sexp arg)
+ (move-beginning-of-line 1))
+
+;;;###autoload
+(defun +fold-hideshow-forward-block-by-indent (_arg)
+ (let ((start (current-indentation)))
+ (forward-line)
+ (unless (= start (current-indentation))
+ (let ((range (+fold-hideshow-indent-range)))
+ (goto-char (cadr range))
+ (end-of-line)))))
+
+;;;###autoload
+(defun +fold-hideshow-set-up-overlay (ov)
+ (when (eq 'code (overlay-get ov 'hs))
+ (when (featurep 'vimish-fold)
+ (overlay-put
+ ov 'before-string
+ (propertize "…" 'display
+ (list vimish-fold-indication-mode
+ 'empty-line
+ 'vimish-fold-fringe))))
+ (overlay-put
+ ov 'display (propertize " [...] " 'face '+fold-hideshow-folded-face))))
+
+
+;;
+;; Indentation detection
+
+(defun +fold--hideshow-empty-line-p ()
+ (string= "" (string-trim (thing-at-point 'line))))
+
+(defun +fold--hideshow-geq-or-empty-p ()
+ (or (+fold--hideshow-empty-line-p) (>= (current-indentation) base)))
+
+(defun +fold--hideshow-g-or-empty-p ()
+ (or (+fold--hideshow-empty-line-p) (> (current-indentation) base)))
+
+(defun +fold--hideshow-seek (start direction before skip predicate)
+ "Seeks forward (if direction is 1) or backward (if direction is -1) from start, until predicate
+fails. If before is nil, it will return the first line where predicate fails, otherwise it returns
+the last line where predicate holds."
+ (save-excursion
+ (goto-char start)
+ (goto-char (point-at-bol))
+ (let ((bnd (if (> 0 direction)
+ (point-min)
+ (point-max)))
+ (pt (point)))
+ (when skip (forward-line direction))
+ (cl-loop while (and (/= (point) bnd) (funcall predicate))
+ do (progn
+ (when before (setq pt (point-at-bol)))
+ (forward-line direction)
+ (unless before (setq pt (point-at-bol)))))
+ pt)))
+
+(defun +fold-hideshow-indent-range (&optional point)
+ "Return the point at the begin and end of the text block with the same (or
+greater) indentation. If `point' is supplied and non-nil it will return the
+begin and end of the block surrounding point."
+ (save-excursion
+ (when point
+ (goto-char point))
+ (let ((base (current-indentation))
+ (begin (point))
+ (end (point)))
+ (setq begin (+fold--hideshow-seek begin -1 t nil #'+fold--hideshow-geq-or-empty-p)
+ begin (+fold--hideshow-seek begin 1 nil nil #'+fold--hideshow-g-or-empty-p)
+ end (+fold--hideshow-seek end 1 t nil #'+fold--hideshow-geq-or-empty-p)
+ end (+fold--hideshow-seek end -1 nil nil #'+fold--hideshow-empty-line-p))
+ (list begin end base))))
diff --git a/modules/editor/fold/config.el b/modules/editor/fold/config.el
new file mode 100644
index 000000000..f14221525
--- /dev/null
+++ b/modules/editor/fold/config.el
@@ -0,0 +1,73 @@
+;;; editor/fold/config.el -*- lexical-binding: t; -*-
+
+(when (featurep! :feature evil)
+ ;; Add vimish-fold, outline-mode & hideshow support to folding commands
+ (define-key! 'global
+ [remap evil-toggle-fold] #'+fold/toggle
+ [remap evil-close-fold] #'+fold/close
+ [remap evil-open-fold] #'+fold/open
+ [remap evil-open-fold-rec] #'+fold/open
+ [remap evil-close-folds] #'+fold/close-all
+ [remap evil-open-folds] #'+fold/open-all)
+ (evil-define-key* 'motion 'global
+ "zj" #'+fold/next
+ "zk" #'+fold/previous))
+
+
+;;
+;; Packages
+
+(def-package! hideshow ; built-in
+ :defer t
+ :init
+ ;; Ensure `hs-minor-mode' is active when triggering these commands
+ (advice-add #'hs-toggle-hiding :before #'+fold-hideshow*ensure-mode)
+ (advice-add #'hs-hide-block :before #'+fold-hideshow*ensure-mode)
+ (advice-add #'hs-hide-level :before #'+fold-hideshow*ensure-mode)
+ (advice-add #'hs-show-all :before #'+fold-hideshow*ensure-mode)
+ (advice-add #'hs-hide-all :before #'+fold-hideshow*ensure-mode)
+ :config
+ (setq hs-hide-comments-when-hiding-all nil
+ ;; Nicer code-folding overlays (with fringe indicators)
+ hs-set-up-overlay #'+fold-hideshow-set-up-overlay)
+
+ ;; extra folding support for more languages
+ (unless (assq 't hs-special-modes-alist)
+ (setq hs-special-modes-alist
+ (append
+ '((vimrc-mode "{{{" "}}}" "\"")
+ (yaml-mode "\\s-*\\_<\\(?:[^:]+\\)\\_>"
+ ""
+ "#"
+ +fold-hideshow-forward-block-by-indent nil)
+ (haml-mode "[#.%]" "\n" "/" +fold-hideshow-haml-forward-sexp nil)
+ (ruby-mode "class\\|d\\(?:ef\\|o\\)\\|module\\|[[{]"
+ "end\\|[]}]"
+ "#\\|=begin"
+ ruby-forward-sexp)
+ (enh-ruby-mode "class\\|d\\(?:ef\\|o\\)\\|module\\|[[{]"
+ "end\\|[]}]"
+ "#\\|=begin"
+ enh-ruby-forward-sexp nil)
+ (matlab-mode "if\\|switch\\|case\\|otherwise\\|while\\|for\\|try\\|catch"
+ "end"
+ nil (lambda (_arg) (matlab-forward-sexp))))
+ hs-special-modes-alist
+ '((t))))))
+
+
+(def-package! evil-vimish-fold
+ :when (featurep! :feature evil)
+ :commands (evil-vimish-fold/next-fold evil-vimish-fold/previous-fold
+ evil-vimish-fold/delete evil-vimish-fold/delete-all
+ evil-vimish-fold/create evil-vimish-fold/create-line)
+ :init
+ (setq vimish-fold-dir (concat doom-cache-dir "vimish-fold/")
+ vimish-fold-indication-mode 'right-fringe)
+ (evil-define-key* 'motion 'global
+ "zf" #'evil-vimish-fold/create
+ "zF" #'evil-vimish-fold/create-line
+ "zd" #'vimish-fold-delete
+ "zE" #'vimish-fold-delete-all)
+ :config
+ (vimish-fold-global-mode +1))
diff --git a/modules/ui/evil-goggles/packages.el b/modules/editor/fold/packages.el
similarity index 52%
rename from modules/ui/evil-goggles/packages.el
rename to modules/editor/fold/packages.el
index a4140cf69..40b63a215 100644
--- a/modules/ui/evil-goggles/packages.el
+++ b/modules/editor/fold/packages.el
@@ -1,5 +1,5 @@
;; -*- no-byte-compile: t; -*-
-;;; ui/evil-goggles/packages.el
+;;; editor/fold/packages.el
(when (featurep! :feature evil)
- (package! evil-goggles))
+ (package! evil-vimish-fold))
diff --git a/modules/editor/format/autoload/evil.el b/modules/editor/format/autoload/evil.el
new file mode 100644
index 000000000..c84caa83b
--- /dev/null
+++ b/modules/editor/format/autoload/evil.el
@@ -0,0 +1,8 @@
+;;; editor/format/autoload/evil.el -*- lexical-binding: t; -*-
+;;;###if (featurep! :feature evil)
+
+;;;###autoload (autoload '+format:region "editor/format/autoload/evil" nil t)
+(evil-define-operator +format:region (beg end)
+ "Evil ex interface to `+format/region'."
+ (interactive "")
+ (+format/region beg end))
diff --git a/modules/editor/format/autoload/format.el b/modules/editor/format/autoload/format.el
new file mode 100644
index 000000000..8cea65a04
--- /dev/null
+++ b/modules/editor/format/autoload/format.el
@@ -0,0 +1,229 @@
+;;; editor/format/autoload.el -*- lexical-binding: t; -*-
+
+(defvar +format-region-p nil
+ "Is non-nil if currently reformatting a selected region, rather than the whole
+buffer.")
+
+;;;###autoload
+(autoload 'format-all-probe "format-all")
+
+(defun +format--delete-whole-line (&optional arg)
+ "Delete the current line without putting it in the `kill-ring'.
+Derived from function `kill-whole-line'. ARG is defined as for that
+function.
+
+Stolen shamelessly from go-mode"
+ (setq arg (or arg 1))
+ (if (and (> arg 0)
+ (eobp)
+ (save-excursion (forward-visible-line 0) (eobp)))
+ (signal 'end-of-buffer nil))
+ (if (and (< arg 0)
+ (bobp)
+ (save-excursion (end-of-visible-line) (bobp)))
+ (signal 'beginning-of-buffer nil))
+ (cond ((zerop arg)
+ (delete-region (progn (forward-visible-line 0) (point))
+ (progn (end-of-visible-line) (point))))
+ ((< arg 0)
+ (delete-region (progn (end-of-visible-line) (point))
+ (progn (forward-visible-line (1+ arg))
+ (unless (bobp)
+ (backward-char))
+ (point))))
+ ((delete-region (progn (forward-visible-line 0) (point))
+ (progn (forward-visible-line arg) (point))))))
+
+;;;###autoload
+(defun +format--apply-rcs-patch (patch-buffer)
+ "Apply an RCS-formatted diff from PATCH-BUFFER to the current buffer.
+
+Stolen shamelessly from go-mode"
+ (let ((target-buffer (current-buffer))
+ ;; Relative offset between buffer line numbers and line numbers
+ ;; in patch.
+ ;;
+ ;; Line numbers in the patch are based on the source file, so
+ ;; we have to keep an offset when making changes to the
+ ;; buffer.
+ ;;
+ ;; Appending lines decrements the offset (possibly making it
+ ;; negative), deleting lines increments it. This order
+ ;; simplifies the forward-line invocations.
+ (line-offset 0)
+ (column (current-column)))
+ (save-excursion
+ (with-current-buffer patch-buffer
+ (goto-char (point-min))
+ (while (not (eobp))
+ (unless (looking-at "^\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)")
+ (error "Invalid rcs patch or internal error in +format--apply-rcs-patch"))
+ (forward-line)
+ (let ((action (match-string 1))
+ (from (string-to-number (match-string 2)))
+ (len (string-to-number (match-string 3))))
+ (cond
+ ((equal action "a")
+ (let ((start (point)))
+ (forward-line len)
+ (let ((text (buffer-substring start (point))))
+ (with-current-buffer target-buffer
+ (cl-decf line-offset len)
+ (goto-char (point-min))
+ (forward-line (- from len line-offset))
+ (insert text)))))
+ ((equal action "d")
+ (with-current-buffer target-buffer
+ (goto-char (point-min))
+ (forward-line (1- (- from line-offset)))
+ (cl-incf line-offset len)
+ (+format--delete-whole-line len)))
+ ((error "Invalid rcs patch or internal error in +format--apply-rcs-patch")))))))
+ (move-to-column column)))
+
+(defun +format--current-indentation ()
+ (save-excursion
+ (goto-char (point-min))
+ (skip-chars-forward " \t\n")
+ (current-indentation)))
+
+
+;;
+;; Public library
+
+(defun +format-completing-read ()
+ (require 'format-all)
+ (let* ((fmtlist (mapcar #'symbol-name (hash-table-keys format-all-format-table)))
+ (fmt (completing-read "Formatter: " fmtlist)))
+ (if fmt (cons (intern fmt) t))))
+
+;;;###autoload
+(defun +format*probe (orig-fn)
+ "Use `+format-with' instead, if it is set."
+ (if +format-with
+ (list +format-with t)
+ (funcall orig-fn)))
+
+;;;###autoload
+(defun +format-buffer (formatter mode-result &optional preserve-indent-p)
+ "Format the source code in the current buffer.
+
+Returns any of the following values:
+
+ 'unknown No formatter is defined for this major-mode
+ 'error Couldn't format buffer due to formatter errors
+ 'noop Buffer is already formatted
+
+Otherwise, returns a list: (list OUTPUT ERRORS FIRST-DIFF), where OUTPUT is the
+formatted text, ERRORS are any errors in string format, and FIRST-DIFF is the
+position of the first change in the buffer.
+
+See `+format/buffer' for the interactive version of this function, and
+`+format|buffer' to use as a `before-save-hook' hook."
+ (if (not formatter)
+ 'no-formatter
+ (let ((f-function (gethash formatter format-all-format-table))
+ (executable (format-all-formatter-executable formatter))
+ (indent 0))
+ (pcase-let
+ ((`(,output ,errput ,first-diff)
+ ;; Since `format-all' functions (and various formatting functions,
+ ;; like `gofmt') widen the buffer, in order to only format a region of
+ ;; text, we must make a copy of the buffer to apply formatting to.
+ (let ((output (buffer-substring-no-properties (point-min) (point-max))))
+ (with-temp-buffer
+ (insert output)
+ ;; Since we're piping a region of text to the formatter, remove
+ ;; any leading indentation to make it look like a file.
+ (when preserve-indent-p
+ (setq indent (+format--current-indentation))
+ (when (> indent 0)
+ (indent-rigidly (point-min) (point-max) (- indent))))
+ (funcall f-function executable mode-result)))))
+ (unwind-protect
+ (cond ((null output) 'error)
+ ((eq output t) 'noop)
+ ((let ((tmpfile (make-temp-file "doom-format"))
+ (patchbuf (get-buffer-create " *doom format patch*"))
+ (coding-system-for-read 'utf-8)
+ (coding-system-for-write 'utf-8))
+ (unwind-protect
+ (progn
+ (with-current-buffer patchbuf
+ (erase-buffer))
+ (with-temp-file tmpfile
+ (erase-buffer)
+ (insert output)
+ (when (> indent 0)
+ ;; restore indentation without affecting new
+ ;; indentation
+ (indent-rigidly (point-min) (point-max)
+ (max 0 (- indent (+format--current-indentation))))))
+ (if (zerop (call-process-region (point-min) (point-max) "diff" nil patchbuf nil "-n" "-" tmpfile))
+ 'noop
+ (+format--apply-rcs-patch patchbuf)
+ (list output errput first-diff)))
+ (kill-buffer patchbuf)
+ (delete-file tmpfile)))))
+ (unless (= 0 (length errput))
+ (message "Formatter error output:\n%s" errput)))))))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun +format/buffer (&optional arg)
+ "Format the source code in the current buffer."
+ (interactive "P")
+ (let ((+format-with (or (if arg (+format-completing-read)) +format-with)))
+ (pcase-let ((`(,formatter ,mode-result) (format-all-probe)))
+ (pcase
+ (+format-buffer
+ formatter mode-result
+ (or +format-preserve-indentation +format-region-p))
+ (`no-formatter
+ (when (called-interactively-p 'any)
+ (message "No formatter specified for %s" major-mode))
+ nil)
+ (`error (message "Failed to format buffer due to errors") nil)
+ (`noop (message "Buffer was already formatted") nil)
+ (_ (message "Formatted (%s)" formatter) t)))))
+
+;;;###autoload
+(defun +format/region (beg end &optional arg)
+ "Runs the active formatter on the lines within BEG and END.
+
+WARNING: this may not work everywhere. It will throw errors if the region
+contains a syntax error in isolation. It is mostly useful for formatting
+snippets or single lines."
+ (interactive "rP")
+ (save-restriction
+ (narrow-to-region beg end)
+ (let ((+format-region-p t))
+ (+format/buffer arg))))
+
+;;;###autoload
+(defun +format/region-or-buffer ()
+ "Runs the active formatter on the selected region (or whole buffer, if nothing
+is selected)."
+ (interactive)
+ (call-interactively
+ (if (use-region-p)
+ #'+format/region
+ #'+format/buffer)))
+
+
+;;
+;; Hooks
+
+;;;###autoload
+(defun +format|enable-on-save ()
+ "Enables formatting on save."
+ (add-hook 'before-save-hook #'+format|buffer nil t))
+
+;;;###autoload
+(defalias '+format|buffer #'+format/buffer
+ "Format the source code in the current buffer with minimal feedback.
+
+Meant for `before-save-hook'.")
diff --git a/modules/editor/format/autoload/settings.el b/modules/editor/format/autoload/settings.el
new file mode 100644
index 000000000..669a3f1a3
--- /dev/null
+++ b/modules/editor/format/autoload/settings.el
@@ -0,0 +1,203 @@
+;;; editor/format/autoload/settings.el -*- lexical-binding: t; -*-
+
+;; This must be redefined here because `format-all' only makes it available at
+;; compile time.
+(defconst +format-system-type
+ (cl-case system-type
+ (windows-nt 'windows)
+ (cygwin 'windows)
+ (darwin 'macos)
+ (gnu/linux 'linux)
+ (berkeley-unix
+ (save-match-data
+ (let ((case-fold-search t))
+ (cond ((string-match "freebsd" system-configuration) 'freebsd)
+ ((string-match "openbsd" system-configuration) 'openbsd)
+ ((string-match "netbsd" system-configuration) 'netbsd))))))
+ "Current operating system according to the format-all package.")
+
+(defun +format--resolve-system (choices)
+ "Get first choice matching `format-all-system-type' from CHOICES."
+ (cl-loop for choice in choices
+ if (atom choice) return choice
+ else if (eql +format-system-type (car choice))
+ return (cadr choice)))
+
+
+(defun +format--make-command (formatter &rest _)
+ `(format-all-buffer-thunk
+ (lambda (input)
+ (with-silent-modifications
+ (setq buffer-file-name ,(buffer-file-name (buffer-base-buffer))
+ default-directory ,default-directory)
+ (delay-mode-hooks (funcall ',major-mode))
+ (insert input)
+ (condition-case e
+ (progn
+ (doom-log "formatter (commandp) %s" #',formatter)
+ (call-interactively #',formatter)
+ (list nil ""))
+ (error (list t (error-message-string e))))))))
+
+(defun +format--make-function (formatter &rest _)
+ `(progn
+ (doom-log "formatter (functionp) %s" #',formatter)
+ (format-all-buffer-thunk #',formatter)))
+
+(defun +format--make-shell-command (command ok-statuses error-regexp)
+ (+format--make-shell-command-list (split-string command " " t)
+ ok-statuses error-regexp))
+
+(defun +format--make-shell-command-list (command-list ok-statuses error-regexp)
+ `(let (args)
+ (dolist (arg ',command-list)
+ (cond ((stringp arg)
+ (push arg args))
+ ((listp arg)
+ (catch 'skip
+ (let (subargs this)
+ (while (setq this (pop arg))
+ (cond ((not (stringp (car arg)))
+ (let ((val (eval (pop arg) t)))
+ (unless val (throw 'skip nil))
+ (push (format this val) subargs)))
+ ((stringp this)
+ (push this subargs))))
+ (setq args (append subargs args)))))))
+ (doom-log "formatter (arglist) %s" args)
+ (if ,(and (or ok-statuses error-regexp) t)
+ (apply #'format-all-buffer-hard
+ ',ok-statuses ,error-regexp
+ (reverse args))
+ (apply #'format-all-buffer-easy (reverse args)))))
+
+(cl-defun +format--set (name &key function modes unset)
+ (declare (indent defun))
+ (when (and unset (not (gethash name format-all-format-table)))
+ (error "'%s' formatter does not exist to be unset" name))
+ (puthash name function format-all-format-table)
+ (dolist (mode (doom-enlist modes))
+ (cl-destructuring-bind (m &optional probe)
+ (doom-enlist mode)
+ (if unset
+ (puthash m (assq-delete-all name (gethash key format-all-mode-table))
+ format-all-mode-table)
+ (format-all-pushhash
+ m (cons name (if probe `(lambda () ,probe)))
+ format-all-mode-table)))))
+
+;;;###autodef
+(cl-defun set-formatter!
+ (name formatter &key modes filter ok-statuses error-regexp)
+ "Define (or modify) a formatter named NAME.
+
+Supported keywords: :modes :install :filter :ok-statuses :error-regexp
+
+NAME is a symbol that identifies this formatter.
+
+FORMATTER can be a symbol referring to another formatter, a function, string or
+nested list.
+
+ If a function, it should be a formatter function that
+ `format-all-buffer-thunk' will accept.
+ If a string, it is assumed to be a shell command that the buffer's text will
+ be piped to (through stdin).
+ If a list, it should represent a shell command as a list of arguments. Each
+ element is either a string or list (STRING ARG) where STRING is a format
+ string and ARG is both a predicate and argument for STRING. If ARG is nil,
+ STRING will be omitted from the vector.
+
+MODES is a major mode, a list thereof, or a list of two-element sublists with
+the structure: (MAJOR-MODE FORM). FORM is evaluated when the buffer is formatted
+and its return value serves two purposes:
+
+ 1. It is a predicate for this formatter. Assuming the MAJOR-MODE matches the
+ current mode, if FORM evaluates to nil, the formatter is skipped.
+ 2. It's return value is made available to FORMATTER if it is a function or
+ list of shell arguments via the `mode-result' variable.
+
+FILTER is a function that takes three arguments: the formatted output, any error
+output and the position of the first change. This function must return these
+three after making whatever changes you like to them. This might be useful if
+the output contains ANSI color codes that need to be stripped out (as is the
+case with elm-format).
+
+OK-STATUSES and ERROR-REGEXP are ignored if FORMATTER is not a shell command.
+
+OK-STATUSES is a list of integer exit codes that should be treated as success
+codes. However, if ERROR-REGEXP is given, and the program's stderr contains that
+regexp, then the formatting is considered failed even if the exit status is in
+OK-STATUSES.
+
+Basic examples:
+
+ (set-formatter! 'asmfmt \"asmfmt\" :modes '(asm-mode nasm-mode))
+ (set-formatter! 'black \"black -q -\")
+ (set-formatter! 'html-tidy \"tidy -q -indent\" :modes '(html-mode web-mode))
+
+Advanced examples:
+
+ (set-formatter!
+ 'clang-format
+ '(\"clang-format\"
+ (\"-assume-filename=%S\" (or buffer-file-name mode-result \"\")))
+ :modes
+ '((c-mode \".c\")
+ (c++-mode \".cpp\")
+ (java-mode \".java\")
+ (objc-mode \".m\")
+ (protobuf-mode \".proto\")))
+
+ (set-formatter! 'html-tidy
+ '(\"tidy\" \"-q\" \"-indent\"
+ (\"-xml\" (memq major-mode '(nxml-mode xml-mode))))
+ :modes
+ '(html-mode
+ (web-mode (and (equal \"none\" web-mode-engine)
+ (car (member web-mode-content-type '(\"xml\" \"html\"))))))
+ :ok-statuses '(0 1)
+ :executable \"tidy\")
+
+ (set-formatter! 'html-tidy ; overwrite predefined html-tidy formatter
+ '(\"tidy\" \"-q\" \"-indent\"
+ \"--tidy-mark\" \"no\"
+ \"--drop-empty-elements\" \"no\"
+ \"--show-body-only\" \"auto\"
+ (\"--indent-spaces\" \"%d\" tab-width)
+ (\"--indent-with-tabs\" \"%s\" (if indent-tabs-mode \"yes\" \"no\"))
+ (\"-xml\" (memq major-mode '(nxml-mode xml-mode))))
+ :ok-statuses '(0 1)))
+
+ (set-formatter! 'elm-format
+ \"elm-format --yes --stdin\"
+ :filter
+ (lambda (output errput first-diff)
+ (list output
+ (format-all-remove-ansi-color errput)
+ first-diff)))"
+ (declare (indent defun))
+ (cl-check-type name symbol)
+ (after! format-all
+ (if (null formatter)
+ (+format--set name
+ :unset t
+ :modes modes)
+ (let ((fn (funcall (cond ((stringp formatter)
+ #'+format--make-shell-command)
+ ((listp formatter)
+ #'+format--make-shell-command-list)
+ ((and (commandp formatter)
+ (not (stringp formatter)))
+ #'+format--make-command)
+ ((functionp formatter)
+ #'+format--make-function))
+ formatter
+ ok-statuses
+ error-regexp)))
+ (cl-check-type filter (or function null))
+ (+format--set name
+ :function
+ `(lambda (executable mode-result)
+ ,(if filter `(apply #',filter ,fn) fn))
+ :modes modes)
+ name))))
diff --git a/modules/editor/format/config.el b/modules/editor/format/config.el
new file mode 100644
index 000000000..7e36d9bb4
--- /dev/null
+++ b/modules/editor/format/config.el
@@ -0,0 +1,58 @@
+;;; editor/format/config.el -*- lexical-binding: t; -*-
+
+(defvar +format-on-save-enabled-modes
+ '(not emacs-lisp-mode ; elisp's mechanisms are good enough
+ sql-mode) ; sqlformat is currently broken
+ "A list of major modes in which to reformat the buffer upon saving.
+
+If this list begins with `not', then it negates the list.
+If it is `t', it is enabled in all modes.
+If nil, it is disabled in all modes, the same as if the +onsave flag wasn't
+ used at all.
+
+Irrelevant if you do not have the +onsave flag enabled for this module.")
+
+(defvar +format-preserve-indentation t
+ "If non-nil, the leading indentation is preserved when formatting the whole
+buffer. This is particularly useful for partials.
+
+Indentation is always preserved when formatting regions. ")
+
+(defvar-local +format-with nil
+ "Set this to explicitly use a certain formatter for the current buffer.")
+
+
+;;
+;; Bootstrap
+
+(defun +format|enable-on-save-maybe ()
+ "Enable formatting on save in certain major modes.
+
+This is controlled by `+format-on-save-enabled-modes'."
+ (unless (or (eq major-mode 'fundamental-mode)
+ (cond ((booleanp +format-on-save-enabled-modes)
+ (null +format-on-save-enabled-modes))
+ ((eq (car +format-on-save-enabled-modes) 'not)
+ (memq major-mode (cdr +format-on-save-enabled-modes)))
+ ((not (memq major-mode +format-on-save-enabled-modes))))
+ (not (require 'format-all nil t)))
+ (add-hook 'before-save-hook #'+format|buffer nil t)))
+
+(when (featurep! +onsave)
+ (add-hook 'after-change-major-mode-hook #'+format|enable-on-save-maybe))
+
+
+;;
+;; Hacks
+
+;; Allow a specific formatter to be used by setting `+format-with', either
+;; buffer-locally or let-bound.
+(advice-add #'format-all-probe :around #'+format*probe)
+
+;; Doom uses a modded `format-all-buffer', which
+;; 1. Doesn't move the cursorafter reformatting,
+;; 2. Can reformat regions, rather than the entire buffer (while preserving
+;; leading indentation),
+;; 3. Applies changes via RCS patch, line by line, as not to protect buffer
+;; markers and avoid any jarring cursor+window scrolling.
+(advice-add #'format-all-buffer :override #'+format/buffer)
diff --git a/modules/editor/format/packages.el b/modules/editor/format/packages.el
new file mode 100644
index 000000000..858b6d9ee
--- /dev/null
+++ b/modules/editor/format/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; editor/format/packages.el
+
+(package! format-all)
diff --git a/modules/editor/format/test/test-format.el b/modules/editor/format/test/test-format.el
new file mode 100644
index 000000000..955123533
--- /dev/null
+++ b/modules/editor/format/test/test-format.el
@@ -0,0 +1,103 @@
+;; -*- no-byte-compile: t; -*-
+;;; editor/format/test/test-format.el
+
+(load! "../autoload/settings")
+(load! "../autoload/format")
+(require! :editor format)
+(require 'format-all)
+
+;;
+(describe "editor/format"
+ :var (format-all-format-table
+ format-all-mode-table)
+
+ (before-each
+ (setq format-all-format-table (make-hash-table)
+ format-all-mode-table (make-hash-table)))
+
+ (describe "set-formatter!"
+ (before-each
+ (set-formatter! 'test (lambda () (interactive))))
+
+ (it "defines a formatter"
+ (set-formatter! 'new (lambda () (interactive)))
+ (expect (gethash 'new format-all-mode-table) :to-equal nil)
+ (expect (functionp (gethash 'new format-all-format-table))))
+
+ (it "defines a formatter with modes"
+ (set-formatter! 'new (lambda () (interactive))
+ :modes '(a-mode (b-mode "x")))
+ (expect (gethash 'a-mode format-all-mode-table)
+ :to-equal '((new)))
+ (expect (gethash 'b-mode format-all-mode-table)
+ :to-equal '((new . (lambda () "x")))))
+
+ (it "replaces a pre-existing formatter"
+ (let ((old-fn (gethash 'test format-all-format-table)))
+ (set-formatter! 'test "echo")
+ (expect (gethash 'test format-all-format-table) :not :to-equal old-fn)))
+
+ (it "unsets a pre-existing formatter"
+ (set-formatter! 'test nil)
+ (expect (gethash 'test format-all-format-table) :to-be nil))
+
+ (it "errors when unsetting non-existent formatter"
+ (expect (set-formatter! 'doesnt-exist nil) :to-throw)))
+
+
+ ;; TODO
+ (xdescribe "hooks"
+ (describe "format|enable-on-save-maybe")
+ (describe "format|enable-on-save"))
+
+
+ ;; TODO
+ (xdescribe "formatting"
+ (before-each
+ (set-formatter! 'command
+ (lambda ()
+ (interactive)
+ (let ((first-line (car (split-string (buffer-string) "\n"))))
+ (erase-buffer)
+ (insert first-line)))
+ :modes '(text-mode))
+ (set-formatter! 'faulty-command
+ (lambda ()
+ (interactive)
+ (error "This is a test"))
+ :modes '(text-mode))
+ (set-formatter! 'function
+ (lambda (input)
+ (insert (car (split-string input "\n")))
+ (list nil nil))
+ :modes '(text-mode))
+ (set-formatter! 'shellcmd "head -n 1"
+ :modes '(text-mode))
+ (set-formatter! 'cmdlist '("head" "-n" "1")
+ :modes '(text-mode)))
+
+ (describe "with an interactive command"
+ (it "formats a buffer" )
+ (it "formats a region" )
+ (it "no-ops if no change" )
+ (it "doesn't modify the buffer in case of errors" )
+ (it "preserves indentation" ))
+
+ (describe "with a function"
+ (it "formats a buffer" )
+ (it "formats a region" )
+ (it "no-ops if no change" )
+ (it "doesn't modify the buffer in case of errors" )
+ (it "preserves indentation" ))
+
+ (describe "with a shell command")
+
+ (describe "with a shell command list"
+ (it "formats a buffer" )
+ (it "formats a region" )
+ (it "no-ops if no change" )
+ (it "doesn't modify the buffer in case of errors" )
+ (it "preserves indentation" )
+
+ (it "interpolates non-strings into format strings" )
+ (it "conditionally appends sublisted options" ))))
diff --git a/modules/editor/lispy/README.org b/modules/editor/lispy/README.org
new file mode 100644
index 000000000..a7d428860
--- /dev/null
+++ b/modules/editor/lispy/README.org
@@ -0,0 +1,36 @@
+#+TITLE: :editor lispy
+
+This modules adds [[https://github.com/noctuid/lispyville][lispy]] key functionality in Lisp languages.
+
+This includes:
+
+- Common Lisp
+- Emacs Lisp
+- Scheme
+- Racket
+- [[http://docs.hylang.org/en/stable/][Hy]]
+- [[http://lfe.io/][LFE]]
+- Clojure
+
+If evil is enabled, lispyville would also be activated for every mode where
+lispy is active
+
+The default key themes that are set are as follows:
+
+#+BEGIN_SRC emacs-lisp
+(lispyville-set-key-theme
+ '((operators normal)
+ c-w
+ (prettify insert)
+ (atom-movement normal visual)
+ slurp/barf-lispy
+ (wrap normal insert)
+ additional
+ additional-insert
+ (additional-wrap normal insert)
+ (escape insert)))
+#+END_SRC
+
+See noctuid's [[https://github.com/noctuid/lispyville/blob/master/README.org][README]] for more info on specific keybindings (starting [[https://github.com/noctuid/lispyville#operators-key-theme][here]]) of
+each key theme. Think of ~lispyville-set-key-theme~ as adding
+~parinfer-extensions~ via ~(setq parinfer-extensions '(blah blah blah))~.
diff --git a/modules/editor/lispy/config.el b/modules/editor/lispy/config.el
new file mode 100644
index 000000000..0e32a7893
--- /dev/null
+++ b/modules/editor/lispy/config.el
@@ -0,0 +1,29 @@
+;;; editor/lispy/config.el -*- lexical-binding: t; -*-
+
+(def-package! lispy
+ :hook ((common-lisp-mode . lispy-mode)
+ (emacs-lisp-mode . lispy-mode)
+ (scheme-mode . lispy-mode)
+ (racket-mode . lispy-mode)
+ (hy-mode . lispy-mode)
+ (lfe-mode . lispy-mode)
+ (clojure-mode . lispy-mode))
+ :config
+ (setq lispy-close-quotes-at-end-p t)
+ (add-hook 'lispy-mode-hook #'turn-off-smartparens-mode))
+
+(def-package! lispyville
+ :when (featurep! :feature evil)
+ :hook (lispy-mode . lispyville-mode)
+ :config
+ (lispyville-set-key-theme
+ '((operators normal)
+ c-w
+ (prettify insert)
+ (atom-movement normal visual)
+ slurp/barf-lispy
+ (wrap normal insert)
+ additional
+ additional-insert
+ (additional-wrap normal insert)
+ (escape insert))))
diff --git a/modules/editor/lispy/packages.el b/modules/editor/lispy/packages.el
new file mode 100644
index 000000000..953527d27
--- /dev/null
+++ b/modules/editor/lispy/packages.el
@@ -0,0 +1,7 @@
+;; -*- no-byte-compile: t; -*-
+;;; editor/lispyville/packages.el
+
+(package! lispy)
+
+(when (featurep! :feature evil)
+ (package! lispyville))
diff --git a/modules/feature/evil/autoload/evil-mc.el b/modules/editor/multiple-cursors/autoload/evil-mc.el
similarity index 64%
rename from modules/feature/evil/autoload/evil-mc.el
rename to modules/editor/multiple-cursors/autoload/evil-mc.el
index 5e76a5d8e..c83f1d52d 100644
--- a/modules/feature/evil/autoload/evil-mc.el
+++ b/modules/editor/multiple-cursors/autoload/evil-mc.el
@@ -1,7 +1,8 @@
-;;; feature/evil/autoload/evil-mc.el -*- lexical-binding: t; -*-
+;;; editor/multiple-cursors/autoload/evil-mc.el -*- lexical-binding: t; -*-
+;;;###if (featurep! :feature evil)
;;;###autoload
-(defun +evil/mc-toggle-cursors ()
+(defun +multiple-cursors/evil-mc-toggle-cursors ()
"Toggle frozen state of evil-mc cursors."
(interactive)
(setq evil-mc-frozen (not (and (evil-mc-has-cursors-p)
@@ -10,8 +11,8 @@
(message "evil-mc paused")
(message "evil-mc resumed")))
-;;;###autoload (autoload '+evil/mc-make-cursor-here "feature/evil/autoload/evil-mc" nil t)
-(evil-define-command +evil/mc-make-cursor-here ()
+;;;###autoload (autoload '+multiple-cursors/evil-mc-make-cursor-here "editor/multiple-cursors/autoload/evil-mc" nil t)
+(evil-define-command +multiple-cursors/evil-mc-make-cursor-here ()
"Create a cursor at point. If in visual block or line mode, then create
cursors on each line of the selection, on the column of the cursor. Otherwise
pauses cursors."
@@ -43,23 +44,31 @@ pauses cursors."
;; I assume I don't want the cursors to move yet
(evil-mc-make-cursor-here))))
-;;;###autoload (autoload '+evil:mc "feature/evil/autoload/evil-mc" nil t)
-(evil-define-command +evil:mc (beg end type pattern &optional bang)
+;;;###autoload (autoload '+multiple-cursors:evil-mc "editor/multiple-cursors/autoload/evil-mc" nil t)
+(evil-define-command +multiple-cursors:evil-mc (beg end type pattern &optional bang)
"Create mc cursors at each match of PATTERN within BEG and END, and leave the
cursor at the final match. If BANG, then treat PATTERN as literal."
:move-point nil
:evil-mc t
(interactive "/g>")
+ (unless (and (stringp pattern)
+ (not (string-empty-p pattern)))
+ (user-error "A regexp pattern is required"))
(require 'evil-mc)
- (setq evil-mc-pattern (cons (evil-mc-make-pattern (if bang (regexp-quote pattern) pattern) nil)
- (list beg end type)))
+ (setq evil-mc-pattern
+ (cons (evil-ex-make-search-pattern
+ (if bang (regexp-quote pattern) pattern))
+ (list beg end type)))
(save-excursion
(evil-with-restriction beg end
- (evil-mc-make-cursors-for-all)
- (evil-mc-print-cursors-info "Created")))
+ (evil-mc-make-cursors-for-all)))
(evil-exit-visual-state)
(evil-mc-goto-cursor
(if (= (evil-visual-direction) 1)
(evil-mc-find-last-cursor)
(evil-mc-find-first-cursor))
- nil))
+ nil)
+ (evil-mc-undo-cursor-at-pos (point))
+ (if (evil-mc-has-cursors-p)
+ (evil-mc-print-cursors-info "Created")
+ (evil-mc-message "No cursors were created")))
diff --git a/modules/editor/multiple-cursors/config.el b/modules/editor/multiple-cursors/config.el
new file mode 100644
index 000000000..86ec2bb0c
--- /dev/null
+++ b/modules/editor/multiple-cursors/config.el
@@ -0,0 +1,115 @@
+;;; editor/multiple-cursors/config.el -*- lexical-binding: t; -*-
+
+(def-package! evil-mc
+ :when (featurep! :feature evil)
+ :commands (evil-mc-make-cursor-here evil-mc-make-all-cursors
+ evil-mc-undo-all-cursors evil-mc-pause-cursors
+ evil-mc-resume-cursors evil-mc-make-and-goto-first-cursor
+ evil-mc-make-and-goto-last-cursor
+ evil-mc-make-cursor-move-next-line
+ evil-mc-make-cursor-move-prev-line evil-mc-make-cursor-at-pos
+ evil-mc-has-cursors-p evil-mc-make-and-goto-next-cursor
+ evil-mc-skip-and-goto-next-cursor evil-mc-make-and-goto-prev-cursor
+ evil-mc-skip-and-goto-prev-cursor evil-mc-make-and-goto-next-match
+ evil-mc-skip-and-goto-next-match evil-mc-skip-and-goto-next-match
+ evil-mc-make-and-goto-prev-match evil-mc-skip-and-goto-prev-match)
+ :init
+ (defvar evil-mc-key-map (make-sparse-keymap))
+ :config
+ (global-evil-mc-mode +1)
+ (setq evil-mc-enable-bar-cursor (not (or IS-MAC IS-WINDOWS)))
+
+ (after! smartparens
+ ;; Make evil-mc cooperate with smartparens better
+ (let ((vars (cdr (assq :default evil-mc-cursor-variables))))
+ (unless (memq (car sp--mc/cursor-specific-vars) vars)
+ (setcdr (assq :default evil-mc-cursor-variables)
+ (append vars sp--mc/cursor-specific-vars)))))
+
+ ;; Add custom commands to whitelisted commands
+ (dolist (fn '(doom/backward-to-bol-or-indent doom/forward-to-last-non-comment-or-eol
+ doom/backward-kill-to-bol-and-indent delete-char))
+ (add-to-list 'evil-mc-custom-known-commands `(,fn (:default . evil-mc-execute-default-call-with-count))))
+ ;; Have evil-mc work with explicit `evil-escape' (typically bound to C-g)
+ (add-to-list 'evil-mc-custom-known-commands '(evil-escape (:default . evil-mc-execute-default-evil-normal-state)))
+
+ ;; Activate evil-mc cursors upon switching to insert mode
+ (add-hook 'evil-insert-state-entry-hook #'evil-mc-resume-cursors)
+
+ ;; disable evil-escape in evil-mc; causes unwanted text on invocation
+ (add-to-list 'evil-mc-incompatible-minor-modes 'evil-escape-mode nil #'eq)
+
+ (defun +multiple-cursors|escape-multiple-cursors ()
+ "Clear evil-mc cursors and restore state."
+ (when (evil-mc-has-cursors-p)
+ (evil-mc-undo-all-cursors)
+ (evil-mc-resume-cursors)
+ t))
+ (add-hook 'doom-escape-hook #'+multiple-cursors|escape-multiple-cursors)
+
+ ;; Forward declare these so that ex completion and evil-mc support is
+ ;; recognized before the autoloaded functions are loaded.
+ (evil-add-command-properties '+evil:align :evil-mc t)
+ (evil-set-command-properties '+multiple-cursors:evil-mc
+ :move-point nil
+ :ex-arg 'global-match
+ :ex-bang t
+ :evil-mc t))
+
+
+(after! multiple-cursors-core
+ (setq mc/list-file (concat doom-etc-dir "mc-lists.el"))
+
+ ;; TODO multiple-cursors config for Emacs users?
+
+ ;; mc doesn't play well with evil, this attempts to assuage some of its
+ ;; problems so that any plugins that depend on multiple-cursors (which I have
+ ;; no control over) can still use it in relative safety.
+ (when (featurep! :feature evil)
+ (evil-define-key* '(normal emacs) mc/keymap [escape] #'mc/keyboard-quit)
+
+ (defvar +mc--compat-evil-prev-state nil)
+ (defvar +mc--compat-mark-was-active nil)
+
+ (defun +multiple-cursors|compat-switch-to-emacs-state ()
+ (when (and (bound-and-true-p evil-mode)
+ (not (memq evil-state '(insert emacs))))
+ (setq +mc--compat-evil-prev-state evil-state)
+ (when (region-active-p)
+ (setq +mc--compat-mark-was-active t))
+ (let ((mark-before (mark))
+ (point-before (point)))
+ (evil-emacs-state 1)
+ (when (or +mc--compat-mark-was-active (region-active-p))
+ (goto-char point-before)
+ (set-mark mark-before)))))
+ (add-hook 'multiple-cursors-mode-enabled-hook #'+multiple-cursors|compat-switch-to-emacs-state)
+
+ (defun +multiple-cursors|compat-back-to-previous-state ()
+ (when +mc--compat-evil-prev-state
+ (unwind-protect
+ (case +mc--compat-evil-prev-state
+ ((normal visual) (evil-force-normal-state))
+ (t (message "Don't know how to handle previous state: %S"
+ +mc--compat-evil-prev-state)))
+ (setq +mc--compat-evil-prev-state nil)
+ (setq +mc--compat-mark-was-active nil))))
+ (add-hook 'multiple-cursors-mode-disabled-hook #'+multiple-cursors|compat-back-to-previous-state)
+
+ ;; When running edit-lines, point will return (position + 1) as a
+ ;; result of how evil deals with regions
+ (defun +multiple-cursors*adjust-mark-for-evil (&rest _)
+ (when (and (bound-and-true-p evil-mode)
+ (not (memq evil-state '(insert emacs))))
+ (if (> (point) (mark))
+ (goto-char (1- (point)))
+ (push-mark (1- (mark))))))
+ (advice-add #'mc/edit-lines :before #'+multiple-cursors*adjust-mark-for-evil)
+
+ (defun +multiple-cursors|evil-compat-rect-switch-state ()
+ (if rectangular-region-mode
+ (+multiple-cursors|compat-switch-to-emacs-state)
+ (setq +mc--compat-evil-prev-state nil)))
+ (add-hook 'rectangular-region-mode-hook '+multiple-cursors|evil-compat-rect-switch-state)
+
+ (defvar mc--default-cmds-to-run-once nil)))
diff --git a/modules/editor/multiple-cursors/packages.el b/modules/editor/multiple-cursors/packages.el
new file mode 100644
index 000000000..5519a9a4e
--- /dev/null
+++ b/modules/editor/multiple-cursors/packages.el
@@ -0,0 +1,9 @@
+;; -*- no-byte-compile: t; -*-
+;;; editor/multiple-cursors/packages.el
+
+(cond ((featurep! :feature evil)
+ (package! evil-multiedit)
+ (package! evil-mc))
+
+ ((package! multiple-cursors)))
+
diff --git a/modules/editor/parinfer/README.org b/modules/editor/parinfer/README.org
new file mode 100644
index 000000000..e079c708a
--- /dev/null
+++ b/modules/editor/parinfer/README.org
@@ -0,0 +1,3 @@
+#+TITLE: :editor parinfer
+
+You can find out more about parinfer at https://shaunlebron.github.io/parinfer/
diff --git a/modules/editor/parinfer/config.el b/modules/editor/parinfer/config.el
new file mode 100644
index 000000000..fc24cb37a
--- /dev/null
+++ b/modules/editor/parinfer/config.el
@@ -0,0 +1,19 @@
+;;; editor/parinfer/config.el -*- lexical-binding: t; -*-
+
+(def-package! parinfer
+ :hook ((emacs-lisp-mode clojure-mode scheme-mode lisp-mode) . parinfer-mode)
+ :init
+ (setq parinfer-extensions
+ '(defaults
+ pretty-parens
+ smart-tab
+ smart-yank))
+ (when (featurep! :feature evil +everywhere)
+ (push 'evil parinfer-extensions))
+ :config
+ (map! :map parinfer-mode-map
+ "\"" nil ; smartparens handles this
+ :i "" #'parinfer-smart-tab:dwim-right-or-complete
+ :i "" #'parinfer-smart-tab:dwim-left
+ :localleader
+ :desc "Toggle parinfer-mode" "m" #'parinfer-toggle-mode))
diff --git a/modules/editor/parinfer/packages.el b/modules/editor/parinfer/packages.el
new file mode 100644
index 000000000..13875d6a6
--- /dev/null
+++ b/modules/editor/parinfer/packages.el
@@ -0,0 +1,14 @@
+;; -*- no-byte-compile: t; -*-
+;;; editor/parinfer/packages.el
+
+(when (featurep! :feature evil)
+ ;; Parinfer uses `evil-define-key' without loading evil, so if evil is
+ ;; installed *after* parinfer, parinfer will throw up void-function errors.
+ ;; because evil-define-key (a macro) wasn't expanded at compile-time. So we
+ ;; make sure evil is installed before parinfer...
+ (package! evil)
+ ;; ...and that it can see `evil-define-key' if evil was installed in a
+ ;; separate session:
+ (autoload 'evil-define-key "evil-core" nil nil 'macro))
+
+(package! parinfer)
diff --git a/modules/editor/rotate-text/autoload.el b/modules/editor/rotate-text/autoload.el
new file mode 100644
index 000000000..f696ead51
--- /dev/null
+++ b/modules/editor/rotate-text/autoload.el
@@ -0,0 +1,19 @@
+;;; editor/rotate-text/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(after! rotate-text
+ (add-to-list 'rotate-text-words '("true" "false")))
+
+;;;###autodef
+(cl-defun set-rotate-patterns! (modes &key symbols words patterns)
+ "Declare :symbols, :words or :patterns (all lists of strings) that
+`rotate-text' will cycle through."
+ (declare (indent defun))
+ (dolist (mode (doom-enlist modes))
+ (let ((fn-name (intern (format "+rotate-text|init-%s" mode))))
+ (fset fn-name
+ (lambda ()
+ (setq-local rotate-text-local-symbols symbols)
+ (setq-local rotate-text-local-words words)
+ (setq-local rotate-text-local-patterns patterns)))
+ (add-hook (intern (format "%s-hook" mode)) fn-name))))
diff --git a/modules/tools/rotate-text/packages.el b/modules/editor/rotate-text/packages.el
similarity index 76%
rename from modules/tools/rotate-text/packages.el
rename to modules/editor/rotate-text/packages.el
index f2c3547b7..955e531ed 100644
--- a/modules/tools/rotate-text/packages.el
+++ b/modules/editor/rotate-text/packages.el
@@ -1,4 +1,4 @@
;; -*- no-byte-compile: t; -*-
-;;; tools/rotate-text/packages.el
+;;; editor/rotate-text/packages.el
(package! rotate-text :recipe (:fetcher github :repo "debug-ito/rotate-text.el"))
diff --git a/modules/emacs/dired/config.el b/modules/emacs/dired/config.el
new file mode 100644
index 000000000..bcfe7cc00
--- /dev/null
+++ b/modules/emacs/dired/config.el
@@ -0,0 +1,262 @@
+;;; tools/dired/config.el -*- lexical-binding: t; -*-
+
+(def-package! dired
+ :commands dired-jump
+ :init
+ (setq ;; Always copy/delete recursively
+ dired-recursive-copies 'always
+ dired-recursive-deletes 'top
+ ;; Auto refresh dired, but be quiet about it
+ global-auto-revert-non-file-buffers t
+ auto-revert-verbose nil
+ dired-hide-details-hide-symlink-targets nil
+ ;; files
+ image-dired-dir (concat doom-cache-dir "image-dired/")
+ image-dired-db-file (concat image-dired-dir "db.el")
+ image-dired-gallery-dir (concat image-dired-dir "gallery/")
+ image-dired-temp-image-file (concat image-dired-dir "temp-image")
+ image-dired-temp-rotate-image-file (concat image-dired-dir "temp-rotate-image"))
+ :config
+ (let ((args (list "-aBhl" "--group-directories-first")))
+ (when IS-BSD
+ ;; Use GNU ls as `gls' from `coreutils' if available. Add `(setq
+ ;; dired-use-ls-dired nil)' to your config to suppress the Dired warning
+ ;; when not using GNU ls.
+ (if-let* ((gls (executable-find "gls")))
+ (setq insert-directory-program gls)
+ (setq args (delete "--group-directories-first" args))
+ (message "Cannot find `gls` (GNU ls). Install coreutils via your system package manager")))
+ (setq dired-listing-switches (string-join args " ")))
+
+ (defun +dired|sort-directories-first ()
+ "List directories first in dired buffers."
+ (save-excursion
+ (let (buffer-read-only)
+ (forward-line 2) ;; beyond dir. header
+ (sort-regexp-fields t "^.*$" "[ ]*." (point) (point-max))))
+ (and (featurep 'xemacs)
+ (fboundp 'dired-insert-set-properties)
+ (dired-insert-set-properties (point-min) (point-max)))
+ (set-buffer-modified-p nil))
+ (add-hook 'dired-after-readin-hook #'+dired|sort-directories-first)
+
+ ;; Automatically create missing directories when creating new files
+ (defun +dired|create-non-existent-directory ()
+ (let ((parent-directory (file-name-directory buffer-file-name)))
+ (when (and (not (file-exists-p parent-directory))
+ (y-or-n-p (format "Directory `%s' does not exist! Create it?" parent-directory)))
+ (make-directory parent-directory t))))
+ (add-to-list 'find-file-not-found-functions '+dired|create-non-existent-directory nil #'eq)
+
+ ;; Kill buffer when quitting dired buffers
+ (define-key dired-mode-map [remap quit-window] (λ! (quit-window t))))
+
+
+(def-package! dired-k
+ :unless (featurep! +ranger)
+ :hook (dired-initial-position . dired-k)
+ :hook (dired-after-readin . dired-k-no-revert)
+ :config
+ (defun +dired*interrupt-process (orig-fn &rest args)
+ "Fixes dired-k killing git processes too abruptly, leaving behind disruptive
+.git/index.lock files."
+ (cl-letf (((symbol-function #'kill-process)
+ (symbol-function #'interrupt-process)))
+ (apply orig-fn args)))
+ (advice-add #'dired-k--start-git-status :around #'+dired*interrupt-process)
+
+ (defun +dired*dired-k-highlight (orig-fn &rest args)
+ "Butt out if the requested directory is remote (i.e. through tramp)."
+ (unless (file-remote-p default-directory)
+ (apply orig-fn args)))
+ (advice-add #'dired-k--highlight :around #'+dired*dired-k-highlight))
+
+
+(def-package! ranger
+ :when (featurep! +ranger)
+ :after dired
+ :init
+ ;; set up image-dired to allow picture resize
+ (setq image-dired-dir (concat doom-cache-dir "image-dir"))
+ :config
+ (unless (file-directory-p image-dired-dir)
+ (make-directory image-dired-dir))
+
+ (set-popup-rule! "^\\*ranger" :ignore t)
+
+ (setq ranger-override-dired t
+ ranger-cleanup-on-disable t
+ ranger-omit-regexp "^\.DS_Store$"
+ ranger-excluded-extensions '("mkv" "iso" "mp4")
+ ranger-deer-show-details nil
+ ranger-max-preview-size 10
+ ranger-show-literal nil
+ dired-omit-verbose nil))
+
+
+(def-package! all-the-icons-dired
+ :when (featurep! +icons)
+ :hook (dired-mode . all-the-icons-dired-mode))
+
+
+(def-package! dired-x
+ :hook (dired-mode . dired-omit-mode)
+ :config
+ (setq dired-omit-verbose nil))
+
+
+;;
+;; Evil integration
+
+(map! :when (featurep! :feature evil +everywhere)
+ :after dired
+ :map dired-mode-map
+ :n "q" #'quit-window
+ :m "j" #'dired-next-line
+ :m "k" #'dired-previous-line
+ :n [mouse-2] #'dired-mouse-find-file-other-window
+ :n [follow-link] #'mouse-face
+ ;; Commands to mark or flag certain categories of files
+ :n "#" #'dired-flag-auto-save-files
+ :n "." #'dired-clean-directory
+ :n "~" #'dired-flag-backup-files
+ ;; Upper case keys (except !) for operating on the marked files
+ :n "A" #'dired-do-find-regexp
+ :n "C" #'dired-do-copy
+ :n "B" #'dired-do-byte-compile
+ :n "D" #'dired-do-delete
+ :n "gG" #'dired-do-chgrp ;; FIXME: This can probably live on a better binding.
+ :n "H" #'dired-do-hardlink
+ :n "L" #'dired-do-load
+ :n "M" #'dired-do-chmod
+ :n "O" #'dired-do-chown
+ :n "P" #'dired-do-print
+ :n "Q" #'dired-do-find-regexp-and-replace
+ :n "R" #'dired-do-rename
+ :n "S" #'dired-do-symlink
+ :n "T" #'dired-do-touch
+ :n "X" #'dired-do-shell-command
+ :n "Z" #'dired-do-compress
+ :n "c" #'dired-do-compress-to
+ :n "!" #'dired-do-shell-command
+ :n "&" #'dired-do-async-shell-command
+ ;; Comparison commands
+ :n "=" #'dired-diff
+ ;; Tree Dired commands
+ :n "M-C-?" #'dired-unmark-all-files
+ :n "M-C-d" #'dired-tree-down
+ :n "M-C-u" #'dired-tree-up
+ :n "M-C-n" #'dired-next-subdir
+ :n "M-C-p" #'dired-prev-subdir
+ ;; move to marked files
+ :n "M-{" #'dired-prev-marked-file
+ :n "M-}" #'dired-next-marked-file
+ ;; Make all regexp commands share a `%' prefix:
+ ;; We used to get to the submap via a symbol dired-regexp-prefix, but that
+ ;; seems to serve little purpose, and copy-keymap does a better job
+ ;; without it.
+ :n "%" nil
+ :n "%u" #'dired-upcase
+ :n "%l" #'dired-downcase
+ :n "%d" #'dired-flag-files-regexp
+ :n "%g" #'dired-mark-files-containing-regexp
+ :n "%m" #'dired-mark-files-regexp
+ :n "%r" #'dired-do-rename-regexp
+ :n "%C" #'dired-do-copy-regexp
+ :n "%H" #'dired-do-hardlink-regexp
+ :n "%R" #'dired-do-rename-regexp
+ :n "%S" #'dired-do-symlink-regexp
+ :n "%&" #'dired-flag-garbage-files
+ ;; mark
+ :n "*" nil
+ :n "**" #'dired-mark-executables
+ :n "*/" #'dired-mark-directories
+ :n "*@" #'dired-mark-symlinks
+ :n "*%" #'dired-mark-files-regexp
+ :n "*(" #'dired-mark-sexp
+ :n "*." #'dired-mark-extension
+ :n "*O" #'dired-mark-omitted
+ :n "*c" #'dired-change-marks
+ :n "*s" #'dired-mark-subdir-files
+ :n "*m" #'dired-mark
+ :n "*u" #'dired-unmark
+ :n "*?" #'dired-unmark-all-files
+ :n "*!" #'dired-unmark-all-marks
+ :n "U" #'dired-unmark-all-marks
+ :n "* " #'dired-unmark-backward
+ :n "* C-n" #'dired-next-marked-file
+ :n "* C-p" #'dired-prev-marked-file
+ :n "*t" #'dired-toggle-marks
+ ;; Lower keys for commands not operating on all the marked files
+ :n "a" #'dired-find-alternate-file
+ :n "d" #'dired-flag-file-deletion
+ :n "gf" #'dired-find-file
+ :n "C-m" #'dired-find-file
+ :n "gr" #'revert-buffer
+ :n "i" #'dired-toggle-read-only
+ :n "I" #'dired-maybe-insert-subdir
+ :n "J" #'dired-goto-file
+ :n "K" #'dired-do-kill-lines
+ :n "r" #'dired-do-redisplay
+ :n "m" #'dired-mark
+ :n "t" #'dired-toggle-marks
+ :n "u" #'dired-unmark ; also "*u"
+ :n "W" #'browse-url-of-dired-file
+ :n "x" #'dired-do-flagged-delete
+ :n "gy" #'dired-show-file-type ;; FIXME: This could probably go on a better key.
+ :n "Y" #'dired-copy-filename-as-kill
+ :n "+" #'dired-create-directory
+ ;; open
+ :n "" #'dired-find-file
+ :n "S-" #'dired-find-file-other-window
+ :n "M-" #'dired-display-file
+ :n "gO" #'dired-find-file-other-window
+ :n "go" #'dired-view-file
+ ;; sort
+ :n "o" #'dired-sort-toggle-or-edit
+ ;; moving
+ :m "gj" #'dired-next-dirline
+ :m "gk" #'dired-prev-dirline
+ :n "[" #'dired-prev-dirline
+ :n "]" #'dired-next-dirline
+ :n "<" #'dired-prev-dirline
+ :n ">" #'dired-next-dirline
+ :n "^" #'dired-up-directory
+ :n [?\S-\ ] #'dired-previous-line
+ :n [remap next-line] #'dired-next-line
+ :n [remap previous-line] #'dired-previous-line
+ ;; hiding
+ :n "g$" #'dired-hide-subdir ;; FIXME: This can probably live on a better binding.
+ :n "M-$" #'dired-hide-all
+ :n "(" #'dired-hide-details-mode
+ ;; isearch
+ :n "M-s a C-s" #'dired-do-isearch
+ :n "M-s a M-C-s" #'dired-do-isearch-regexp
+ :n "M-s f C-s" #'dired-isearch-filenames
+ :n "M-s f M-C-s" #'dired-isearch-filenames-regexp
+ ;; misc
+ :n [remap read-only-mode] #'dired-toggle-read-only
+ ;; `toggle-read-only' is an obsolete alias for `read-only-mode'
+ :n [remap toggle-read-only] #'dired-toggle-read-only
+ :n "g?" #'dired-summary
+ :n "" #'dired-unmark-backward
+ :n [remap undo] #'dired-undo
+ :n [remap advertised-undo] #'dired-undo
+ ;; thumbnail manipulation (image-dired)
+ :n "C-t d" #'image-dired-display-thumbs
+ :n "C-t t" #'image-dired-tag-files
+ :n "C-t r" #'image-dired-delete-tag
+ :n "C-t j" #'image-dired-jump-thumbnail-buffer
+ :n "C-t i" #'image-dired-dired-display-image
+ :n "C-t x" #'image-dired-dired-display-external
+ :n "C-t a" #'image-dired-display-thumbs-append
+ :n "C-t ." #'image-dired-display-thumb
+ :n "C-t c" #'image-dired-dired-comment-files
+ :n "C-t f" #'image-dired-mark-tagged-files
+ :n "C-t C-t" #'image-dired-dired-toggle-marked-thumbs
+ :n "C-t e" #'image-dired-dired-edit-comment-and-tags
+ ;; encryption and decryption (epa-dired)
+ :n ";d" #'epa-dired-do-decrypt
+ :n ";v" #'epa-dired-do-verify
+ :n ";s" #'epa-dired-do-sign
+ :n ";e" #'epa-dired-do-encrypt)
diff --git a/modules/emacs/dired/packages.el b/modules/emacs/dired/packages.el
new file mode 100644
index 000000000..9bc49ca9a
--- /dev/null
+++ b/modules/emacs/dired/packages.el
@@ -0,0 +1,8 @@
+;; -*- no-byte-compile: t; -*-
+;;; emacs/dired/packages.el
+
+(package! dired-k)
+(when (featurep! +ranger)
+ (package! ranger))
+(when (featurep! +icons)
+ (package! all-the-icons-dired))
diff --git a/modules/emacs/electric/autoload.el b/modules/emacs/electric/autoload.el
new file mode 100644
index 000000000..f4e33728b
--- /dev/null
+++ b/modules/emacs/electric/autoload.el
@@ -0,0 +1,26 @@
+;;; emacs/electric/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autodef
+(defun set-electric! (modes &rest plist)
+ "Declare that WORDS (list of strings) or CHARS (lists of chars) should trigger
+electric indentation.
+
+Enables `electric-indent-local-mode' in MODES.
+
+\(fn MODES &key WORDS CHARS)"
+ (declare (indent defun))
+ (dolist (mode (doom-enlist modes))
+ (let ((hook (intern (format "%s-hook" mode)))
+ (fn (intern (format "+electric|init-%s" mode))))
+ (cond ((null (car-safe plist))
+ (remove-hook hook fn)
+ (unintern fn nil))
+ ((fset fn
+ (lambda ()
+ (when (eq major-mode mode)
+ (setq-local electric-indent-inhibit nil)
+ (cl-destructuring-bind (&key chars words) plist
+ (electric-indent-local-mode +1)
+ (if chars (setq electric-indent-chars chars))
+ (if words (setq +electric-indent-words words))))))
+ (add-hook hook fn))))))
diff --git a/modules/emacs/electric/config.el b/modules/emacs/electric/config.el
new file mode 100644
index 000000000..2075d3c3a
--- /dev/null
+++ b/modules/emacs/electric/config.el
@@ -0,0 +1,19 @@
+;;; emacs/electric/config.el -*- lexical-binding: t; -*-
+
+;; Smarter, keyword-based electric-indent
+
+(defvar-local +electric-indent-words '()
+ "The list of electric words. Typing these will trigger reindentation of the
+current line.")
+
+;;
+(after! electric
+ (setq-default electric-indent-chars '(?\n ?\^?))
+
+ (defun +electric-indent|char (_c)
+ (when (and (eolp) +electric-indent-words)
+ (save-excursion
+ (backward-word)
+ (looking-at-p (concat "\\<" (regexp-opt +electric-indent-words))))))
+ (add-to-list 'electric-indent-functions #'+electric-indent|char nil #'eq))
+
diff --git a/modules/emacs/eshell/autoload/commands.el b/modules/emacs/eshell/autoload/commands.el
new file mode 100644
index 000000000..1661a6dad
--- /dev/null
+++ b/modules/emacs/eshell/autoload/commands.el
@@ -0,0 +1,18 @@
+;;; emacs/eshell/autoload/commands.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun eshell/cd-to-project ()
+ "Change to the project root of the current directory."
+ (eshell/cd (doom-project-root (eshell/pwd))))
+
+;;;###autoload
+(defun eshell/quit-and-close (&rest _)
+ "Quit the current eshell buffer and close the window it's in."
+ (setq-local +eshell-kill-window-on-exit t)
+ (throw 'eshell-terminal t))
+
+;;;###autoload
+(defun eshell/mkdir-and-cd (dir)
+ "Create a directory then cd into it."
+ (make-directory dir t)
+ (eshell/cd dir))
diff --git a/modules/emacs/eshell/autoload/eshell.el b/modules/emacs/eshell/autoload/eshell.el
new file mode 100644
index 000000000..884889801
--- /dev/null
+++ b/modules/emacs/eshell/autoload/eshell.el
@@ -0,0 +1,297 @@
+;;; emacs/eshell/autoload/eshell.el -*- lexical-binding: t; -*-
+
+(defvar eshell-buffer-name "*doom eshell*")
+
+(defvar +eshell-buffers (make-ring 25)
+ "List of open eshell buffers.")
+
+
+(defvar +eshell--last-buffer nil)
+
+
+;;
+;; Helpers
+
+(defun +eshell--add-buffer (buf)
+ (ring-remove+insert+extend +eshell-buffers buf 'grow))
+
+(defun +eshell--remove-buffer (buf)
+ (when-let* ((idx (ring-member +eshell-buffers buf)))
+ (ring-remove +eshell-buffers idx)
+ t))
+
+(defun +eshell--bury-buffer (&optional dedicated-p)
+ (unless (switch-to-prev-buffer nil 'bury)
+ (switch-to-buffer (doom-fallback-buffer)))
+ (when (eq major-mode 'eshell-mode)
+ (switch-to-buffer (doom-fallback-buffer)))
+ (when +eshell-enable-new-shell-on-split
+ (when-let* ((win (get-buffer-window (+eshell/open t))))
+ (set-window-dedicated-p win dedicated-p))))
+
+(defun +eshell--setup-window (window &optional flag)
+ (when (window-live-p window)
+ (set-window-parameter window 'no-other-window flag)
+ (set-window-parameter window 'visible flag)))
+
+(defun +eshell--unused-buffer (&optional new-p)
+ (or (unless new-p
+ (cl-loop for buf in (+eshell-buffers)
+ if (and (buffer-live-p buf)
+ (not (get-buffer-window buf t)))
+ return buf))
+ (generate-new-buffer eshell-buffer-name)))
+
+;;;###autoload
+(defun +eshell-last-buffer (&optional noerror)
+ "Return the last opened eshell buffer."
+ (let ((buffer (cl-find-if #'buffer-live-p (+eshell-buffers))))
+ (cond (buffer)
+ (noerror nil)
+ ((user-error "No live eshell buffers remaining")))))
+
+;;;###autoload
+(defun +eshell-buffers ()
+ "TODO"
+ (ring-elements +eshell-buffers))
+
+;;;###autoload
+(defun +eshell-run-command (command &optional buffer)
+ "TODO"
+ (let ((buffer
+ (or buffer
+ (if (eq major-mode 'eshell-mode)
+ (current-buffer)
+ (cl-find-if #'buffer-live-p (+eshell-buffers))))))
+ (unless buffer
+ (user-error "No living eshell buffers available"))
+ (unless (buffer-live-p buffer)
+ (user-error "Cannot operate on a dead buffer"))
+ (with-current-buffer buffer
+ (goto-char eshell-last-output-end)
+ (goto-char (line-end-position))
+ (insert command)
+ (eshell-send-input nil t))))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun +eshell/open (arg &optional command)
+ "Open eshell in the current buffer."
+ (interactive "P")
+ (when (eq major-mode 'eshell-mode)
+ (user-error "Already in an eshell buffer"))
+ (let* ((default-directory (or (if arg default-directory (doom-project-root))
+ default-directory))
+ (buf (+eshell--unused-buffer)))
+ (with-current-buffer (switch-to-buffer buf)
+ (if (eq major-mode 'eshell-mode)
+ (run-hooks 'eshell-mode-hook)
+ (eshell-mode))
+ (if command (+eshell-run-command command buf)))
+ buf))
+
+;;;###autoload
+(defun +eshell/open-popup (arg &optional command)
+ "Open eshell in a popup window."
+ (interactive "P")
+ (let* ((default-directory (or (if arg default-directory (doom-project-root))
+ default-directory))
+ (buf (+eshell--unused-buffer)))
+ (with-current-buffer (pop-to-buffer buf)
+ (if (eq major-mode 'eshell-mode)
+ (run-hooks 'eshell-mode-hook)
+ (eshell-mode))
+ (if command (+eshell-run-command command buf)))
+ buf))
+
+;;;###autoload
+(defun +eshell/open-fullscreen (arg &optional command)
+ "Open eshell in a separate workspace. Requires the (:feature workspaces)
+module to be loaded."
+ (interactive "P")
+ (let ((default-directory (or (if arg default-directory (doom-project-root))
+ default-directory))
+ (buf (+eshell--unused-buffer 'new)))
+ (set-frame-parameter nil 'saved-wconf (current-window-configuration))
+ (delete-other-windows)
+ (with-current-buffer (switch-to-buffer buf)
+ (eshell-mode)
+ (if command (+eshell-run-command command buf)))
+ buf))
+
+
+;;
+;; Keybinds
+
+;;;###autoload
+(defun +eshell/search-history ()
+ "Search the eshell command history with helm, ivy or `eshell-list-history'."
+ (interactive)
+ (cond ((featurep! :completion ivy)
+ (require 'em-hist)
+ (let* ((ivy-completion-beg (eshell-bol))
+ (ivy-completion-end (point-at-eol))
+ (input (buffer-substring-no-properties
+ ivy-completion-beg
+ ivy-completion-end)))
+ ;; Better than `counsel-esh-history' because that doesn't
+ ;; pre-populate the initial input or selection.
+ (ivy-read "Command: "
+ (delete-dups
+ (when (> (ring-size eshell-history-ring) 0)
+ (ring-elements eshell-history-ring)))
+ :initial-input input
+ :action #'ivy-completion-in-region-action)))
+ ((featurep! :completion helm)
+ (helm-eshell-history))
+ ((eshell-list-history))))
+
+;;;###autoload
+(defun +eshell/pcomplete ()
+ "Use pcomplete with completion-in-region backend instead of popup window at
+bottom. This ties pcomplete into ivy or helm, if they are enabled."
+ (interactive)
+ (require 'pcomplete)
+ (ignore-errors (pcomplete-std-complete)))
+
+;;;###autoload
+(defun +eshell/quit-or-delete-char (arg)
+ "Delete a character (ahead of the cursor) or quit eshell if there's nothing to
+delete."
+ (interactive "p")
+ (if (and (eolp) (looking-back eshell-prompt-regexp nil))
+ (eshell-life-is-too-much)
+ (delete-char arg)))
+
+;;;###autoload
+(defun +eshell/split-below ()
+ "Create a new eshell window below the current one."
+ (interactive)
+ (let ((ignore-window-parameters t)
+ (dedicated-p (window-dedicated-p))
+ (+eshell-enable-new-shell-on-split
+ (or +eshell-enable-new-shell-on-split (frame-parameter nil 'saved-wconf))))
+ (select-window (split-window-vertically))
+ (+eshell--bury-buffer dedicated-p)))
+
+;;;###autoload
+(defun +eshell/split-right ()
+ "Create a new eshell window to the right of the current one."
+ (interactive)
+ (let* ((ignore-window-parameters t)
+ (dedicated-p (window-dedicated-p))
+ (+eshell-enable-new-shell-on-split
+ (or +eshell-enable-new-shell-on-split (frame-parameter nil 'saved-wconf))))
+ (select-window (split-window-horizontally))
+ (+eshell--bury-buffer dedicated-p)))
+
+;;;###autoload
+(defun +eshell/switch-to-next ()
+ "Switch to the next eshell buffer."
+ (interactive)
+ (when (ring-empty-p +eshell-buffers)
+ (user-error "No eshell buffers are available"))
+ (switch-to-buffer (ring-next +eshell-buffers (current-buffer))))
+
+;;;###autoload
+(defun +eshell/switch-to-previous ()
+ "Switch to the previous eshell buffer."
+ (interactive)
+ (when (ring-empty-p +eshell-buffers)
+ (user-error "No eshell buffers are available"))
+ (switch-to-buffer (ring-previous +eshell-buffers (current-buffer))))
+
+;;;###autoload
+(defun +eshell/switch-to-last ()
+ "Switch to the last eshell buffer that was open (and is still alive)."
+ (interactive)
+ (unless (buffer-live-p +eshell--last-buffer)
+ (setq +eshell--last-buffer nil)
+ (user-error "No last eshell buffer to jump to"))
+ (switch-to-buffer +eshell--last-buffer))
+
+;;;###autoload
+(defun +eshell/switch-to (buffer)
+ "Interactively switch to another eshell buffer."
+ (interactive
+ (let ((buffers (doom-buffers-in-mode
+ 'eshell-mode (delq (current-buffer) (+eshell-buffers)))))
+ (if (not buffers)
+ (user-error "No eshell buffers are available")
+ (list
+ (completing-read "Eshell buffers"
+ (mapcar #'buffer-name buffers)
+ #'get-buffer
+ 'require-match
+ nil nil
+ (when (eq major-mode 'eshell-mode)
+ (buffer-name (current-buffer))))))))
+ (if-let* ((window (get-buffer-window buffer)))
+ (select-window window)
+ (switch-to-buffer buffer)))
+
+;;;###autoload
+(defun +eshell/kill-and-close ()
+ "Kill the current eshell buffer and close its window."
+ (interactive)
+ (unless (eq major-mode 'eshell-mode)
+ (user-error "Not in an eshell buffer"))
+ (let ((+eshell-kill-window-on-exit t))
+ (kill-this-buffer)))
+
+
+;;
+;; Hooks
+
+;;;###autoload
+(defun +eshell|init ()
+ "Initialize and track this eshell buffer in `+eshell-buffers'."
+ (let ((current-buffer (current-buffer)))
+ (dolist (buf (+eshell-buffers))
+ (unless (buffer-live-p buf)
+ (+eshell--remove-buffer buf)))
+ (+eshell--setup-window (get-buffer-window current-buffer))
+ (+eshell--add-buffer current-buffer)
+ (setq +eshell--last-buffer current-buffer)))
+
+;;;###autoload
+(defun +eshell|cleanup ()
+ "Close window (or workspace) on quit."
+ (let ((buf (current-buffer)))
+ (when (+eshell--remove-buffer buf)
+ (when-let* ((win (get-buffer-window buf)))
+ (+eshell--setup-window win nil)
+ (cond ((and (one-window-p t)
+ (window-configuration-p (frame-parameter nil 'saved-wconf)))
+ (set-window-configuration (frame-parameter nil 'saved-wconf))
+ (set-frame-parameter win 'saved-wconf nil))
+ ((one-window-p)
+ (let ((prev (save-window-excursion (previous-buffer))))
+ (unless (and prev (doom-real-buffer-p prev))
+ (switch-to-buffer (doom-fallback-buffer)))))
+ ((or (window-dedicated-p win)
+ +eshell-kill-window-on-exit)
+ (let ((ignore-window-parameters t)
+ (popup-p (window-dedicated-p win)))
+ (delete-window win)
+ (when popup-p
+ (cl-loop for win in (window-list)
+ for buf = (window-buffer win)
+ for mode = (buffer-local-value 'major-mode buf)
+ if (eq mode 'eshell-mode)
+ return (select-window win))))))))))
+
+;;;###autoload
+(defun +eshell|switch-workspace (type)
+ (when (eq type 'frame)
+ (setq +eshell-buffers
+ (or (persp-parameter 'eshell-buffers)
+ (make-ring 25)))))
+
+;;;###autoload
+(defun +eshell|save-workspace (_workspace target)
+ (when (framep target)
+ (set-persp-parameter 'eshell-buffers +eshell-buffers)))
diff --git a/modules/emacs/eshell/autoload/evil.el b/modules/emacs/eshell/autoload/evil.el
new file mode 100644
index 000000000..a6fd3c911
--- /dev/null
+++ b/modules/emacs/eshell/autoload/evil.el
@@ -0,0 +1,76 @@
+;;; emacs/eshell/autoload/evil.el -*- lexical-binding: t; -*-
+;;;###if (featurep! :feature evil)
+
+;;;###autoload
+(defun +eshell|init-evil ()
+ "Replace `evil-collection-eshell-next-prompt-on-insert' with
+`+eshell|goto-prompt-on-insert', which ensures the point is on the prompt when
+changing to insert mode."
+ (dolist (hook '(evil-replace-state-entry-hook evil-insert-state-entry-hook))
+ (remove-hook hook 'evil-collection-eshell-next-prompt-on-insert t)
+ (add-hook hook '+eshell|goto-prompt-on-insert nil t)))
+
+;;;###autoload (autoload '+eshell:run "emacs/eshell/autoload/evil" nil t)
+(evil-define-command +eshell:run (command bang)
+ "TODO"
+ (interactive "")
+ (let ((buffer (+eshell-last-buffer))
+ (command (+evil*resolve-vim-path command)))
+ (cond (buffer
+ (select-window (get-buffer-window buffer))
+ (+eshell-run-command command buffer))
+ (bang (+eshell/open nil command))
+ ((+eshell/open-popup nil command)))))
+
+;;;###autoload
+(defun +eshell|goto-prompt-on-insert ()
+ "Move cursor to the prompt when switching to insert mode (if point isn't
+already there)."
+ (when (< (point) eshell-last-output-end)
+ (goto-char
+ (if (memq this-command '(evil-append evil-append-line))
+ (point-max)
+ eshell-last-output-end))))
+
+;;;###autoload
+(defun +eshell/goto-end-of-prompt ()
+ "Move cursor to the prompt when switching to insert mode (if point isn't
+already there)."
+ (interactive)
+ (goto-char (point-max))
+ (evil-append 1))
+
+;;;###autoload (autoload '+eshell/evil-change "emacs/eshell/autoload/evil" nil t)
+(evil-define-operator +eshell/evil-change (beg end type register yank-handler delete-func)
+ "Like `evil-change' but will not delete/copy the prompt."
+ (interactive "")
+ (save-restriction
+ (narrow-to-region eshell-last-output-end (point-max))
+ (evil-change (max beg (point-min))
+ (if (eq type 'line) (point-max) (min (or end (point-max)) (point-max)))
+ type register yank-handler delete-func)))
+
+;;;###autoload (autoload '+eshell/evil-change-line "emacs/eshell/autoload/evil" nil t)
+(evil-define-operator +eshell/evil-change-line (beg end type register yank-handler)
+ "Change to end of line."
+ :motion evil-end-of-line
+ (interactive "")
+ (+eshell/evil-change beg end type register yank-handler #'evil-delete-line))
+
+;;;###autoload (autoload '+eshell/evil-delete "emacs/eshell/autoload/evil" nil t)
+(evil-define-operator +eshell/evil-delete (beg end type register yank-handler)
+ "Like `evil-delete' but will not delete/copy the prompt."
+ (interactive "")
+ (save-restriction
+ (narrow-to-region eshell-last-output-end (point-max))
+ (evil-delete (if beg (max beg (point-min)) (point-min))
+ (if (eq type 'line) (point-max) (min (or end (point-max)) (point-max)))
+ type register yank-handler)))
+
+;;;###autoload (autoload '+eshell/evil-delete-line "emacs/eshell/autoload/evil" nil t)
+(evil-define-operator +eshell/evil-delete-line (_beg end type register yank-handler)
+ "Change to end of line."
+ :motion nil
+ :keep-visual t
+ (interactive "")
+ (+eshell/evil-delete (point) end type register yank-handler))
diff --git a/modules/emacs/eshell/autoload/prompts.el b/modules/emacs/eshell/autoload/prompts.el
new file mode 100644
index 000000000..1507350ac
--- /dev/null
+++ b/modules/emacs/eshell/autoload/prompts.el
@@ -0,0 +1,34 @@
+;;; emacs/eshell/autoload/prompts.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defface +eshell-prompt-pwd '((t :inherit font-lock-constant-face))
+ "TODO"
+ :group 'eshell)
+
+;;;###autoload
+(defface +eshell-prompt-git-branch '((t :inherit font-lock-builtin-face))
+ "TODO"
+ :group 'eshell)
+
+
+(defun +eshell--current-git-branch ()
+ (let ((branch (car (cl-loop for match in (split-string (shell-command-to-string "git branch") "\n")
+ if (string-match-p "^\*" match)
+ collect match))))
+ (if (not (eq branch nil))
+ (format " [%s]" (substring branch 2))
+ "")))
+
+;;;###autoload
+(defun +eshell-default-prompt ()
+ "Generate the prompt string for eshell. Use for `eshell-prompt-function'."
+ (concat (if (bobp) "" "\n")
+ (let ((pwd (eshell/pwd)))
+ (propertize (if (equal pwd "~")
+ pwd
+ (abbreviate-file-name (shrink-path-file pwd)))
+ 'face '+eshell-prompt-pwd))
+ (propertize (+eshell--current-git-branch)
+ 'face '+eshell-prompt-git-branch)
+ (propertize " λ" 'face (if (zerop eshell-last-command-status) 'success 'error))
+ " "))
diff --git a/modules/emacs/eshell/autoload/settings.el b/modules/emacs/eshell/autoload/settings.el
new file mode 100644
index 000000000..276f44b02
--- /dev/null
+++ b/modules/emacs/eshell/autoload/settings.el
@@ -0,0 +1,20 @@
+;;; emacs/eshell/autoload/settings.el -*- lexical-binding: t; -*-
+
+;;;###autodef
+(defun set-eshell-alias! (&rest aliases)
+ "Define aliases for eshell."
+ (or (cl-evenp (length aliases))
+ (signal 'wrong-number-of-arguments (list 'even (length aliases))))
+ (after! eshell
+ (while aliases
+ (let ((alias (pop aliases))
+ (command (pop aliases)))
+ (if-let* ((oldval (assoc alias +eshell-aliases)))
+ (setcdr oldval (list command))
+ (push (list alias command) +eshell-aliases))))
+ (when (boundp 'eshell-command-aliases-list)
+ (if +eshell--default-aliases
+ (setq eshell-command-aliases-list
+ (append +eshell--default-aliases
+ +eshell-aliases))
+ (setq eshell-command-aliases-list +eshell-aliases)))))
diff --git a/modules/emacs/eshell/config.el b/modules/emacs/eshell/config.el
new file mode 100644
index 000000000..42ed6a6ea
--- /dev/null
+++ b/modules/emacs/eshell/config.el
@@ -0,0 +1,165 @@
+;;; emacs/eshell/config.el -*- lexical-binding: t; -*-
+
+;; see:
+;; + `+eshell/open': open in current buffer
+;; + `+eshell/open-popup': open in a popup
+;; + `+eshell/open-fullscreen': open eshell fullscreen (will restore window
+;; config when quitting the last eshell buffer)
+
+(defvar +eshell-config-dir
+ (expand-file-name "eshell/" doom-private-dir)
+ "Where to store eshell configuration files, as opposed to
+`eshell-directory-name', which is where Doom will store temporary/data files.")
+
+(defvar +eshell-enable-new-shell-on-split t
+ "If non-nil, spawn a new eshell session after splitting from an eshell
+buffer.")
+
+(defvar +eshell-kill-window-on-exit nil
+ "If non-nil, eshell will close windows along with its eshell buffers.")
+
+(defvar +eshell-aliases
+ '(("q" "exit") ; built-in
+ ("f" "find-file $1")
+ ("bd" "eshell-up $1") ; `eshell-up'
+ ("rg" "rg --color=always $*")
+ ("ag" "ag --color=always $*")
+ ("l" "ls -lh")
+ ("ll" "ls -lah")
+ ("clear" "clear-scrollback")) ; more sensible than default
+ "An alist of default eshell aliases, meant to emulate useful shell utilities,
+like fasd and bd. Note that you may overwrite these in your
+`eshell-aliases-file'. This is here to provide an alternative, elisp-centric way
+to define your aliases.
+
+You should use `det-eshell-alias!' to change this.")
+
+;;
+(defvar eshell-directory-name (concat doom-etc-dir "eshell"))
+
+;; These files are exceptions, because they may contain configuration
+(defvar eshell-aliases-file (concat +eshell-config-dir "alias"))
+(defvar eshell-rc-script (concat +eshell-config-dir "profile"))
+(defvar eshell-login-script (concat +eshell-config-dir "login"))
+
+
+(defvar +eshell--default-aliases nil)
+
+
+;;
+;; Packages
+
+(after! eshell ; built-in
+ (setq eshell-banner-message
+ '(format "%s %s\n"
+ (propertize (format " %s " (string-trim (buffer-name)))
+ 'face 'mode-line-highlight)
+ (propertize (current-time-string)
+ 'face 'font-lock-keyword-face))
+ eshell-scroll-to-bottom-on-input 'all
+ eshell-scroll-to-bottom-on-output 'all
+ eshell-buffer-shorthand t
+ eshell-kill-processes-on-exit t
+ eshell-hist-ignoredups t
+ ;; don't record command in history if prefixed with whitespace
+ eshell-input-filter #'eshell-input-filter-initial-space
+ ;; em-prompt
+ eshell-prompt-regexp "^.* λ "
+ eshell-prompt-function #'+eshell-default-prompt
+ ;; em-glob
+ eshell-glob-case-insensitive t
+ eshell-error-if-no-glob t)
+
+ ;; Consider eshell buffers real
+ (add-hook 'eshell-mode-hook #'doom|mark-buffer-as-real)
+
+ ;; Keep track of open eshell buffers
+ (add-hook 'eshell-mode-hook #'+eshell|init)
+ (add-hook 'eshell-exit-hook #'+eshell|cleanup)
+
+ ;; Enable autopairing in eshell
+ (add-hook 'eshell-mode-hook #'smartparens-mode)
+
+ ;; Persp-mode/workspaces integration
+ (when (featurep! :feature workspaces)
+ (add-hook 'persp-activated-functions #'+eshell|switch-workspace)
+ (add-hook 'persp-before-switch-functions #'+eshell|save-workspace))
+
+ ;; UI enhancements
+ (defun +eshell|remove-fringes ()
+ (set-window-fringes nil 0 0)
+ (set-window-margins nil 1 nil))
+ (add-hook 'eshell-mode-hook #'+eshell|remove-fringes)
+
+ (defun +eshell|enable-text-wrapping ()
+ (visual-line-mode +1)
+ (set-display-table-slot standard-display-table 0 ?\ ))
+ (add-hook 'eshell-mode-hook #'+eshell|enable-text-wrapping)
+
+ (add-hook 'eshell-mode-hook #'hide-mode-line-mode)
+
+ ;; Don't auto-write our aliases! Let us manage our own `eshell-aliases-file'
+ ;; or configure `+eshell-aliases' via elisp.
+ (advice-add #'eshell-write-aliases-list :override #'ignore)
+
+ ;; Visual commands require a proper terminal. Eshell can't handle that, so
+ ;; it delegates these commands to a term buffer.
+ (after! em-term
+ (dolist (cmd '("tmux" "htop" "vim" "nvim" "ncmpcpp"))
+ (add-to-list 'eshell-visual-commands cmd)))
+
+ (defun +eshell|init-aliases ()
+ (setq +eshell--default-aliases eshell-command-aliases-list
+ eshell-command-aliases-list
+ (append eshell-command-aliases-list
+ +eshell-aliases)))
+ (add-hook 'eshell-alias-load-hook #'+eshell|init-aliases)
+
+ (when (featurep! :feature evil +everywhere)
+ (add-hook 'eshell-mode-hook #'+eshell|init-evil))
+
+ (defun +eshell|init-keymap ()
+ "Setup eshell keybindings. This must be done in a hook because eshell-mode
+redefines its keys every time `eshell-mode' is enabled."
+ (map! :map eshell-mode-map
+ :n [return] #'+eshell/goto-end-of-prompt
+ :n "c" #'+eshell/evil-change
+ :n "C" #'+eshell/evil-change-line
+ :n "d" #'+eshell/evil-delete
+ :n "D" #'+eshell/evil-delete-line
+ :i [tab] #'+eshell/pcomplete
+ :i "C-j" #'evil-window-down
+ :i "C-k" #'evil-window-up
+ :i "C-h" #'evil-window-left
+ :i "C-l" #'evil-window-right
+ :i "C-d" #'+eshell/quit-or-delete-char
+ :i "C-p" #'eshell-previous-input
+ :i "C-n" #'eshell-next-input
+ "C-s" #'+eshell/search-history
+ "C-c s" #'+eshell/split-below
+ "C-c v" #'+eshell/split-right
+ "C-c x" #'+eshell/kill-and-close
+ [remap split-window-below] #'+eshell/split-below
+ [remap split-window-right] #'+eshell/split-right
+ [remap doom/backward-to-bol-or-indent] #'eshell-bol
+ [remap doom/backward-kill-to-bol-and-indent] #'eshell-kill-input
+ [remap evil-window-split] #'+eshell/split-below
+ [remap evil-window-vsplit] #'+eshell/split-right))
+ (add-hook 'eshell-first-time-mode-hook #'+eshell|init-keymap))
+
+
+(def-package! eshell-up
+ :commands (eshell-up eshell-up-peek))
+
+
+(def-package! shrink-path
+ :commands shrink-path-file)
+
+
+(def-package! eshell-z
+ :after eshell
+ :config
+ ;; Use zsh's db if it exists, otherwise, store it in `doom-cache-dir'
+ (unless (file-exists-p eshell-z-freq-dir-hash-table-file-name)
+ (setq eshell-z-freq-dir-hash-table-file-name
+ (expand-file-name "z" eshell-directory-name))))
diff --git a/modules/emacs/eshell/packages.el b/modules/emacs/eshell/packages.el
new file mode 100644
index 000000000..e7f060578
--- /dev/null
+++ b/modules/emacs/eshell/packages.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; emacs/eshell/packages.el
+
+(package! eshell-up)
+(package! eshell-z)
+(package! shrink-path)
diff --git a/modules/emacs/imenu/config.el b/modules/emacs/imenu/config.el
new file mode 100644
index 000000000..88fed1090
--- /dev/null
+++ b/modules/emacs/imenu/config.el
@@ -0,0 +1,11 @@
+;;; emacs/imenu/config.el -*- lexical-binding: t; -*-
+
+;; `imenu-anywhere'
+(setq imenu-anywhere-delimiter ": ")
+
+
+(after! imenu-list
+ (setq imenu-list-idle-update-delay 0.5)
+
+ (set-popup-rule! "^\\*Ilist"
+ :side 'right :size 35 :quit nil :select nil :ttl 0))
diff --git a/modules/tools/imenu/packages.el b/modules/emacs/imenu/packages.el
similarity index 74%
rename from modules/tools/imenu/packages.el
rename to modules/emacs/imenu/packages.el
index 0328b7016..60b3cfc93 100644
--- a/modules/tools/imenu/packages.el
+++ b/modules/emacs/imenu/packages.el
@@ -1,5 +1,5 @@
;; -*- no-byte-compile: t; -*-
-;;; tools/imenu/packages.el
+;;; emacs/imenu/packages.el
(package! imenu-anywhere)
(package! imenu-list)
diff --git a/modules/emacs/term/autoload.el b/modules/emacs/term/autoload.el
new file mode 100644
index 000000000..b30d29183
--- /dev/null
+++ b/modules/emacs/term/autoload.el
@@ -0,0 +1,33 @@
+;;; emacs/term/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +term/open (arg)
+ "Open a terminal buffer in the current window. If ARG (universal argument) is
+non-nil, cd into the current project's root."
+ (interactive "P")
+ (let ((default-directory
+ (if arg
+ (or (doom-project-root) default-directory)
+ default-directory)))
+ ;; Doom's switch-buffer hooks prevent themselves from triggering when
+ ;; switching from buffer A back to A. Because `multi-term' uses `set-buffer'
+ ;; before `switch-to-buffer', the hooks don't trigger, so we use this
+ ;; roundabout way to trigger them properly.
+ (switch-to-buffer (save-window-excursion (multi-term)))))
+
+;;;###autoload
+(defun +term/open-popup (arg)
+ "Open a terminal popup window. If ARG (universal argument) is
+non-nil, cd into the current project's root."
+ (interactive "P")
+ (let ((default-directory
+ (if arg
+ (or (doom-project-root) default-directory)
+ default-directory)))
+ (pop-to-buffer (save-window-excursion (multi-term)))))
+
+;;;###autoload
+(defun +term/open-popup-in-project ()
+ "Open a terminal popup window in the root of the current project."
+ (interactive)
+ (+term/open-popup t))
diff --git a/modules/emacs/term/config.el b/modules/emacs/term/config.el
new file mode 100644
index 000000000..cf7b9a2b2
--- /dev/null
+++ b/modules/emacs/term/config.el
@@ -0,0 +1,8 @@
+;;; emacs/term/config.el -*- lexical-binding: t; -*-
+
+;; `multi-term'
+(setq multi-term-dedicated-window-height 20
+ multi-term-switch-after-close 'PREVIOUS)
+
+;; `term' (built-in)
+(add-hook 'term-mode-hook #'doom|mark-buffer-as-real)
diff --git a/modules/tools/term/packages.el b/modules/emacs/term/packages.el
similarity index 66%
rename from modules/tools/term/packages.el
rename to modules/emacs/term/packages.el
index 87724c0ef..963aff83e 100644
--- a/modules/tools/term/packages.el
+++ b/modules/emacs/term/packages.el
@@ -1,4 +1,4 @@
;; -*- no-byte-compile: t; -*-
-;;; tools/term/packages.el
+;;; emacs/term/packages.el
(package! multi-term)
diff --git a/modules/emacs/vc/autoload/evil.el b/modules/emacs/vc/autoload/evil.el
new file mode 100644
index 000000000..7803f94c9
--- /dev/null
+++ b/modules/emacs/vc/autoload/evil.el
@@ -0,0 +1,8 @@
+;;; emacs/vc/autoload/evil.el -*- lexical-binding: t; -*-
+;;;###if (featurep! :feature evil)
+
+;;;###autoload (autoload '+vc:git-browse "emacs/vc/autoload/evil" nil t)
+(evil-define-command +vc:git-browse (bang)
+ "Ex interface to `+vc/git-browse-region-or-line'."
+ (interactive "")
+ (+vc/git-browse-region-or-line bang))
diff --git a/modules/emacs/vc/autoload/vc.el b/modules/emacs/vc/autoload/vc.el
new file mode 100644
index 000000000..e2efb9bdc
--- /dev/null
+++ b/modules/emacs/vc/autoload/vc.el
@@ -0,0 +1,79 @@
+;;; emacs/vc/autoload/vc.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +vc-git-root-url ()
+ "Return the root git repo URL for the current file."
+ (require 'git-link)
+ (let* ((remote (git-link--select-remote))
+ (remote-url (git-link--remote-url remote))
+ (remote-info (if remote-url (git-link--parse-remote remote-url))))
+ (if remote-info
+ (format "https://%s/%s" (car remote-info) (cadr remote-info))
+ (error "Remote `%s' is unknown or contains an unsupported URL" remote))))
+
+(defvar git-link-open-in-browser)
+;;;###autoload
+(defun +vc/git-browse-region-or-line (&optional arg)
+ "Open the website for the current version controlled file. Fallback to
+repository root."
+ (interactive "P")
+ (require 'git-link)
+ (cl-destructuring-bind (beg end)
+ (if buffer-file-name (git-link--get-region))
+ (let ((git-link-open-in-browser (not arg)))
+ (git-link (git-link--select-remote) beg end))))
+
+;;;###autoload
+(defun +vc*update-header-line (revision)
+ "Show revision details in the header-line, instead of the minibuffer.
+
+Sometimes I forget `git-timemachine' is enabled in a buffer. Putting revision
+info in the `header-line-format' is a good indication."
+ (let* ((date-relative (nth 3 revision))
+ (date-full (nth 4 revision))
+ (author (if git-timemachine-show-author (concat (nth 6 revision) ": ") ""))
+ (sha-or-subject (if (eq git-timemachine-minibuffer-detail 'commit) (car revision) (nth 5 revision))))
+ (setq header-line-format
+ (format "%s%s [%s (%s)]"
+ (propertize author 'face 'git-timemachine-minibuffer-author-face)
+ (propertize sha-or-subject 'face 'git-timemachine-minibuffer-detail-face)
+ date-full date-relative))))
+
+;;;###autoload (autoload '+vc-smerge-hydra/body "emacs/vc/autoload" nil t)
+(defhydra +vc-smerge-hydra (:hint nil
+ :pre (if (not smerge-mode) (smerge-mode 1))
+ ;; Disable `smerge-mode' when quitting hydra if
+ ;; no merge conflicts remain.
+ :post (smerge-auto-leave))
+ "
+ [smerge]
+ Movement Keep Diff Other
+ ╭─────────────────────────────────────────────────────────╯
+ ^_g_^ [_b_] base [_<_] upper/base [_C_] Combine
+ ^_C-k_^ [_u_] upper [_=_] upper/lower [_r_] resolve
+ ^_k_ ↑^ [_l_] lower [_>_] base/lower [_R_] remove
+ ^_j_ ↓^ [_a_] all [_H_] hightlight
+ ^_C-j_^ [_RET_] current [_E_] ediff ╭──────────
+ ^_G_^ │ [_q_] quit
+"
+ ("g" (progn (goto-char (point-min)) (smerge-next)))
+ ("G" (progn (goto-char (point-max)) (smerge-prev)))
+ ("C-j" smerge-next)
+ ("C-k" smerge-prev)
+ ("j" next-line)
+ ("k" previous-line)
+ ("b" smerge-keep-base)
+ ("u" smerge-keep-upper)
+ ("l" smerge-keep-lower)
+ ("a" smerge-keep-all)
+ ("RET" smerge-keep-current)
+ ("\C-m" smerge-keep-current)
+ ("<" smerge-diff-base-upper)
+ ("=" smerge-diff-upper-lower)
+ (">" smerge-diff-base-lower)
+ ("H" smerge-refine)
+ ("E" smerge-ediff)
+ ("C" smerge-combine-with-next)
+ ("r" smerge-resolve)
+ ("R" smerge-kill-current)
+ ("q" nil :color blue))
diff --git a/modules/emacs/vc/config.el b/modules/emacs/vc/config.el
new file mode 100644
index 000000000..38a590e84
--- /dev/null
+++ b/modules/emacs/vc/config.el
@@ -0,0 +1,63 @@
+;;; emacs/vc/config.el -*- lexical-binding: t; -*-
+
+;; `git-timemachine'
+(after! git-timemachine
+ ;; Sometimes I forget `git-timemachine' is enabled in a buffer, so instead of
+ ;; showing revision details in the minibuffer, show them in
+ ;; `header-line-format', which has better visibility.
+ (setq git-timemachine-show-minibuffer-details t)
+ (advice-add #'git-timemachine--show-minibuffer-details :override #'+vc*update-header-line)
+
+ (after! evil
+ ;; rehash evil keybindings so they are recognized
+ (add-hook 'git-timemachine-mode-hook #'evil-normalize-keymaps))
+
+ (when (featurep! :tools magit)
+ (add-transient-hook! #'git-timemachine-blame (require 'magit-blame))))
+
+
+;; `git-commit-mode'
+(after! git-commit-mode
+ (set-yas-minor-mode! 'git-commit-mode))
+
+(defun +vc|enforce-git-commit-conventions ()
+ "See https://chris.beams.io/posts/git-commit/"
+ (setq fill-column 72
+ git-commit-summary-max-length 50
+ git-commit-style-convention-checks '(overlong-summary-line non-empty-second-line)))
+(add-hook 'git-commit-mode-hook #'+vc|enforce-git-commit-conventions)
+
+(defun +vc|start-in-insert-state-maybe ()
+ "Start git-commit-mode in insert state if in a blank commit message,
+otherwise in default state."
+ (when (and (bound-and-true-p evil-mode)
+ (bobp) (eolp))
+ (evil-insert-state)))
+(add-hook 'git-commit-setup-hook #'+vc|start-in-insert-state-maybe)
+
+
+;;
+;; `vc' (built-in)
+
+;; `vc-hooks'
+(setq vc-make-backup-files nil)
+
+;; `vc-annotate'
+(after! vc-annotate
+ (set-popup-rules!
+ '(("^\\vc-d" :select nil) ; *vc-diff*
+ ("^\\vc-c" :select t))) ; *vc-change-log*
+ (set-evil-initial-state!
+ '(vc-annotate-mode vc-git-log-view-mode)
+ 'normal))
+
+;; `smerge-mode'
+(after! smerge-mode
+ (unless EMACS26+
+ (with-no-warnings
+ (defalias #'smerge-keep-upper #'smerge-keep-mine)
+ (defalias #'smerge-keep-lower #'smerge-keep-other)
+ (defalias #'smerge-diff-base-upper #'smerge-diff-base-mine)
+ (defalias #'smerge-diff-upper-lower #'smerge-diff-mine-other)
+ (defalias #'smerge-diff-base-lower #'smerge-diff-base-other))))
+
diff --git a/modules/emacs/vc/packages.el b/modules/emacs/vc/packages.el
new file mode 100644
index 000000000..3506fa5a0
--- /dev/null
+++ b/modules/emacs/vc/packages.el
@@ -0,0 +1,7 @@
+;; -*- no-byte-compile: t; -*-
+;;; emacs/vc/packages.el
+
+(package! git-link)
+(package! git-timemachine)
+(package! gitconfig-mode)
+(package! gitignore-mode)
diff --git a/modules/feature/debugger/config.el b/modules/feature/debugger/config.el
index 384975d2f..7cfb631a0 100644
--- a/modules/feature/debugger/config.el
+++ b/modules/feature/debugger/config.el
@@ -3,9 +3,7 @@
(def-package! realgud
:commands (realgud:gdb realgud:trepanjs realgud:bashdb realgud:zshdb)
:config
- (set! :popup
- '("^\\*\\(g\\|zsh\\|bash\\)db.*?\\*$" :size 20 :regexp t)
- '("^\\*trepanjs.*?\\*$" :size 20 :regexp t))
+ (set-popup-rule! "^\\*\\(?:trepanjs:\\(?:g\\|zsh\\|bash\\)db\\)" :size 20)
;; TODO Temporary Ex commands for the debugger
;; (def-tmp-excmd! doom:def-debug-on doom:def-debug-off
diff --git a/modules/feature/eval/README.org b/modules/feature/eval/README.org
index 851ea6a48..f52e6c1a3 100644
--- a/modules/feature/eval/README.org
+++ b/modules/feature/eval/README.org
@@ -1,67 +1,91 @@
-#+TITLE: :feature eval
+#+TITLE: feature/eval
+#+DATE: February 13, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
-This modules adds support for evaluating code from inside Emacs, including
-REPLs.
+* Table of Contents :TOC_3:noexport:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+ - [[Hacks][Hacks]]
+- [[Prerequisites][Prerequisites]]
+- [[Features][Features]]
+ - [[Inline Code Evaluation][Inline Code Evaluation]]
+ - [[REPLs][REPLs]]
+- [[Configuration][Configuration]]
+ - [[Register a REPL for a major-mode][Register a REPL for a major-mode]]
+ - [[Change how code is evaluated in a major mode][Change how code is evaluated in a major mode]]
+- [[Troubleshooting][Troubleshooting]]
-* Table of Contents :TOC:
-- [[#install][Install]]
-- [[#usage][Usage]]
- - [[#repls][REPLs]]
- - [[#code-evaluation][*Code Evaluation*]]
-- [[#configuration][Configuration]]
- - [[#repls-1][REPLs]]
- - [[#code-evaluation-1][Code Evaluation]]
+* Description
+This modules adds inline code evaluation support to Emacs, and supplies a
+universal interface for opening and interacting with REPLs.
-* Install
-This module has no external dependencies. However, specific languages may
-require additional setup.
+** Module Flags
+This module has no flags.
-Check the README.org in that language's module for details.
+** Plugins
++ [[https://github.com/syohex/emacs-quickrun][quickrun]]
-* Usage
-** REPLs
-Invoked via:
-+ ~:repl~ (evil ex-command)
-+ = o r= in normal mode (or visual mode, which sends the selection to
- the open REPL)
-+ ~M-x +eval/open-repl~
-+ ~M-x +eval/send-region-to-repl~ while a selection (and REPL) is active
+** Hacks
++ Quickrun has been modified to:
+ + Use only one output window, in case of consecutive execution of code.
+ + The quickrun window will resize itself to fit its output, once the
+ underlying process is finished executing the code.
-** *Code Evaluation*
+* Prerequisites
+This module has no direct prerequisites.
+
+However, specific languages may require additional setup. Check the
+documentation of that language's module for details.
+
+* Features
+** Inline Code Evaluation
Quickrun can be invoked via:
+ ~M-x +eval/buffer~ (or ~gR~, or ~M-r~)
+ ~M-x +eval/region~
+ ~M-x +eval/region-and-replace~
+ Evil users can use the ~gr~ operator to select and run a region.
-* Configuration
** REPLs
-REPLs are defined for most of the languages Doom supports (check its README.org
-to see if it does).
+Invoked via:
++ =SPC o r= or ~:repl~ will open a REPL in a popup window. =C-u SPC o r= or
+ ~:repl!~ will open a REPL in the current window. If a REPL is already open and
+ a selection is active, it will be sent to the REPL.
++ ~M-x +eval/open-repl-other-window~
++ ~M-x +eval/open-repl-same-window~
++ ~M-x +eval/send-region-to-repl~ while a selection (and REPL) is active
-Otherwise, you can define your own for a specified major-mode with the =:repl=
-setting.
+* Configuration
+** Register a REPL for a major-mode
+REPLs are defined for most languages Doom supports. Check that language module's
+README.org to see if it does (and if it requires additional setup).
-~(set! :repl MAJOR-MODE FUNCTION)~
+To use them, you may use ~M-x +eval/open-repl-other-window~, ~M-x
++eval/open-repl-same-window~, ~:repl~ (for evil users) or the default binding:
+=SPC o r=. These will open a REPL in a popup window.
-FUNCTION must return the repl buffer. Any window changes are ignored, then
-handed off to shackle (assuming shackle-mode is on) to display in a popup
-window.
+#+begin_quote
+You can simply call that mode's REPL command manually. e.g. ~M-x ielm~, but
+#+end_quote
+
+Otherwise, you can define your own for a specified major mode:
+
+~(set-repl-handler! MAJOR-MODE FUNCTION)~
+
+FUNCTION should return a repl buffer. Any window changes in this function are
+ignored, then the REPL is opened in a popup window.
#+BEGIN_SRC emacs-lisp
-(defun +emacs-lisp/repl ()
+(defun +lua/open-repl ()
(interactive)
- (pop-to-buffer
- (or (get-buffer "*ielm*")
- (progn (ielm)
- (let ((buf (get-buffer "*ielm*")))
- (bury-buffer buf)
- buf)))))
+ (lua-start-process "lua" "lua")
+ (pop-to-buffer lua-process-buffer))
-(set! :repl 'emacs-lisp-mode #'+emacs-lisp/repl)
+(set-repl-handler! 'lua-mode #'+lua/open-repl)
#+END_SRC
-** Code Evaluation
+** Change how code is evaluated in a major mode
Run regions or entire buffers with [[https://github.com/syohex/emacs-quickrun][Quickrun]]. Output is show in a popup window.
Quickrun includes support for many languages, usually by sending text directly
@@ -72,21 +96,22 @@ without support (like [[https://crystal-lang.org/][Crystal]]), or a language wit
Here's how you define a "runner":
#+BEGIN_SRC emacs-lisp
-(set! :eval 'crystal-mode
- '((:command . "crystal")
- (:exec . "%c %s")
- (:description . "Run Crystal script")))
+(set-eval-handler! 'crystal-mode
+ '((:command . "crystal")
+ (:exec . "%c %s")
+ (:description . "Run Crystal script")))
#+END_SRC
A simpler version is simply to use the path to the binary:
#+BEGIN_SRC emacs-lisp
-(set! :eval 'groovy-mode "groovy")
+(set-eval-handler! 'groovy-mode "groovy")
#+END_SRC
Or if you'd rather run an elisp command:
#+BEGIN_SRC emacs-lisp
-(set! :eval 'emacs-lisp-mode #'+emacs-lisp-eval)
+(set-eval-handler! 'emacs-lisp-mode #'+emacs-lisp-eval)
#+END_SRC
+* Troubleshooting
diff --git a/modules/feature/eval/autoload/eval.el b/modules/feature/eval/autoload/eval.el
index 75006b142..d7f8df29c 100644
--- a/modules/feature/eval/autoload/eval.el
+++ b/modules/feature/eval/autoload/eval.el
@@ -17,6 +17,23 @@
(funcall runner beg end)
(quickrun-region beg end))))
+;;;###autoload
+(defun +eval/line-or-region ()
+ "Evaluate the current line or selected region."
+ (interactive)
+ (if (use-region-p)
+ (call-interactively #'+eval/region)
+ (+eval/region (line-beginning-position) (line-end-position))))
+
+;;;###autoload
+(defun +eval/buffer-or-region ()
+ "Evaluate the whole buffer."
+ (interactive)
+ (call-interactively
+ (if (use-region-p)
+ #'+eval/region
+ #'+eval/buffer)))
+
;;;###autoload
(defun +eval/region-and-replace (beg end)
"Evaluation a region between BEG and END, and replace it with the result."
diff --git a/modules/feature/eval/autoload/evil.el b/modules/feature/eval/autoload/evil.el
index e36320976..279b41e32 100644
--- a/modules/feature/eval/autoload/evil.el
+++ b/modules/feature/eval/autoload/evil.el
@@ -19,5 +19,5 @@
:move-point nil
(interactive "")
(if (evil-normal-state-p)
- (+eval/open-repl)
+ (+eval/open-repl-other-window bang)
(+eval/send-region-to-repl beg end bang)))
diff --git a/modules/feature/eval/autoload/repl.el b/modules/feature/eval/autoload/repl.el
index 5655a64f3..58fde8c07 100644
--- a/modules/feature/eval/autoload/repl.el
+++ b/modules/feature/eval/autoload/repl.el
@@ -1,40 +1,90 @@
;;; feature/eval/autoload/repl.el -*- lexical-binding: t; -*-
-(defvar +eval-repl-buffer nil
+(defvar +eval-repl-buffers (make-hash-table :test 'equal)
"The buffer of the last open repl.")
-(defun +eval--ensure-in-repl-buffer (&optional command)
- (or (eq (current-buffer) +eval-repl-buffer)
- (progn
- (if (and +eval-repl-buffer (buffer-live-p +eval-repl-buffer))
- (if-let* ((win (get-buffer-window +eval-repl-buffer)))
- (select-window win)
- (doom-popup-buffer +eval-repl-buffer))
- (when command
- (let ((repl-buffer (save-window-excursion (call-interactively command))))
- (unless (bufferp repl-buffer)
- (error "REPL command didn't return a buffer"))
- (with-current-buffer repl-buffer (+eval-repl-mode +1))
- (setq +eval-repl-buffer repl-buffer)
- (select-window (doom-popup-buffer repl-buffer)))))
- (when (eq (current-buffer) +eval-repl-buffer)
- (goto-char (if (and (derived-mode-p 'comint-mode)
- (cdr comint-last-prompt))
- (cdr comint-last-prompt)
- (point-max)))
- t))))
+(define-minor-mode +eval-repl-mode
+ "A minor mode for REPL buffers.")
-;;;###autoload
-(defun +eval/open-repl ()
- "Opens (or reopens) the REPL associated with the current major-mode and place
-the cursor at the prompt."
- (interactive)
- (when-let* ((command (cdr (assq major-mode +eval-repls))))
- (when (+eval--ensure-in-repl-buffer command)
+(defun +eval--ensure-in-repl-buffer (&optional command other-window-p)
+ (maphash (lambda (key buffer)
+ (unless (buffer-live-p buffer)
+ (remhash key +eval-repl-buffers)))
+ +eval-repl-buffers)
+ (let* ((project-root (doom-project-root))
+ (key (cons major-mode project-root))
+ (buffer (gethash key +eval-repl-buffers)))
+ (cl-check-type buffer (or buffer null))
+ (unless (eq buffer (current-buffer))
+ (funcall (if other-window-p #'pop-to-buffer #'switch-to-buffer)
+ (if (buffer-live-p buffer)
+ buffer
+ (setq buffer
+ (save-window-excursion
+ (if (commandp command)
+ (call-interactively command)
+ (funcall command))))
+ (cond ((null buffer)
+ (error "REPL handler %S couldn't open the REPL buffer" command))
+ ((not (bufferp buffer))
+ (error "REPL handler %S failed to return a buffer" command)))
+ (with-current-buffer buffer
+ (+eval-repl-mode +1))
+ (puthash key buffer +eval-repl-buffers)
+ buffer)))
+ (with-current-buffer buffer
+ (goto-char (if (and (derived-mode-p 'comint-mode)
+ (cdr comint-last-prompt))
+ (cdr comint-last-prompt)
+ (point-max)))
+ buffer)))
+
+(defun +eval-open-repl (prompt-p &optional other-window-p)
+ (let ((command (cdr (assq major-mode +eval-repls))))
+ (when (or (not command) prompt-p)
+ (let* ((choices (or (cl-loop for sym being the symbols
+ for sym-name = (symbol-name sym)
+ if (string-match "^\\(?:\\+\\)?\\([^/]+\\)/open-\\(?:\\(.+\\)-\\)?repl$" sym-name)
+ collect
+ (format "%s (%s)"
+ (match-string-no-properties 1 sym-name)
+ (or (match-string-no-properties 2 sym-name) "default")))
+ (user-error "There are no known available REPLs")))
+ (choice (or (completing-read "Open a REPL for: " choices)
+ (user-error "Aborting")))
+ (choice-split (split-string choice " " t))
+ (module (car choice-split))
+ (repl (substring (cadr choice-split) 1 -1)))
+ (setq command
+ (intern-soft
+ (format "+%s/open-%srepl" module
+ (if (string= repl "default")
+ ""
+ repl))))))
+ (unless (commandp command)
+ (error "Couldn't find a valid REPL for %s" major-mode))
+ (when (+eval--ensure-in-repl-buffer command other-window-p)
(when (bound-and-true-p evil-mode)
(call-interactively #'evil-append-line))
t)))
+;;;###autoload
+(defun +eval/open-repl-same-window (&optional arg)
+ "Opens (or reopens) the REPL associated with the current major-mode and place
+the cursor at the prompt.
+
+If ARG (universal argument), prompt for a specific REPL to open."
+ (interactive "P")
+ (+eval-open-repl arg))
+
+;;;###autoload
+(defun +eval/open-repl-other-window (&optional arg)
+ "Does `+eval/open-repl', but in a popup window.
+
+If ARG (universal argument), prompt for a specific REPL to open."
+ (interactive "P")
+ (+eval-open-repl arg t))
+
;;;###autoload
(defun +eval/send-region-to-repl (beg end &optional auto-execute-p)
"REPL must be open! Sends a selected region to it. If AUTO-EXECUTE-P, then
diff --git a/modules/feature/eval/autoload/settings.el b/modules/feature/eval/autoload/settings.el
new file mode 100644
index 000000000..0cf618514
--- /dev/null
+++ b/modules/feature/eval/autoload/settings.el
@@ -0,0 +1,58 @@
+;;; feature/eval/autoload/settings.el -*- lexical-binding: t; -*-
+
+;;
+;; REPLs
+
+;;;###autoload
+(defvar +eval-repls nil
+ "An alist mapping major modes to plists that describe REPLs. Used by
+`+eval/open-repl-other-window' and filled with the `:repl' setting.")
+
+;;;###autodef
+(defun set-repl-handler! (modes command)
+ "Defines a REPL for MODES.
+
+MODES is either a single major mode symbol or a list of them. COMMAND is a
+function that creates and returns the REPL buffer.
+
+COMMAND can either be a function that takes no arguments, or an interactive
+command that will be called interactively."
+ (dolist (mode (doom-enlist modes))
+ (setf (alist-get mode +eval-repls) command)))
+
+
+;;
+;; Evaluation
+
+;;;###autoload
+(defvar +eval-runners nil
+ "Alist mapping major modes to interactive runner functions.")
+
+;;;###autodef
+(defun set-eval-handler! (mode command)
+ "Define a code evaluator for major mode MODE with `quickrun'.
+
+1. If MODE is a string and COMMAND is the string, MODE is a file regexp and
+ COMMAND is a string key for an entry in `quickrun-file-alist'.
+2. If MODE is not a string and COMMAND is a string, MODE is a major-mode symbol
+ and COMMAND is a key (for `quickrun--language-alist'), and will be registered
+ in `quickrun--major-mode-alist'.
+3. If MODE is not a string and COMMAND is an alist, see `quickrun-add-command':
+ (quickrun-add-command MODE COMMAND :mode MODE).
+4. If MODE is not a string and COMMANd is a symbol, add it to
+ `+eval-runners', which is used by `+eval/region'."
+ (declare (indent defun))
+ (cond ((symbolp command)
+ (push (cons mode command) +eval-runners))
+ ((stringp command)
+ (after! quickrun
+ (push (cons mode command)
+ (if (stringp mode)
+ quickrun-file-alist
+ quickrun--major-mode-alist))))
+ ((listp command)
+ (after! quickrun
+ (quickrun-add-command
+ (or (cdr (assq mode quickrun--major-mode-alist))
+ (string-remove-suffix "-mode" (symbol-name mode)))
+ command :mode mode)))))
diff --git a/modules/feature/eval/config.el b/modules/feature/eval/config.el
index ba954353c..b5c21ef7e 100644
--- a/modules/feature/eval/config.el
+++ b/modules/feature/eval/config.el
@@ -1,76 +1,17 @@
;;; feature/eval/config.el -*- lexical-binding: t; -*-
-;;
-;; REPLs
-;;
-
-(defvar +eval-repls nil
- "An alist mapping major modes to plists that describe REPLs. Used by
-`+eval/open-repl' and filled with the `:repl' setting.")
-
-(define-minor-mode +eval-repl-mode
- "A minor mode for REPL buffers.")
-
-(def-setting! :repl (mode command)
- "Define a REPL for a mode. MODE is a major mode symbol and COMMAND is a
-function that creates and returns the REPL buffer."
- `(push (cons ,mode ,command) +eval-repls))
-
-(set! :popup
- '(:custom (lambda (b &rest _) (buffer-local-value '+eval-repl-mode b)))
- :size 16 :noesc t)
-
-
-;;
-;; Evaluation
-;;
-
;; remove ellipsis when printing sexps in message buffer
(setq eval-expression-print-length nil
eval-expression-print-level nil)
-(defvar +eval-runners nil
- "Alist mapping major modes to interactive runner functions.")
-(def-setting! :eval (mode command)
- "Define a code evaluator for major mode MODE with `quickrun'.
+;;
+;; Packages
-1. If MODE is a string and COMMAND is the string, MODE is a file regexp and
- COMMAND is a string key for an entry in `quickrun-file-alist'.
-2. If MODE is not a string and COMMAND is a string, MODE is a major-mode symbol
- and COMMAND is a key (for `quickrun--language-alist'), and will be registered
- in `quickrun--major-mode-alist'.
-3. If MODE is not a string and COMMAND is an alist, see `quickrun-add-command':
- (quickrun-add-command MODE COMMAND :mode MODE).
-4. If MODE is not a string and COMMANd is a symbol, add it to
- `+eval-runners', which is used by `+eval/region'."
- (let ((command (doom-unquote command)))
- (cond ((symbolp command)
- `(push (cons ,mode ',command) +eval-runners))
- ((stringp command)
- `(after! quickrun
- (push (cons ,mode ',command)
- ,(if (stringp mode)
- 'quickrun-file-alist
- 'quickrun--major-mode-alist))))
- ((listp command)
- `(after! quickrun
- (quickrun-add-command
- ,(symbol-name (doom-unquote mode))
- ',command :mode ,mode))))))
+(after! quickrun
+ (setq quickrun-focus-p nil)
-(def-package! quickrun
- :commands (quickrun
- quickrun-region
- quickrun-with-arg
- quickrun-shell
- quickrun-compile-only
- quickrun-replace-region)
- :init
- (unless (boundp 'display-line-numbers)
- (add-hook 'quickrun--mode-hook #'nlinum-mode))
- :config
- (set! :popup "*quickrun*" :size 6 :autokill t :autoclose t)
+ (set-popup-rule! "^\\*quickrun" :size 0.3 :ttl 0)
(defun +eval*quickrun-auto-close (&rest _)
"Allows us to silently re-run quickrun from within the quickrun buffer."
@@ -82,10 +23,16 @@ function that creates and returns the REPL buffer."
(advice-add #'quickrun :before #'+eval*quickrun-auto-close)
(advice-add #'quickrun-region :before #'+eval*quickrun-auto-close)
+ (defun +eval|quickrun-shrink-window ()
+ "Shrink the quickrun output window once code evaluation is complete."
+ (with-selected-window (get-buffer-window quickrun--buffer-name)
+ (let ((ignore-window-parameters t))
+ (shrink-window-if-larger-than-buffer))))
+ (add-hook 'quickrun-after-run-hook #'+eval|quickrun-shrink-window)
+
(defun +eval|quickrun-scroll-to-bof ()
"Ensures window is scrolled to BOF on invocation."
(with-selected-window (get-buffer-window quickrun--buffer-name)
- (goto-char (point-min))
- (doom-popup-fit-to-buffer)))
+ (goto-char (point-min))))
(add-hook 'quickrun-after-run-hook #'+eval|quickrun-scroll-to-bof))
diff --git a/modules/feature/evil/+commands.el b/modules/feature/evil/+commands.el
new file mode 100644
index 000000000..82dedee36
--- /dev/null
+++ b/modules/feature/evil/+commands.el
@@ -0,0 +1,174 @@
+;;; feature/evil/+commands.el -*- lexical-binding: t; -*-
+
+(evil-define-operator +evil:open-scratch-buffer (bang)
+ (interactive "")
+ (doom/open-scratch-buffer bang))
+
+(evil-define-command +evil:pwd (bang)
+ "Display the current working directory. If BANG, copy it to your clipboard."
+ (interactive "")
+ (if (not bang)
+ (pwd)
+ (kill-new default-directory)
+ (message "Copied to clipboard")))
+
+(evil-define-command +evil:make (arguments &optional bang)
+ "Run make with ARGUMENTS.
+If BANG is non-nil, open compilation output in a comint buffer.
+
+If BANG, then run ARGUMENTS as a full command. This command understands vim file
+modifiers (like %:p:h). See `+evil*resolve-vim-path' for details."
+ (interactive "")
+ (+evil:compile (format "make %s"
+ (evil-ex-replace-special-filenames
+ arguments))
+ bang))
+
+(evil-define-command +evil:compile (arguments &optional bang)
+ "Run `compile-command' with ARGUMENTS.
+If BANG is non-nil, open compilation output in a comint buffer.
+
+This command understands vim file modifiers (like %:p:h). See
+`+evil*resolve-vim-path' for details."
+ (interactive "")
+ (compile (evil-ex-replace-special-filenames
+ (format "%s %s"
+ (eval compile-command)
+ arguments))
+ bang))
+
+(evil-define-command +evil:reverse-lines (beg end)
+ "Reverse lines between BEG and END."
+ (interactive "")
+ (reverse-region beg end))
+
+(evil-define-command +evil:cd (&optional path)
+ "Change `default-directory' with `cd'."
+ (interactive "")
+ (let ((path (or path "~")))
+ (cd path)
+ (message "Changed directory to '%s'" (abbreviate-file-name (expand-file-name path)))))
+
+(evil-define-command +evil:kill-all-buffers (&optional bang)
+ "Kill all buffers. If BANG, kill current session too."
+ (interactive "")
+ (if (and bang (fboundp '+workspace/kill-session))
+ (+workspace/kill-session)
+ (doom/kill-all-buffers)))
+
+(evil-define-command +evil:kill-matching-buffers (&optional bang pattern)
+ "Kill all buffers matching PATTERN regexp. If BANG, only match project
+buffers."
+ (interactive "")
+ (doom/kill-matching-buffers pattern bang))
+
+
+;;
+;; Commands
+
+;;; Custom commands
+;; Editing
+(evil-ex-define-cmd "@" #'+evil:macro-on-all-lines) ; TODO Test me
+(evil-ex-define-cmd "al[ign]" #'+evil:align)
+(evil-ex-define-cmd "ral[ign]" #'+evil:align-right)
+(evil-ex-define-cmd "enhtml" #'+web:encode-html-entities)
+(evil-ex-define-cmd "dehtml" #'+web:decode-html-entities)
+(evil-ex-define-cmd "mc" #'+multiple-cursors:evil-mc)
+(evil-ex-define-cmd "iedit" #'evil-multiedit-ex-match)
+(evil-ex-define-cmd "na[rrow]" #'+evil:narrow-buffer)
+(evil-ex-define-cmd "retab" #'+evil:retab)
+(evil-ex-define-cmd "rev[erse]" #'+evil:reverse-lines)
+
+;;; External resources
+;; TODO (evil-ex-define-cmd "db" #'doom:db)
+;; TODO (evil-ex-define-cmd "dbu[se]" #'doom:db-select)
+;; TODO (evil-ex-define-cmd "go[ogle]" #'doom:google-search)
+(evil-ex-define-cmd "lo[okup]" #'+lookup:online)
+(evil-ex-define-cmd "dash" #'+lookup:dash)
+(evil-ex-define-cmd "http" #'httpd-start) ; start http server
+(evil-ex-define-cmd "repl" #'+eval:repl) ; invoke or send to repl
+
+;; TODO (evil-ex-define-cmd "rx" 'doom:regex) ; open re-builder
+(evil-ex-define-cmd "sh[ell]" #'+eshell:run)
+(evil-ex-define-cmd "t[mux]" #'+tmux:run) ; send to tmux
+(evil-ex-define-cmd "tcd" #'+tmux:cd-here) ; cd to default-directory in tmux
+(evil-ex-define-cmd "pad" #'+evil:open-scratch-buffer)
+
+;;; GIT
+(evil-ex-define-cmd "gist" #'+gist:send) ; send current buffer/region to gist
+(evil-ex-define-cmd "gistl" #'+gist:list) ; list gists by user
+(evil-ex-define-cmd "gbrowse" #'+vc:git-browse) ; show file/region in github/gitlab
+(evil-ex-define-cmd "gissues" #'forge-browse-issues) ; show github issues
+(evil-ex-define-cmd "git" #'magit-status) ; open magit status window
+(evil-ex-define-cmd "gstage" #'magit-stage)
+(evil-ex-define-cmd "gunstage" #'magit-unstage)
+(evil-ex-define-cmd "gblame" #'magit-blame)
+(evil-ex-define-cmd "grevert" #'git-gutter:revert-hunk)
+
+;;; Dealing with buffers
+(evil-ex-define-cmd "k[ill]" #'doom/kill-this-buffer)
+(evil-ex-define-cmd "k[ill]all" #'+evil:kill-all-buffers)
+(evil-ex-define-cmd "k[ill]m" #'+evil:kill-matching-buffers)
+(evil-ex-define-cmd "k[ill]o" #'doom/kill-other-buffers)
+(evil-ex-define-cmd "k[ill]b" #'doom/kill-buried-buffers)
+(evil-ex-define-cmd "l[ast]" #'doom/popup-restore)
+(evil-ex-define-cmd "m[sg]" #'view-echo-area-messages)
+(evil-ex-define-cmd "pop[up]" #'doom/popup-this-buffer)
+
+;;; Project navigation
+(evil-ex-define-cmd "a" #'projectile-find-other-file)
+(evil-ex-define-cmd "cd" #'+evil:cd)
+(evil-ex-define-cmd "pwd" #'+evil:pwd)
+
+(cond ((featurep! :completion ivy)
+ (evil-ex-define-cmd "ag" #'+ivy:ag)
+ (evil-ex-define-cmd "agc[wd]" #'+ivy:ag-from-cwd)
+ (evil-ex-define-cmd "rg" #'+ivy:rg)
+ (evil-ex-define-cmd "rgc[wd]" #'+ivy:rg-from-cwd)
+ (evil-ex-define-cmd "pt" #'+ivy:pt)
+ (evil-ex-define-cmd "ptc[wd]" #'+ivy:pt-from-cwd)
+ (evil-ex-define-cmd "grep" #'+ivy:grep)
+ (evil-ex-define-cmd "grepc[wd]" #'+ivy:grep-from-cwd)
+ (evil-ex-define-cmd "sw[iper]" #'+ivy:swiper)
+ (evil-ex-define-cmd "todo" #'+ivy:todo))
+
+ ((featurep! :completion helm)
+ (evil-ex-define-cmd "ag" #'+helm:ag)
+ (evil-ex-define-cmd "agc[wd]" #'+helm:ag-from-cwd)
+ (evil-ex-define-cmd "rg" #'+helm:rg)
+ (evil-ex-define-cmd "rgc[wd]" #'+helm:rg-from-cwd)
+ (evil-ex-define-cmd "pt" #'+helm:pt)
+ (evil-ex-define-cmd "ptc[wd]" #'+helm:pt-from-cwd)
+ (evil-ex-define-cmd "grep" #'+helm:grep)
+ (evil-ex-define-cmd "grepc[wd]" #'+helm:grep-from-cwd)
+ ;; (evil-ex-define-cmd "todo" #'+helm:todo) TODO implement `+helm:todo'
+ ))
+
+;;; Project tools
+(evil-ex-define-cmd "compile" #'+evil:compile)
+(evil-ex-define-cmd "mak[e]" #'+evil:make)
+;; (evil-ex-define-cmd "debug" #'+debug/run)
+(evil-ex-define-cmd "er[rors]" #'flycheck-list-errors)
+
+;;; File operations
+(evil-ex-define-cmd "cp" #'+evil:copy-this-file)
+(evil-ex-define-cmd "mv" #'+evil:move-this-file)
+(evil-ex-define-cmd "rm" #'+evil:delete-this-file)
+
+;;; Sessions/tabs
+(evil-ex-define-cmd "sclear" #'+workspace/kill-session)
+(evil-ex-define-cmd "sl[oad]" #'doom/quickload-session)
+(evil-ex-define-cmd "ss[ave]" #'doom/quicksave-session)
+(evil-ex-define-cmd "tabc[lose]" #'+workspace:delete)
+(evil-ex-define-cmd "tabclear" #'doom/kill-all-buffers)
+(evil-ex-define-cmd "tabl[ast]" #'+workspace/switch-to-last)
+(evil-ex-define-cmd "tabload" #'+workspace:load)
+(evil-ex-define-cmd "tabn[ew]" #'+workspace:new)
+(evil-ex-define-cmd "tabn[ext]" #'+workspace:switch-next)
+(evil-ex-define-cmd "tabp[rev]" #'+workspace:switch-previous)
+(evil-ex-define-cmd "tabr[ename]" #'+workspace:rename)
+(evil-ex-define-cmd "tabs" #'+workspace/display)
+(evil-ex-define-cmd "tabsave" #'+workspace:save)
+
+;;; Org-mode
+(evil-ex-define-cmd "cap" #'org-capture)
diff --git a/modules/feature/evil/+everywhere.el b/modules/feature/evil/+everywhere.el
new file mode 100644
index 000000000..71c2e09e2
--- /dev/null
+++ b/modules/feature/evil/+everywhere.el
@@ -0,0 +1,204 @@
+;;; feature/evil/+everywhere.el -*- lexical-binding: t; -*-
+
+;; We load evil-collection ourselves for these reasons:
+;;
+;; 1. To truly lazy load it. Some of its modules, like
+;; evil-collection-{elisp-mode,buff-menu} are loaded immediately, because
+;; Emacs loads their packages immediately, which pulls in all of
+;; evil-collection (and other packages with it, sometimes).
+;; 2. This ensures a predictable load order, versus lazy loading using :defer or
+;; :after-call. This means users can use (after! org ...) and be sure that
+;; their changes will override evil-collection's.
+;; 3. Eventually, I'd like to remove evil-collection. It changes too often,
+;; introduces breaking bugs too frequently, and I don't always agree with
+;; their design choices. Regardless, there are useful tidbits I'd like to
+;; keep. This will be a slow transition, but this file is where most of it
+;; will happen.
+;; 4. Adds `+evil-collection-disabled-list', to make it easier for users to
+;; disable modules, and to reduce the effort required to maintain our copy of
+;; `evil-collection-list' (now I can just copy it from time to time).
+
+(defvar +evil-collection-disabled-list
+ '(anaconda-mode
+ buff-menu
+ comint
+ company
+ custom
+ eldoc
+ elisp-mode
+ ert
+ free-keys
+ help
+ helm
+ image
+ ivy
+ kotlin-mode
+ occur
+ package-menu
+ ruby-mode
+ simple
+ slime)
+ "A list of `evil-collection' modules to ignore. See the definition of this
+variable for an explanation of the defaults (in comments). See
+`evil-collection-mode-list' for a list of available options.")
+
+(defvar evil-collection-setup-minibuffer nil)
+
+;; This has to be defined here since evil-collection doesn't autoload its own.
+;; It must be updated whenever evil-collection updates theirs.
+(defvar evil-collection-mode-list
+ `(ag
+ alchemist
+ anaconda-mode
+ arc-mode
+ bookmark
+ (buff-menu "buff-menu")
+ calc
+ calendar
+ cider
+ cmake-mode
+ comint
+ company
+ compile
+ custom
+ cus-theme
+ daemons
+ deadgrep
+ debbugs
+ debug
+ diff-mode
+ dired
+ disk-usage
+ doc-view
+ ebib
+ edbi
+ edebug
+ ediff
+ eglot
+ elfeed
+ elisp-mode
+ elisp-refs
+ emms
+ epa
+ ert
+ eshell
+ eval-sexp-fu
+ evil-mc
+ eww
+ flycheck
+ flymake
+ free-keys
+ geiser
+ ggtags
+ git-timemachine
+ go-mode
+ grep
+ guix
+ hackernews
+ helm
+ help
+ helpful
+ ibuffer
+ image
+ image-dired
+ image+
+ imenu-list
+ indium
+ info
+ ivy
+ js2-mode
+ log-view
+ lsp-ui-imenu
+ lua-mode
+ kotlin-mode
+ macrostep
+ man
+ magit
+ magit-todos
+ ,@(when evil-collection-setup-minibuffer '(minibuffer))
+ mu4e
+ mu4e-conversation
+ neotree
+ notmuch
+ nov
+ ;; occur is in replace.el which was built-in before Emacs 26.
+ (occur ,(if (<= emacs-major-version 25) "replace" 'replace))
+ omnisharp
+ outline
+ p4
+ (package-menu package)
+ pass
+ (pdf pdf-view)
+ popup
+ proced
+ process-menu
+ prodigy
+ profiler
+ python
+ quickrun
+ racer
+ realgud
+ reftex
+ restclient
+ rjsx-mode
+ robe
+ ruby-mode
+ rtags
+ simple
+ slime
+ (term term ansi-term multi-term)
+ tetris
+ tide
+ transmission
+ typescript-mode
+ vc-annotate
+ vc-dir
+ vc-git
+ vdiff
+ view
+ vlf
+ w3m
+ wdired
+ wgrep
+ which-key
+ woman
+ xref
+ youtube-dl
+ (ztree ztree-diff)))
+
+(defun +evil-collection-init (module)
+ (unless (memq (or (car-safe module) module) +evil-collection-disabled-list)
+ (doom-log "Initialized evil-collection-%s" (or (car-safe module) module))
+ (with-demoted-errors "evil-collection error: %s"
+ (evil-collection-init (list module)))))
+
+
+;;
+;; Bootstrap
+
+;; These modes belong to packages that Emacs always loads at startup, causing
+;; evil-collection to load immediately. We avoid this by loading them after
+;; evil-collection has first loaded...
+(after! evil-collection
+ (let (+evil-collection-disabled-list)
+ (mapc #'+evil-collection-init '(comint custom help))))
+
+;; ...or on first invokation of their associated major/minor modes.
+(add-transient-hook! 'Buffer-menu-mode
+ (+evil-collection-init '(buff-menu "buff-menu")))
+(add-transient-hook! 'image-mode
+ (+evil-collection-init 'image))
+(add-transient-hook! 'emacs-lisp-mode
+ (+evil-collection-init 'elisp-mode))
+(add-transient-hook! 'occur-mode
+ (+evil-collection-init (if EMACS26+ 'replace "replace")))
+
+(evil-define-key* 'normal process-menu-mode-map
+ "q" #'kill-this-buffer
+ "d" #'process-menu-delete-process)
+
+;; Load the rest
+(dolist (mode evil-collection-mode-list)
+ (dolist (req (or (cdr-safe mode) (list mode)))
+ (with-eval-after-load req
+ (+evil-collection-init mode))))
diff --git a/modules/feature/evil/README.org b/modules/feature/evil/README.org
index 4093c770a..0998398c0 100644
--- a/modules/feature/evil/README.org
+++ b/modules/feature/evil/README.org
@@ -1,61 +1,176 @@
-#+TITLE: :feature evil
+#+TITLE: feature/evil
+#+DATE: February 2, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
+* Table of Contents :TOC_3:noexport:
+- [[#description][Description]]
+ - [[#module-flags][Module Flags]]
+ - [[#plugins][Plugins]]
+ - [[#hacks][Hacks]]
+- [[#prerequisites][Prerequisites]]
+- [[#features][Features]]
+ - [[#ported-vim-plugins][Ported vim plugins]]
+ - [[#custom-text-objects][Custom Text Objects]]
+ - [[#custom-ex-commands][Custom Ex Commands]]
+- [[#configuration][Configuration]]
+ - [[#removing-evil-mode][Removing evil-mode]]
+ - [[#restoring-old-substitution-behavior-on-ss][Restoring old substitution behavior on s/S]]
+
+* Description
This holy module brings the vim experience to Emacs.
-* Table of Contents :TOC:
-- [[#removing-evil-mode][Removing evil-mode]]
-- [[#features][Features]]
- - [[#multiple-cursors][Multiple-cursors]]
- - [[#a-hybrid-code-folding-system][A hybrid code-folding system]]
- - [[#hacks][Hacks]]
- - [[#differences-from-vim][Differences from vim]]
+** Module Flags
++ =+everywhere= Enables evilified keybinds everywhere possible. Uses the
+ [[https://github.com/emacs-evil/evil-collection][evil-collection]] plugin as a foundation.
-* Removing evil-mode
-See the [[https://github.com/hlissner/doom-emacs/wiki/FAQ#remove-vimevil-for-a-more-vanilla-emacs-experience][corresponding question in the FAQ]].
-
-* Features
-+ A better ~:g[lobal]~ command with incremental highlighting.
-+ Adds the ~:al[ign]~ ex command: offers an ex interface to ~align-regexp~ with
- incremental highlighting.
-+ Support for more of vim's filename modifiers in ex commands (like ~:p~, ~:p:h~
- or ~:t~) than vanilla evil-mode offers.
-+ A list of new text objects:
- + Blocks: ~B~ (from ~evil-textobj-anyblock~)
- + Args: ~a~ (from ~evil-args~)
- + Indentation: ~i~ / ~I~ / ~J~ (from ~evil-indent-plus~)
-+ Incorporates vim functionality ported to evil:
- + ~vim-commentary~ => ~evil-commentary~
- + ~vim-easymotion~ => ~evil-easymotion~
- + ~vim-multiedit~ => ~evil-multiedit~
- + ~vim-multiple-cursors~ => ~evil-mc~ & ~evil-multiedit~
- + ~vim-seek~ or ~vim-sneak~ => ~evil-snipe~
- + ~vim-surround~ => ~evil-embrace~ & ~evil-surround~
-+ =NERDTree= equivalent is available in =:tools neotree=
-
-** Multiple-cursors
-Two multiple-cursor implementations exist in this module: ~evil-mc~ and
-~evil-multiedit~. Together, these provide the functionality of
-~vim-multiple-cursors~.
-
-The former lets you place "clone" cursors. The latter lets you interactively
-edit many regions at once (like an interactive version of ~:%s~).
-
-** A hybrid code-folding system
-This module combines ~evil-vimish-fold~ and ~hideshow~. The former allows
-arbitrary folds and the latter allows folds on markers and indentation.
-Together, they create a more consistent (and feature-complete) code-folding
-system.
-
-Most vim folding keys should work, e.g. =zr=, =zm=, =za=, =zo=, etc.
+** Plugins
++ [[https://github.com/emacs-evil/evil][evil]]
++ [[https://github.com/wcsmith/evil-args][evil-args]]
++ [[https://github.com/linktohack/evil-commentary][evil-commentary]]
++ [[https://github.com/PythonNut/evil-easymotion][evil-easymotion]]
++ [[https://github.com/cute-jumper/evil-embrace.el][evil-embrace]]
++ [[https://github.com/syl20bnr/evil-escape][evil-escape]]
++ [[https://github.com/Dewdrops/evil-exchange][evil-exchange]]
++ [[https://github.com/TheBB/evil-indent-plus][evil-indent-plus]]
++ [[https://github.com/redguardtoo/evil-matchit][evil-matchit]]
++ [[https://github.com/cofi/evil-numbers][evil-numbers]]
++ [[https://github.com/noctuid/evil-textobj-anyblock][evil-textobj-anyblock]]
++ [[https://github.com/hlissner/evil-snipe][evil-snipe]]
++ [[https://github.com/emacs-evil/evil-surround][evil-surround]]
++ [[https://github.com/alexmurray/evil-vimish-fold][evil-vimish-fold]]
++ [[https://github.com/bling/evil-visualstar][evil-visualstar]]
++ [[https://github.com/ninrod/exato][exato]]
++ [[https://github.com/emacs-evil/evil-collection][evil-collection]]*
** Hacks
-+ Automatically moves to new window when splitting
-+ From visual mode, =*= and =#= will search for the current selection instead of
++ When a window is split, the new window will be focused.
++ The o/O keys will respect and continue commented lines (can be disabled by
+ setting ~+evil-want-o/O-to-continue-comments~ to ~nil~).
++ In visual mode, =*= and =#= will search for the current selection instead of
the word-at-point.
++ The ~:g[lobal]~ ex command has been modified to highlight matches.
++ More of vim's filename modifiers are supported in ex commands (like ~:p~,
+ ~:p:h~ or ~:t~) than vanilla evil-mode offers.
++ A custom filename modifier is available in Doom: ~:P~, which expands to the
+ project root (throws an error if not in a project).
-** Differences from vim
-+ Column-wise ranges in ex commands are enabled by default. i.e. the range in
- =:'<,'>s/a/b= will only affects the visual selection, not full lines (see
- ~evil-ex-visual-char-range~).
-+ =:g= will incrementally highlight buffer matches.
+* Prerequisites
+This module has no external prerequisites.
+* Features
+** Ported vim plugins
+The following vim plugins have been ported to evil:
+
+| Vim Plugin | Emacs Plugin | Keybind(s) |
+|-----------------------+--------------------------------+--------------------------------------|
+| vim-commentary | evil-commentary | omap =gc= |
+| vim-easymotion | evil-easymotion | omap =gs= |
+| vim-seek or vim-sneak | evil-snipe | mmap =s=/=S=, omap =z=/=Z= & =x=/=x= |
+| vim-surround | evil-embrace and evil-surround | vmap =S=, omap =ys= |
+
+In other modules:
++ The tools/neotree & tools/treemacs modules provide a =NERDTree= equivalent.
++ The editor/multiple-cursors module contains functionality equal to the
+ following vim plugins:
+ + evil-multiedit => vim-multiedit
+ + evil-mc => vim-multiple-cursors
+
+** Custom Text Objects
+This module provides a couple extra text objects, along with the built-in ones.
+For posterity, here are the built-in ones:
+
++ =w W= words
++ =s= sentences
++ =p= paragraphs
++ =b= parenthesized blocks
++ =b ( ) { } [ ] < >= braces, parentheses and brackets
++ =' " `= quotes
++ =t= tags
++ =o= symbols
+
+And these are text objects added by this module:
+
++ =a= C-style fucntion arguments (provided by ~evil-args~)
++ =B= any block delimited by braces, parentheses or backets (provided by
+ ~evil-textobj-anyblock~)
++ =i j k= By indentation (=k= includes on line above and =j= includes one line
+ below) (provided by ~evil-indent-plus~)
++ =x= XML attributes (provided by ~exato~)
+
+** Custom Ex Commands
+| Ex Command | Description |
+|-----------------------+--------------------------------------------------------------------------------------|
+| ~:@~ | Apply macro on selected lines |
+| ~:ag[!] REGEXP~ | Perform a project search with ag |
+| ~:agcwd[!] REGEXP~ | Perform a project search with ag from the current directory |
+| ~:al[ign][!] REGEXP~ | Align text to the first match of REGEXP. If BANG, align all matches on each line |
+| ~:cp[!] NEWPATH~ | Copy the current file to NEWPATH |
+| ~:dash QUERY~ | Look up QUERY (or the symbol at point) in dash docsets |
+| ~:dehtml [INPUT]~ | HTML decode selected text / inserts result if INPUT is given |
+| ~:enhtml [INPUT]~ | HTML encode selected text / inserts result if INPUT is given |
+| ~:grep[!]~ | Perform a project search with git-grep |
+| ~:grepcwd[!]~ | Perform a project search with git-grep from the current directory |
+| ~:iedit REGEXP~ | Invoke iedit on all matches for REGEXP |
+| ~:k[ill]all[!]~ | Kill all buffers (if BANG, affect buffer across workspaces) |
+| ~:k[ill]b~ | Kill all buried buffers |
+| ~:k[ill]m[!] REGEXP~ | Kill buffers whose name matches REGEXP (if BANG, affect buffers across workspaces) |
+| ~:k[ill]o~ | Kill all other buffers besides the selected one |
+| ~:k[ill]~ | Kill the current buffer |
+| ~:lo[okup] QUERY~ | Look up QUERY on an online search engine |
+| ~:mc REGEXP~ | Invoke multiple cursors on all matches for REGEXP |
+| ~:mv[!] NEWPATH~ | Move the current file to NEWPATH |
+| ~:na[rrow]~ | Narrow the buffer to the selection |
+| ~:pad~ | Open a scratch pad for running code quickly |
+| ~:pt[!]~ | Perform a project search with pt |
+| ~:ptcwd[!]~ | Perform a project search with pt from the current directory |
+| ~:ral[ign][!] REGEXP~ | Right-Align text that matches REGEXP. If BANG, align all matches on each line |
+| ~:repl~ | Open a REPL and/or copy the current selection to it |
+| ~:retab~ | Convert indentation to the default within the selection |
+| ~:rev[erse]~ | Reverse the selected lines |
+| ~:rg[!]~ | Perform a project search with ripgrep |
+| ~:rgcwd[!]~ | Perform a project search with rigprep from the current directory |
+| ~:rm[!] [PATH]~ | Delete the current buffer's file and buffer |
+| ~:tcd[!]~ | Send =cd X= to tmux. X = the project root if BANG, X = ~default-directory~ otherwise |
+
+* Configuration
+** Removing evil-mode
+You must do two things to remove Evil:
+
+1. Remove =:feature evil= from =~/.doom.d/init.el=,
+2. Run ~doom refresh~ to clean up lingering dependencies and refresh yuor
+ autoloads files.
+3. [OPTIONAL] You may want to assign new values to ~doom-leader-alt-key~ and
+ ~doom-localleader-alt-key~. These are bound to =C-c= and =C-c l= by default.
+
+#+begin_quote
+Ignore ~doom-leader-key~ and ~doom-localleader-key~, they don't apply to
+non-evil sessions.
+#+end_quote
+
+Evil-specific configuration and keybindings (defined with ~map!~) will be
+ignored without =:feature evil= present (and omitted when byte-compiling).
+
+Keep in mind that, at the time of this writing, Doom was designed by a vimmer,
+for vimmers. Little consideration has been put into designing a keybind scheme
+for vanilla Emacs users (though it's being worked on!).
+
+That means that much of Doom's functionality will be orphaned in an evil-less
+setup. You'll have to set your own keybinds.
+
+I suggest studying [[file:../../config/default/+emacs-bindings.el][config/default/+emacs-bindings.el]] to see what keybinds are
+available for non-evil users. Otherwise, you may find inspiration [[file:../../../docs/example_configs.org][on the example
+Doom configurations page]].
+
+** Restoring old substitution behavior on s/S
+Doom replaces the =s= and =S= keys with the =evil-snipe= package (a port of
+vim-seek/vim-sneak for 2-character versions of f/F/t/T).
+
+To disable evil-snipe on s/S, you can either:
+
+1. Disable ~evil-snipe-mode~ by adding ~(after! evil-snipe (evil-snipe-mode
+ -1))~ to =$DOOMDIR/config.el=,
+2. Or disable =evil-snipe= completely with ~(package! evil-snipe :disable t)~
+ added to =$DOOMDIR/packages.el=, but this will also disable incremental
+ highlighting for the f/F/t/T motions keys.
+3. Or use =cl= and =cc=, respectively; they do the same thing.
diff --git a/modules/feature/evil/autoload/advice.el b/modules/feature/evil/autoload/advice.el
new file mode 100644
index 000000000..ee16df71d
--- /dev/null
+++ b/modules/feature/evil/autoload/advice.el
@@ -0,0 +1,207 @@
+;;; feature/evil/autoload/advice.el -*- lexical-binding: t; -*-
+
+(defun +evil--insert-newline (&optional above _noextranewline)
+ (let ((pos (save-excursion (beginning-of-line-text) (point)))
+ comment-auto-fill-only-comments)
+ (require 'smartparens)
+ (evil-narrow-to-field
+ (if above
+ (if (save-excursion (nth 4 (sp--syntax-ppss pos)))
+ (evil-save-goal-column
+ (setq evil-auto-indent nil)
+ (goto-char pos)
+ (let ((ws (abs (skip-chars-backward " \t"))))
+ ;; FIXME oh god why
+ (save-excursion
+ (if comment-line-break-function
+ (funcall comment-line-break-function)
+ (comment-indent-new-line))
+ (when (and (derived-mode-p 'c-mode 'c++-mode 'objc-mode 'java-mode 'js2-mode)
+ (eq (char-after) ?/))
+ (insert "*"))
+ (insert
+ (make-string (max 0 (+ ws (skip-chars-backward " \t")))
+ 32)))
+ (insert (make-string (max 1 ws) 32))))
+ (evil-move-beginning-of-line)
+ (insert (if use-hard-newlines hard-newline "\n"))
+ (forward-line -1)
+ (back-to-indentation))
+ (evil-move-end-of-line)
+ (cond ((sp-point-in-comment pos)
+ (setq evil-auto-indent nil)
+ (if comment-line-break-function
+ (funcall comment-line-break-function)
+ (comment-indent-new-line)))
+ ;; TODO Find a better way to do this
+ ((and (eq major-mode 'haskell-mode)
+ (fboundp 'haskell-indentation-newline-and-indent))
+ (setq evil-auto-indent nil)
+ (haskell-indentation-newline-and-indent))
+ (t
+ (insert (if use-hard-newlines hard-newline "\n"))
+ (back-to-indentation)))))))
+
+;;;###autoload
+(defun +evil*insert-newline-below-and-respect-comments (orig-fn count)
+ (if (or (not +evil-want-o/O-to-continue-comments)
+ (not (eq this-command 'evil-open-below))
+ (evil-insert-state-p))
+ (funcall orig-fn count)
+ (cl-letf (((symbol-function 'evil-insert-newline-below)
+ (lambda () (+evil--insert-newline))))
+ (let ((evil-auto-indent evil-auto-indent))
+ (funcall orig-fn count)))))
+
+;;;###autoload
+(defun +evil*insert-newline-above-and-respect-comments (orig-fn count)
+ (if (or (not +evil-want-o/O-to-continue-comments)
+ (not (eq this-command 'evil-open-above))
+ (evil-insert-state-p))
+ (funcall orig-fn count)
+ (cl-letf (((symbol-function 'evil-insert-newline-above)
+ (lambda () (+evil--insert-newline 'above))))
+ (let ((evil-auto-indent evil-auto-indent))
+ (funcall orig-fn count)))))
+
+;;;###autoload
+(defun +evil*static-reindent (orig-fn &rest args)
+ "Don't move cursor on indent."
+ (save-excursion (apply orig-fn args)))
+
+;;;###autoload
+(defun +evil*resolve-vim-path (file-name)
+ "Take a path and resolve any vim-like filename modifiers in it. This adds
+support for most vim file modifiers, as well as:
+
+ %:P Resolves to `doom-project-root'.
+
+See http://vimdoc.sourceforge.net/htmldoc/cmdline.html#filename-modifiers for
+more information on modifiers."
+ (let* (case-fold-search
+ (regexp (concat "\\(?:^\\|[^\\\\]\\)"
+ "\\([#%]\\)"
+ "\\(\\(?::\\(?:[PphtreS~.]\\|g?s[^:\t\n ]+\\)\\)*\\)"))
+ (matches
+ (cl-loop with i = 0
+ while (and (< i (length file-name))
+ (string-match regexp file-name i))
+ do (setq i (1+ (match-beginning 0)))
+ and collect
+ (cl-loop for j to (/ (length (match-data)) 2)
+ collect (match-string j file-name)))))
+ (dolist (match matches)
+ (let ((flags (split-string (car (cdr (cdr match))) ":" t))
+ (path (and buffer-file-name
+ (pcase (car (cdr match))
+ ("%" (file-relative-name buffer-file-name))
+ ("#" (save-excursion (other-window 1) (file-relative-name buffer-file-name))))))
+ flag global)
+ (if (not path)
+ (setq path "")
+ (while flags
+ (setq flag (pop flags))
+ (when (string-suffix-p "\\" flag)
+ (setq flag (concat flag (pop flags))))
+ (when (string-prefix-p "gs" flag)
+ (setq global t
+ flag (substring flag 1)))
+ (setq path
+ (or (pcase (substring flag 0 1)
+ ("p" (expand-file-name path))
+ ("~" (concat "~/" (file-relative-name path "~")))
+ ("." (file-relative-name path default-directory))
+ ("t" (file-name-nondirectory (directory-file-name path)))
+ ("r" (file-name-sans-extension path))
+ ("e" (file-name-extension path))
+ ("S" (shell-quote-argument path))
+ ("h"
+ (let ((parent (file-name-directory (expand-file-name path))))
+ (unless (equal (file-truename path)
+ (file-truename parent))
+ (if (file-name-absolute-p path)
+ (directory-file-name parent)
+ (file-relative-name parent)))))
+ ("s"
+ (if (featurep 'evil)
+ (when-let* ((args (evil-delimited-arguments (substring flag 1) 2)))
+ (let ((pattern (evil-transform-vim-style-regexp (car args)))
+ (replace (cadr args)))
+ (replace-regexp-in-string
+ (if global pattern (concat "\\(" pattern "\\).*\\'"))
+ (evil-transform-vim-style-regexp replace) path t t
+ (unless global 1))))
+ path))
+ ("P"
+ (let ((project-root (doom-project-root (file-name-directory (expand-file-name path)))))
+ (unless project-root
+ (user-error "Not in a project"))
+ (abbreviate-file-name project-root)))
+ (_ path))
+ "")))
+ ;; strip trailing slash, if applicable
+ (when (and (not (string= path "")) (equal (substring path -1) "/"))
+ (setq path (substring path 0 -1))))
+ (setq file-name
+ (replace-regexp-in-string (format "\\(?:^\\|[^\\\\]\\)\\(%s\\)"
+ (regexp-quote (string-trim-left (car match))))
+ path file-name t t 1))))
+ (replace-regexp-in-string regexp "\\1" file-name t)))
+
+;;;###autoload (autoload '+evil*window-split "feature/evil/autoload/advice" nil t)
+(evil-define-command +evil*window-split (&optional count file)
+ "Same as `evil-window-split', but focuses (and recenters) the new split."
+ :repeat nil
+ (interactive "P")
+ (split-window (selected-window) count
+ (if evil-split-window-below 'above 'below))
+ (call-interactively
+ (if evil-split-window-below
+ #'evil-window-up
+ #'evil-window-down))
+ (recenter)
+ (when (and (not count) evil-auto-balance-windows)
+ (balance-windows (window-parent)))
+ (if file (evil-edit file)))
+
+;;;###autoload (autoload '+evil*window-vsplit "feature/evil/autoload/advice" nil t)
+(evil-define-command +evil*window-vsplit (&optional count file)
+ "Same as `evil-window-vsplit', but focuses (and recenters) the new split."
+ :repeat nil
+ (interactive "P")
+ (split-window (selected-window) count
+ (if evil-vsplit-window-right 'left 'right))
+ (call-interactively
+ (if evil-vsplit-window-right
+ #'evil-window-left
+ #'evil-window-right))
+ (recenter)
+ (when (and (not count) evil-auto-balance-windows)
+ (balance-windows (window-parent)))
+ (if file (evil-edit file)))
+
+;;;###autoload
+(defun +evil*escape (&rest _)
+ "Call `doom/escape' if `evil-force-normal-state' is called interactively."
+ (when (called-interactively-p 'any)
+ (call-interactively #'doom/escape)))
+
+;;;###autoload
+(defun +evil*make-numbered-markers-global (orig-fn char)
+ (or (and (>= char ?2) (<= char ?9))
+ (funcall orig-fn char)))
+
+;;;###autoload
+(defun +evil*set-jump (orig-fn &rest args)
+ "Set a jump point and ensure ORIG-FN doesn't set any new jump points."
+ (evil-set-jump (if (markerp (car args)) (car args)))
+ (let ((evil--jumps-jumping t))
+ (apply orig-fn args)))
+
+;;;###autoload
+(defun +evil*fix-dabbrev-in-minibuffer ()
+ "Make `try-expand-dabbrev' from `hippie-expand' work in minibuffer. See
+`he-dabbrev-beg', so we need to redefine syntax for '/'."
+ (set-syntax-table (let* ((table (make-syntax-table)))
+ (modify-syntax-entry ?/ "." table)
+ table)))
diff --git a/modules/feature/evil/autoload/embrace.el b/modules/feature/evil/autoload/embrace.el
new file mode 100644
index 000000000..82737b825
--- /dev/null
+++ b/modules/feature/evil/autoload/embrace.el
@@ -0,0 +1,33 @@
+;;; feature/evil/autoload/embrace.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +evil--embrace-get-pair (char)
+ (if-let* ((pair (cdr-safe (assoc (string-to-char char) evil-surround-pairs-alist))))
+ pair
+ (if-let* ((pair (assoc-default char embrace--pairs-list)))
+ (if-let* ((real-pair (and (functionp (embrace-pair-struct-read-function pair))
+ (funcall (embrace-pair-struct-read-function pair)))))
+ real-pair
+ (cons (embrace-pair-struct-left pair) (embrace-pair-struct-right pair)))
+ (cons char char))))
+
+;;;###autoload
+(defun +evil--embrace-escaped ()
+ "Backslash-escaped surround character support for embrace."
+ (let ((char (read-char "\\")))
+ (if (eq char 27)
+ (cons "" "")
+ (let ((pair (+evil--embrace-get-pair (string char)))
+ (text (if (sp-point-in-string) "\\\\%s" "\\%s")))
+ (cons (format text (car pair))
+ (format text (cdr pair)))))))
+
+;;;###autoload
+(defun +evil--embrace-latex ()
+ "LaTeX command support for embrace."
+ (cons (format "\\%s{" (read-string "\\")) "}"))
+
+;;;###autoload
+(defun +evil--embrace-elisp-fn ()
+ "Elisp function support for embrace."
+ (cons (format "(%s " (or (read-string "(") "")) ")"))
diff --git a/modules/feature/evil/autoload/evil.el b/modules/feature/evil/autoload/evil.el
index 7e1d6d773..cac9a022e 100644
--- a/modules/feature/evil/autoload/evil.el
+++ b/modules/feature/evil/autoload/evil.el
@@ -1,6 +1,20 @@
;; feature/evil/autoload/evil.el -*- lexical-binding: t; -*-
;;;###if (featurep! :feature evil)
+;;;###autodef
+(defun set-evil-initial-state! (modes state)
+ "Set the initialize STATE of MODES using `evil-set-initial-state'."
+ (declare (indent defun))
+ (after! evil
+ (if (listp modes)
+ (dolist (mode (doom-enlist modes))
+ (evil-set-initial-state mode state))
+ (evil-set-initial-state modes state))))
+
+
+;;
+;;; Commands
+
;;;###autoload
(defun +evil/visual-indent ()
"vnoremap < ")
- (unless (and beg end)
- (setq beg (region-beginning)
- end (region-end)))
- (evil-ex-normal beg end
- (concat "@"
- (single-key-description
- (or macro (read-char "@-"))))))
+ (interactive "")
+ (let ((register (or evil-this-register (read-char)))
+ macro)
+ (cond ((or (and (eq register ?@) (eq evil-last-register ?:))
+ (eq register ?:))
+ (setq macro (lambda () (evil-ex-repeat nil))
+ evil-last-register ?:))
+ ((eq register ?@)
+ (unless evil-last-register
+ (user-error "No previously executed keyboard macro."))
+ (setq macro (evil-get-register evil-last-register t)))
+ ((setq macro (evil-get-register register t)
+ evil-last-register register)))
+ (unless macro
+ (user-error "No macro recorded in %c register" register))
+ (evil-change-state 'normal)
+ (evil-with-single-undo
+ (let ((lines (count-lines beg end)))
+ (message "Applied macro in %c register %d times" register lines)
+ (apply-macro-to-region-lines beg end macro)
+ (message "Applied macro in %c register %d times...DONE" register lines)))))
;;;###autoload (autoload '+evil:retab "feature/evil/autoload/evil" nil t)
(evil-define-operator +evil:retab (&optional beg end)
@@ -94,14 +147,15 @@ evil-window-move-* (e.g. `evil-window-move-far-left')"
(doom/retab beg end))
;;;###autoload (autoload '+evil:narrow-buffer "feature/evil/autoload/evil" nil t)
-(evil-define-command +evil:narrow-buffer (beg end &optional bang)
- "Wrapper around `doom-narrow-buffer'."
+(evil-define-operator +evil:narrow-buffer (beg end &optional bang)
+ "Wrapper around `doom/clone-and-narrow-buffer'."
:move-point nil
(interactive "")
- (doom-narrow-buffer beg end bang))
+ (doom/clone-and-narrow-buffer beg end bang))
-;; --- custom arg handlers ----------------
+;;
+;;; Custom arg handlers
(defvar +evil--flag nil)
@@ -123,7 +177,7 @@ evil-window-move-* (e.g. `evil-window-move-far-left')"
(not (zerop (length arg))))
(condition-case lossage
(let ((pattern (evil-ex-make-substitute-pattern
- (if evil-ex-bang (regexp-quote arg) arg)
+ arg
(or flags (list))))
(range (or (evil-copy-range evil-ex-range)
(evil-range (or beg (line-beginning-position))
@@ -167,18 +221,29 @@ evil-window-move-* (e.g. `evil-window-move-far-left')"
;;;###autoload (autoload '+evil:align "feature/evil/autoload/evil" nil t)
(evil-define-operator +evil:align (beg end pattern &optional bang)
- "Ex interface to `align-regexp'. Accepts vim-style regexps."
- (interactive "/>")
+ "Ex interface to `align-regexp'. PATTERN is a vim-style regexp. If BANG,
+repeat the alignment for all matches (otherwise just the first match on each
+line)."
+ (interactive "/g>")
(align-regexp
beg end
- (concat "\\(\\s-*\\)"
- (if bang
- (regexp-quote pattern)
- (evil-transform-vim-style-regexp pattern)))
- 1 1))
+ (concat "\\(\\s-*\\)" (evil-transform-vim-style-regexp pattern))
+ 1 1 bang))
+
+;;;###autoload (autoload '+evil:align-right "feature/evil/autoload/evil" nil t)
+(evil-define-operator +evil:align-right (beg end pattern &optional bang)
+ "Like `+evil:align', except alignments are right-justified. PATTERN is a
+vim-style regexp. If BANG, repeat the alignment for all matches (otherwise just
+the first match on each line)."
+ (interactive "/g>")
+ (align-regexp
+ beg end
+ (concat "\\(" (evil-transform-vim-style-regexp pattern) "\\)")
+ -1 1 bang))
-;; --- wgrep ------------------------------
+;;
+;;; wgrep
;;;###autoload (autoload '+evil-delete "feature/evil/autoload/evil" nil t)
(evil-define-operator +evil-delete (beg end type register yank-handler)
diff --git a/modules/feature/evil/autoload/files.el b/modules/feature/evil/autoload/files.el
index c7d293281..d94705021 100644
--- a/modules/feature/evil/autoload/files.el
+++ b/modules/feature/evil/autoload/files.el
@@ -1,71 +1,13 @@
;;; feature/evil/autoload/files.el -*- lexical-binding: t; -*-
-(defun +evil--forget-file (old-path &optional new-path)
- "Ensure `recentf', `projectile' and `save-place' forget OLD-PATH."
- (when (bound-and-true-p recentf-mode)
- (when new-path
- (recentf-add-file new-path))
- (recentf-remove-if-non-kept old-path))
- (when (and projectile-mode
- (projectile-project-p)
- (projectile-file-cached-p old-path (projectile-project-root)))
- (projectile-purge-file-from-cache old-path))
- (when (bound-and-true-p save-place-mode)
- (save-place-forget-unreadable-files)))
-
;;;###autoload (autoload '+evil:delete-this-file "feature/evil/autoload/files" nil t)
(evil-define-command +evil:delete-this-file (&optional filename force-p)
"Delete FILENAME (defaults to the file associated with current buffer) and
kills the buffer. If FORCE-P, force the deletion (don't ask for confirmation)."
:repeat nil
(interactive "")
- (let* ((fname (file-truename (or filename (buffer-file-name))))
- (fbase (file-name-sans-extension (file-name-nondirectory fname)))
- (buf (current-buffer)))
- (cond ((not (file-exists-p fname))
- (error "File doesn't exist: %s" fname))
- ((not (or force-p (y-or-n-p (format "Really delete %s?" fbase))))
- (message "Aborted")
- nil)
- (t
- (unwind-protect
- (progn (delete-file fname) t)
- (let ((short-path (file-relative-name fname (doom-project-root))))
- (if (file-exists-p fname)
- (error "Failed to delete %s" short-path)
- ;; Ensures that windows displaying this buffer will be switched
- ;; to real buffers (`doom-real-buffer-p')
- (doom/kill-this-buffer-in-all-windows buf t)
- (+evil--forget-file fname)
- (message "Successfully deleted %s" short-path))))))))
-
-(defun +evil--copy-file (old-path new-path &optional force-p)
- (let* ((new-path (expand-file-name new-path))
- (old-path (file-truename old-path))
- (new-path (apply #'expand-file-name
- (if (or (directory-name-p new-path)
- (file-directory-p new-path))
- (list (file-name-nondirectory old-path) new-path)
- (list new-path))))
- (new-path-dir (file-name-directory new-path))
- (project-root (doom-project-root))
- (short-new-name (if (file-in-directory-p new-path project-root)
- (file-relative-name new-path project-root)
- (abbreviate-file-name new-path))))
- (unless (file-directory-p new-path-dir)
- (make-directory new-path-dir t))
- (when (buffer-modified-p)
- (save-buffer))
- (cond ((equal (file-truename old-path)
- (file-truename new-path))
- (throw 'status 'overwrite-self))
- ((and (file-exists-p new-path)
- (not force-p)
- (not (y-or-n-p (format "File already exists at %s, overwrite?" short-new-name))))
- (throw 'status 'aborted))
- (t
- (copy-file old-path new-path t)
- short-new-name))))
+ (doom/delete-this-file (or filename (file-truename buffer-file-name))
+ force-p))
;;;###autoload (autoload '+evil:move-this-file "feature/evil/autoload/files" nil t)
(evil-define-command +evil:move-this-file (new-path &optional force-p)
@@ -74,18 +16,9 @@ filename modifiers (see `+evil*ex-replace-special-filenames'). If FORCE-P,
overwrite the destination file if it exists, without confirmation."
:repeat nil
(interactive "")
- (pcase (catch 'status
- (let ((old-path (buffer-file-name))
- (new-path (expand-file-name new-path)))
- (when-let* ((dest (+evil--copy-file old-path new-path force-p)))
- (delete-file old-path)
- (kill-this-buffer)
- (find-file new-path)
- (+evil--forget-file old-path new-path)
- (message "File successfully moved to %s" dest))))
- ('overwrite-self (error "Cannot overwrite self"))
- ('aborted (message "Aborted"))
- (_ t)))
+ (when (or (not new-path) (string-empty-p new-path))
+ (user-error "No new path was specified"))
+ (doom/move-this-file new-path force-p))
;;;###autoload (autoload '+evil:copy-this-file "feature/evil/autoload/files" nil nil)
(evil-define-command +evil:copy-this-file (new-path &optional force-p)
@@ -94,10 +27,7 @@ filename modifiers (see `+evil*ex-replace-special-filenames'). If FORCE-P,
overwrite the destination file if it exists, without confirmation."
:repeat nil
(interactive "")
- (pcase (catch 'status
- (when-let* ((dest (+evil--copy-file (buffer-file-name) new-path force-p)))
- (message "File successfully copied to %s" dest)))
- ('overwrite-self (error "Cannot overwrite self"))
- ('aborted (message "Aborted"))
- (_ t)))
+ (when (or (not new-path) (string-empty-p new-path))
+ (user-error "No new path was specified"))
+ (doom/copy-this-file new-path force-p))
diff --git a/modules/feature/evil/autoload/folds.el b/modules/feature/evil/autoload/folds.el
deleted file mode 100644
index b4706abc1..000000000
--- a/modules/feature/evil/autoload/folds.el
+++ /dev/null
@@ -1,96 +0,0 @@
-;;; feature/evil/autoload/folds.el -*- lexical-binding: t; -*-
-
-;; `hideshow' is a decent code folding implementation, but it won't let you
-;; create custom folds. `evil-vimish-fold' offers custom folds, but essentially
-;; ignores any other type of folding (indent or custom markers, which
-;; hs-minor-mode gives you).
-;;
-;; So this is my effort to combine them.
-
-(defun +evil--vimish-fold-p ()
- (cl-some #'vimish-fold--vimish-overlay-p (overlays-at (point))))
-
-(defun +evil--ensure-modes (&rest _)
- "Ensure hs-minor-mode is enabled."
- (unless (bound-and-true-p hs-minor-mode)
- (hs-minor-mode +1)))
-
-(require 'hideshow)
-(advice-add #'hs-toggle-hiding :before #'+evil--ensure-modes)
-(advice-add #'hs-hide-block :before #'+evil--ensure-modes)
-(advice-add #'hs-hide-level :before #'+evil--ensure-modes)
-(advice-add #'hs-show-all :before #'+evil--ensure-modes)
-(advice-add #'hs-hide-all :before #'+evil--ensure-modes)
-
-
-;; --- fold commands ----------------------
-
-;;;###autoload
-(defun +evil-fold-p ()
- (or (+evil--vimish-fold-p)
- (ignore-errors
- (+evil--ensure-modes)
- (hs-already-hidden-p))))
-
-(defmacro +evil-from-eol (&rest body)
- "Perform action after moving to the end of the line."
- `(save-excursion
- (end-of-line)
- ,@body))
-
-;;;###autoload (autoload '+evil:fold-toggle "feature/evil/autoload/folds" nil t)
-(evil-define-command +evil:fold-toggle ()
- (interactive)
- (if (+evil--vimish-fold-p)
- (vimish-fold-toggle)
- (+evil-from-eol (hs-toggle-hiding))))
-
-;;;###autoload (autoload '+evil:fold-open "feature/evil/autoload/folds" nil t)
-(evil-define-command +evil:fold-open ()
- (interactive)
- (if (+evil--vimish-fold-p)
- (vimish-fold-unfold)
- (+evil-from-eol (hs-hide-block))))
-
-;;;###autoload (autoload '+evil:fold-close "feature/evil/autoload/folds" nil t)
-(evil-define-command +evil:fold-close ()
- (interactive)
- (if (+evil--vimish-fold-p)
- (vimish-fold-refold)
- (+evil-from-eol (hs-hide-block))))
-
-;;;###autoload (autoload '+evil:fold-open-all "feature/evil/autoload/folds" nil t)
-(evil-define-command +evil:fold-open-all (&optional level)
- "Open folds at LEVEL (or all folds if LEVEL is nil)."
- (interactive "")
- (vimish-fold-unfold-all)
- (if (integerp level)
- (hs-hide-level (1- level))
- (hs-show-all)))
-
-;;;###autoload (autoload '+evil:fold-close-all "feature/evil/autoload/folds" nil t)
-(evil-define-command +evil:fold-close-all (&optional level)
- "Close folds at LEVEL (or all folds if LEVEL is nil)."
- (interactive "")
- (vimish-fold-refold-all)
- (if (integerp level)
- (hs-hide-level (1- level))
- (hs-hide-all)))
-
-
-;; --- misc -------------------------------
-
-;;;###autoload
-(defun +evil/matchit-or-toggle-fold ()
- "Do what I mean. If on a fold-able element, toggle the fold with
-`hs-toggle-hiding'. Otherwise, if on a delimiter, jump to the matching one with
-`evilmi-jump-items'. If in a magit-status buffer, use `magit-section-toggle'."
- (interactive)
- (ignore-errors
- (call-interactively
- (cond ((eq major-mode 'magit-status-mode)
- #'magit-section-toggle)
- ((+evil-fold-p)
- #'+evil:fold-toggle)
- (t
- #'evilmi-jump-items)))))
diff --git a/modules/feature/evil/config.el b/modules/feature/evil/config.el
index bf0efc808..aaa0a9e49 100644
--- a/modules/feature/evil/config.el
+++ b/modules/feature/evil/config.el
@@ -3,28 +3,21 @@
;; I'm a vimmer at heart. Its modal philosophy suits me better, and this module
;; strives to make Emacs a much better vim than vim was.
-(def-setting! :evil-state (modes state)
- "Set the initialize STATE of MODE using `evil-set-initial-state'."
- (let ((unquoted-modes (doom-unquote modes)))
- (if (listp unquoted-modes)
- `(progn
- ,@(cl-loop for mode in unquoted-modes
- collect `(evil-set-initial-state ',mode ,state)))
- `(evil-set-initial-state ,modes ,state))))
+(defvar +evil-want-o/O-to-continue-comments t
+ "If non-nil, the o/O keys will continue comment lines if the point is on a
+line with a linewise comment.")
-
-;;
-;; evil-mode
-;;
-
-(autoload 'goto-last-change "goto-chg")
-(autoload 'goto-last-change-reverse "goto-chg")
+;; Set these defaults before `evil'; use `defvar' so they can be changed prior
+;; to loading.
+(defvar evil-want-C-u-scroll t)
+(defvar evil-want-C-w-scroll t)
+(defvar evil-want-Y-yank-to-eol t)
(def-package! evil
- :init
- (setq evil-want-C-u-scroll t
- evil-want-visual-char-semi-exclusive t
- evil-want-Y-yank-to-eol t
+ :hook (doom-init-modules . evil-mode)
+ :demand t
+ :preface
+ (setq evil-want-visual-char-semi-exclusive t
evil-magic t
evil-echo-state t
evil-indent-convert-tabs t
@@ -33,211 +26,213 @@
evil-ex-visual-char-range t ; column range for ex commands
evil-insert-skip-empty-lines t
evil-mode-line-format 'nil
+ evil-respect-visual-line-mode t
;; more vim-like behavior
evil-symbol-word-search t
- ;; don't activate mark on shift-click
- shift-select-mode nil)
+ ;; cursor appearance
+ evil-default-cursor '+evil-default-cursor
+ evil-normal-state-cursor 'box
+ evil-emacs-state-cursor '(box +evil-emacs-cursor)
+ evil-insert-state-cursor 'bar
+ evil-visual-state-cursor 'hollow
+ ;; must be set before evil/evil-collection is loaded
+ evil-want-keybinding (not (featurep! +everywhere)))
:config
- (add-hook 'doom-init-hook #'evil-mode)
(evil-select-search-module 'evil-search-module 'evil-search)
- (set! :popup
- '("*evil-registers*" :size 0.3)
- '("*Command Line*" :size 8))
+ (put 'evil-define-key* 'lisp-indent-function 'defun)
- ;; Don't interfere with localleader key
- (define-key evil-motion-state-map "\\" nil)
+ ;; Done in a hook to ensure the popup rules load as late as possible
+ (defun +evil|init-popup-rules ()
+ (set-popup-rules!
+ '(("^\\*evil-registers" :size 0.3)
+ ("^\\*Command Line" :size 8))))
+ (add-hook 'doom-init-modules-hook #'+evil|init-popup-rules)
- ;; Set cursor colors later, once theme is loaded
- (defun +evil*init-cursors (&rest _)
- (setq evil-default-cursor (face-background 'cursor nil t)
- evil-normal-state-cursor 'box
- evil-emacs-state-cursor `(,(face-foreground 'warning) box)
- evil-insert-state-cursor 'bar
- evil-visual-state-cursor 'hollow))
- (advice-add #'load-theme :after #'+evil*init-cursors)
+ ;; Change the cursor color in emacs mode
+ (defvar +evil--default-cursor-color
+ (or (ignore-errors (frame-parameter nil 'cursor-color))
+ "#ffffff"))
- ;; default modes
- (dolist (mode '(tabulated-list-mode view-mode comint-mode term-mode calendar-mode Man-mode grep-mode))
- (evil-set-initial-state mode 'emacs))
- (dolist (mode '(help-mode debugger-mode))
- (evil-set-initial-state mode 'normal))
+ (defun +evil-default-cursor () (set-cursor-color +evil--default-cursor-color))
+ (defun +evil-emacs-cursor () (set-cursor-color (face-foreground 'warning)))
- ;; make `try-expand-dabbrev' from `hippie-expand' work in minibuffer
- ;; @see `he-dabbrev-beg', so we need re-define syntax for '/'
- (defun minibuffer-inactive-mode-hook-setup ()
- (set-syntax-table (let* ((table (make-syntax-table)))
- (modify-syntax-entry ?/ "." table)
- table)))
- (add-hook 'minibuffer-inactive-mode-hook #'minibuffer-inactive-mode-hook-setup)
+ (defun +evil|update-cursor-color ()
+ (setq +evil--default-cursor-color (face-background 'cursor)))
+ (add-hook 'doom-load-theme-hook #'+evil|update-cursor-color)
+
+ (defun +evil|update-shift-width ()
+ (setq evil-shift-width tab-width))
+ (add-hook 'after-change-major-mode-hook #'+evil|update-shift-width t)
;; --- keybind fixes ----------------------
- (map! (:after wgrep
- ;; a wrapper that invokes `wgrep-mark-deletion' across lines
- ;; you use `evil-delete' on.
- :map wgrep-mode-map [remap evil-delete] #'+evil-delete)
+ (after! wgrep
+ ;; A wrapper that invokes `wgrep-mark-deletion' across lines you use
+ ;; `evil-delete' in wgrep buffers.
+ (define-key wgrep-mode-map [remap evil-delete] #'+evil-delete))
- ;; replace native folding commands
- [remap evil-toggle-fold] #'+evil:fold-toggle
- [remap evil-close-fold] #'+evil:fold-close
- [remap evil-open-fold] #'+evil:fold-open
- [remap evil-open-fold-rec] #'+evil:fold-open
- [remap evil-close-folds] #'+evil:fold-close-all
- [remap evil-open-folds] #'+evil:fold-open-all)
+ (defun +evil|disable-highlights ()
+ "Disable ex search buffer highlights."
+ (when (evil-ex-hl-active-p 'evil-ex-search)
+ (evil-ex-nohighlight)
+ t))
+ (add-hook 'doom-escape-hook #'+evil|disable-highlights)
;; --- evil hacks -------------------------
- (defvar +evil-esc-hook '(t)
- "A hook run after ESC is pressed in normal mode (invoked by
-`evil-force-normal-state'). If any hook returns non-nil, all hooks after it are
-ignored.")
-
- (defun +evil*attach-escape-hook ()
- "Run the `+evil-esc-hook'."
- (cond ((minibuffer-window-active-p (minibuffer-window))
- ;; quit the minibuffer if open.
- (abort-recursive-edit))
- ((evil-ex-hl-active-p 'evil-ex-search)
- ;; disable ex search buffer highlights.
- (evil-ex-nohighlight))
- (t
- ;; Run all escape hooks. If any returns non-nil, then stop there.
- (run-hook-with-args-until-success '+evil-esc-hook))))
- (advice-add #'evil-force-normal-state :after #'+evil*attach-escape-hook)
-
- (defun +evil*restore-normal-state-on-windmove (orig-fn &rest args)
- "If in anything but normal or motion mode when moving to another window,
-restore normal mode. This prevents insert state from bleeding into other modes
-across windows."
- (unless (memq evil-state '(normal motion emacs))
- (evil-normal-state +1))
- (apply orig-fn args))
- (advice-add #'windmove-do-window-select :around #'+evil*restore-normal-state-on-windmove)
-
- (defun +evil*static-reindent (orig-fn &rest args)
- "Don't move cursor on indent."
- (save-excursion (apply orig-fn args)))
+ (defun +evil|display-vimlike-save-message ()
+ "Shorter, vim-esque save messages."
+ (message "\"%s\" %dL, %dC written"
+ (if buffer-file-name
+ (file-relative-name (file-truename buffer-file-name) (doom-project-root))
+ (buffer-name))
+ (count-lines (point-min) (point-max))
+ (buffer-size)))
+ (unless noninteractive
+ (setq save-silently t)
+ (add-hook 'after-save-hook #'+evil|display-vimlike-save-message))
+ ;; Make ESC (from normal mode) the universal escaper. See `doom-escape-hook'.
+ (advice-add #'evil-force-normal-state :after #'+evil*escape)
+ ;; Don't move cursor when indenting
(advice-add #'evil-indent :around #'+evil*static-reindent)
+ ;; monkey patch `evil-ex-replace-special-filenames' to improve support for
+ ;; file modifiers like %:p:h. This adds support for most of vim's modifiers,
+ ;; and one custom one: %:P (expand to the project root).
+ (advice-add #'evil-ex-replace-special-filenames :override #'+evil*resolve-vim-path)
- ;; monkey patch `evil-ex-replace-special-filenames' to add more ex
- ;; substitution flags to evil-mode
- (advice-add #'evil-ex-replace-special-filenames :override #'doom-resolve-vim-path)
+ ;; make `try-expand-dabbrev' (from `hippie-expand') work in minibuffer
+ (add-hook 'minibuffer-inactive-mode-hook #'+evil*fix-dabbrev-in-minibuffer)
+ ;; Focus and recenter new splits
+ (advice-add #'evil-window-split :override #'+evil*window-split)
+ (advice-add #'evil-window-vsplit :override #'+evil*window-vsplit)
+
+ ;; Integrate evil's jump-list into some navigational commands
+ (advice-add #'counsel-git-grep-action :around #'+evil*set-jump)
+ (advice-add #'helm-ag--find-file-action :around #'+evil*set-jump)
+ (advice-add #'xref-push-marker-stack :around #'+evil*set-jump)
+
+ ;; In evil, registers 2-9 are buffer-local. In vim, they're global, so...
+ (advice-add #'evil-global-marker-p :around #'+evil*make-numbered-markers-global)
+
+ ;; Make o/O continue comments (see `+evil-want-o/O-to-continue-comments')
+ (advice-add #'evil-open-above :around #'+evil*insert-newline-above-and-respect-comments)
+ (advice-add #'evil-open-below :around #'+evil*insert-newline-below-and-respect-comments)
+
+ ;; Recenter screen after most searches
+ (advice-add! '(evil-visualstar/begin-search-forward
+ evil-visualstar/begin-search-backward
+ evil-ex-search-word-backward
+ evil-ex-search-word-backward
+ evil-ex-search-forward
+ evil-ex-search-backward)
+ :after #'doom*recenter)
+
+ ;; --- custom interactive codes -----------
;; These arg types will highlight matches in the current buffer
(evil-ex-define-argument-type buffer-match :runner +evil-ex-buffer-match)
(evil-ex-define-argument-type global-match :runner +evil-ex-global-match)
+ ;; Other commands can make use of this
+ (evil-define-interactive-code "/>"
+ :ex-arg buffer-match (list (if (evil-ex-p) evil-ex-argument)))
+ (evil-define-interactive-code "/g>"
+ :ex-arg global-match (list (if (evil-ex-p) evil-ex-argument)))
;; By default :g[lobal] doesn't highlight matches in the current buffer. I've
;; got to write my own argument type and interactive code to get it to do so.
(evil-ex-define-argument-type global-delim-match :runner +evil-ex-global-delim-match)
-
(dolist (sym '(evil-ex-global evil-ex-global-inverted))
(evil-set-command-property sym :ex-arg 'global-delim-match))
- ;; Other commands can make use of this
- (evil-define-interactive-code "/>"
- :ex-arg buffer-match (list (when (evil-ex-p) evil-ex-argument)))
- (evil-define-interactive-code "/g>"
- :ex-arg global-match (list (when (evil-ex-p) evil-ex-argument)))
-
;; Forward declare these so that ex completion works, even if the autoloaded
;; functions aren't loaded yet.
(evil-set-command-properties
- '+evil:align :move-point t :ex-arg 'buffer-match :ex-bang t :evil-mc t :keep-visual t :suppress-operator t)
- (evil-set-command-properties
- '+evil:mc :move-point nil :ex-arg 'global-match :ex-bang t :evil-mc t)
+ '+evil:align :move-point t :ex-arg 'buffer-match :ex-bang t :keep-visual t :suppress-operator t)
- ;; Move to new split -- setting `evil-split-window-below' &
- ;; `evil-vsplit-window-right' to non-nil mimics this, but that doesn't update
- ;; window history. That means when you delete a new split, Emacs leaves you on
- ;; the 2nd to last window on the history stack, which is jarring.
- (defun +evil*window-follow (&rest _) (evil-window-down 1))
- (defun +evil*window-vfollow (&rest _) (evil-window-right 1))
- (advice-add #'evil-window-split :after #'+evil*window-follow)
- (advice-add #'evil-window-vsplit :after #'+evil*window-vfollow))
+ ;; `evil-collection'
+ (when (featurep! +everywhere)
+ (load! "+everywhere"))
+
+ ;; Custom evil ex commands
+ (load! "+commands"))
;;
-;; Plugins
-;;
+;; Packages
(def-package! evil-commentary
- :commands (evil-commentary evil-commentary-yank evil-commentary-line)
+ :commands (evil-commentary
+ evil-commentary-yank
+ evil-commentary-yank-line
+ evil-commentary-line)
:config (evil-commentary-mode 1))
(def-package! evil-easymotion
- :after evil-snipe
- :commands evilem-create)
+ :commands (evilem-create evilem-default-keybindings)
+ :config
+ ;; Use evil-search backend, instead of isearch
+ (evilem-make-motion evilem-motion-search-next #'evil-ex-search-next
+ :bind ((evil-ex-search-highlight-all nil)))
+ (evilem-make-motion evilem-motion-search-previous #'evil-ex-search-previous
+ :bind ((evil-ex-search-highlight-all nil)))
+
+ (evilem-make-motion evilem-motion-search-word-forward #'evil-ex-search-word-forward
+ :bind ((evil-ex-search-highlight-all nil)))
+ (evilem-make-motion evilem-motion-search-word-backward #'evil-ex-search-word-backward
+ :bind ((evil-ex-search-highlight-all nil))))
(def-package! evil-embrace
- :after evil-surround
+ :commands (embrace-add-pair embrace-add-pair-regexp)
+ :hook (LaTeX-mode . embrace-LaTeX-mode-hook)
+ :hook (org-mode . embrace-org-mode-hook)
+ :hook ((ruby-mode enh-ruby-mode) . embrace-ruby-mode-hook)
+ :hook (emacs-lisp-mode . embrace-emacs-lisp-mode-hook)
+ :hook ((emacs-lisp-mode lisp-mode) . +evil|embrace-lisp-mode-hook)
+ :hook ((org-mode LaTeX-mode) . +evil|embrace-latex-mode-hook)
+ :init
+ (after! evil-surround
+ (evil-embrace-enable-evil-surround-integration))
:config
(setq evil-embrace-show-help-p nil)
- (evil-embrace-enable-evil-surround-integration)
- (defun +evil--embrace-get-pair (char)
- (if-let* ((pair (cdr-safe (assoc (string-to-char char) evil-surround-pairs-alist))))
- pair
- (if-let* ((pair (assoc-default char embrace--pairs-list)))
- (if-let* ((real-pair (and (functionp (embrace-pair-struct-read-function pair))
- (funcall (embrace-pair-struct-read-function pair)))))
- real-pair
- (cons (embrace-pair-struct-left pair) (embrace-pair-struct-right pair)))
- (cons char char))))
+ (defun +evil|embrace-latex-mode-hook ()
+ (embrace-add-pair-regexp ?l "\\[a-z]+{" "}" #'+evil--embrace-latex))
- (defun +evil--embrace-escaped ()
- "Backslash-escaped surround character support for embrace."
- (let ((char (read-char "\\")))
- (if (eq char 27)
- (cons "" "")
- (let ((pair (+evil--embrace-get-pair (string char)))
- (text (if (sp-point-in-string) "\\\\%s" "\\%s")))
- (cons (format text (car pair))
- (format text (cdr pair)))))))
-
- (defun +evil--embrace-latex ()
- "LaTeX command support for embrace."
- (cons (format "\\%s{" (read-string "\\")) "}"))
-
- (defun +evil--embrace-elisp-fn ()
- "Elisp function support for embrace."
- (cons (format "(%s " (or (read-string "(") "")) ")"))
+ (defun +evil|embrace-lisp-mode-hook ()
+ (push (cons ?f (make-embrace-pair-struct
+ :key ?f
+ :read-function #'+evil--embrace-elisp-fn
+ :left-regexp "([^ ]+ "
+ :right-regexp ")"))
+ embrace--pairs-list))
;; Add escaped-sequence support to embrace
- (push (cons ?\\ (make-embrace-pair-struct
- :key ?\\
- :read-function #'+evil--embrace-escaped
- :left-regexp "\\[[{(]"
- :right-regexp "\\[]})]"))
- (default-value 'embrace--pairs-list))
-
- ;; Add extra pairs
- (add-hook 'LaTeX-mode-hook #'embrace-LaTeX-mode-hook)
- (add-hook 'org-mode-hook #'embrace-org-mode-hook)
- (add-hook! emacs-lisp-mode
- (embrace-add-pair ?\` "`" "'"))
- (add-hook! (emacs-lisp-mode lisp-mode)
- (embrace-add-pair-regexp ?f "([^ ]+ " ")" #'+evil--embrace-elisp-fn))
- (add-hook! (org-mode LaTeX-mode)
- (embrace-add-pair-regexp ?l "\\[a-z]+{" "}" #'+evil--embrace-latex)))
+ (setf (alist-get ?\\ (default-value 'embrace--pairs-list))
+ (make-embrace-pair-struct
+ :key ?\\
+ :read-function #'+evil--embrace-escaped
+ :left-regexp "\\[[{(]"
+ :right-regexp "\\[]})]")))
(def-package! evil-escape
- :commands evil-escape-mode
+ :commands (evil-escape)
+ :after-call (evil-normal-state-exit-hook)
:init
(setq evil-escape-excluded-states '(normal visual multiedit emacs motion)
- evil-escape-excluded-major-modes '(neotree-mode)
+ evil-escape-excluded-major-modes '(neotree-mode treemacs-mode term-mode vterm-mode)
evil-escape-key-sequence "jk"
evil-escape-delay 0.25)
- (add-hook 'doom-post-init-hook #'evil-escape-mode)
+ (evil-define-key* '(insert replace visual operator) 'global "\C-g" #'evil-escape)
:config
;; no `evil-escape' in minibuffer
- (push #'minibufferp evil-escape-inhibit-functions)
- (map! :irvo "C-g" #'evil-escape))
+ (add-hook 'evil-escape-inhibit-functions #'minibufferp)
+ ;; so that evil-escape-mode-hook runs, and can be toggled by evil-mc
+ (evil-escape-mode +1))
(def-package! evil-exchange
@@ -247,16 +242,25 @@ across windows."
(when evil-exchange--overlays
(evil-exchange-cancel)
t))
- (add-hook '+evil-esc-hook #'+evil|escape-exchange))
+ (add-hook 'doom-escape-hook #'+evil|escape-exchange))
+
+
+(def-package! evil-numbers
+ :commands (evil-numbers/inc-at-pt evil-numbers/dec-at-pt))
(def-package! evil-matchit
- :commands (evilmi-jump-items evilmi-text-object global-evil-matchit-mode)
+ :commands (evilmi-jump-items global-evil-matchit-mode
+ evilmi-outer-text-object evilmi-inner-text-object)
:config (global-evil-matchit-mode 1)
:init
- (map! [remap evil-jump-item] #'evilmi-jump-items
- :textobj "%" #'evilmi-text-object #'evilmi-text-object)
+ (global-set-key [remap evil-jump-item] #'evilmi-jump-items)
+ (define-key evil-inner-text-objects-map "%" #'evilmi-inner-text-object)
+ (define-key evil-outer-text-objects-map "%" #'evilmi-outer-text-object)
:config
+ ;; Fixes #519 where d% wouldn't leave a dangling end-parenthesis
+ (evil-set-command-properties 'evilmi-jump-items :type 'inclusive :jump t)
+
(defun +evil|simple-matchit ()
"A hook to force evil-matchit to favor simple bracket jumping. Helpful when
the new algorithm is confusing, like in python or ruby."
@@ -264,69 +268,19 @@ the new algorithm is confusing, like in python or ruby."
(add-hook 'python-mode-hook #'+evil|simple-matchit))
-(def-package! evil-multiedit
- :commands (evil-multiedit-match-all
- evil-multiedit-match-and-next
- evil-multiedit-match-and-prev
- evil-multiedit-match-symbol-and-next
- evil-multiedit-match-symbol-and-prev
- evil-multiedit-toggle-or-restrict-region
- evil-multiedit-next
- evil-multiedit-prev
- evil-multiedit-abort
- evil-multiedit-ex-match))
-
-
-(def-package! evil-mc
- :commands (evil-mc-make-cursor-here evil-mc-make-all-cursors
- evil-mc-undo-all-cursors evil-mc-pause-cursors
- evil-mc-resume-cursors evil-mc-make-and-goto-first-cursor
- evil-mc-make-and-goto-last-cursor
- evil-mc-make-cursor-move-next-line
- evil-mc-make-cursor-move-prev-line evil-mc-make-cursor-at-pos
- evil-mc-has-cursors-p evil-mc-make-and-goto-next-cursor
- evil-mc-skip-and-goto-next-cursor evil-mc-make-and-goto-prev-cursor
- evil-mc-skip-and-goto-prev-cursor evil-mc-make-and-goto-next-match
- evil-mc-skip-and-goto-next-match evil-mc-skip-and-goto-next-match
- evil-mc-make-and-goto-prev-match evil-mc-skip-and-goto-prev-match)
- :init
- (defvar evil-mc-key-map (make-sparse-keymap))
- :config
- (global-evil-mc-mode +1)
-
- ;; Add custom commands to whitelisted commands
- (dolist (fn '(doom/deflate-space-maybe doom/inflate-space-maybe
- doom/backward-to-bol-or-indent doom/forward-to-last-non-comment-or-eol
- doom/backward-kill-to-bol-and-indent doom/newline-and-indent))
- (push (cons fn '((:default . evil-mc-execute-default-call)))
- evil-mc-custom-known-commands))
-
- ;; disable evil-escape in evil-mc; causes unwanted text on invocation
- (push 'evil-escape-mode evil-mc-incompatible-minor-modes)
-
- (defun +evil|escape-multiple-cursors ()
- "Clear evil-mc cursors and restore state."
- (when (evil-mc-has-cursors-p)
- (evil-mc-undo-all-cursors)
- (evil-mc-resume-cursors)
- t))
- (add-hook '+evil-esc-hook #'+evil|escape-multiple-cursors))
-
-
(def-package! evil-snipe
:commands (evil-snipe-mode evil-snipe-override-mode
evil-snipe-local-mode evil-snipe-override-local-mode)
+ :after-call pre-command-hook
:init
(setq evil-snipe-smart-case t
evil-snipe-scope 'line
evil-snipe-repeat-scope 'visible
- evil-snipe-char-fold t
- evil-snipe-disabled-modes '(magit-mode elfeed-show-mode elfeed-search-mode)
- evil-snipe-aliases '((?\[ "[[{(]")
- (?\] "[]})]")
- (?\; "[;:]")))
- (add-hook 'doom-post-init-hook #'evil-snipe-mode)
- (add-hook 'doom-post-init-hook #'evil-snipe-override-mode))
+ evil-snipe-char-fold t)
+ :config
+ (add-to-list 'evil-snipe-disabled-modes 'Info-mode nil #'eq)
+ (evil-snipe-mode +1)
+ (evil-snipe-override-mode +1))
(def-package! evil-surround
@@ -337,108 +291,19 @@ the new algorithm is confusing, like in python or ruby."
:config (global-evil-surround-mode 1))
-(def-package! evil-vimish-fold
- :commands evil-vimish-fold-mode
- :init
- (setq vimish-fold-dir (concat doom-cache-dir "vimish-fold/")
- vimish-fold-indication-mode 'right-fringe)
- (add-hook 'doom-post-init-hook #'evil-vimish-fold-mode t))
-
-
-;; Without `evil-visualstar', * and # grab the word at point and search, no
-;; matter what mode you're in. I want to be able to visually select a region and
-;; search for other occurrences of it.
+;; Allows you to use the selection for * and #
(def-package! evil-visualstar
- :commands (global-evil-visualstar-mode
- evil-visualstar/begin-search
+ :commands (evil-visualstar/begin-search
evil-visualstar/begin-search-forward
evil-visualstar/begin-search-backward)
:init
- (map! :v "*" #'evil-visualstar/begin-search-forward
- :v "#" #'evil-visualstar/begin-search-backward)
- :config
- (global-evil-visualstar-mode 1))
+ (evil-define-key* 'visual 'global
+ "*" #'evil-visualstar/begin-search-forward
+ "#" #'evil-visualstar/begin-search-backward))
;;
;; Text object plugins
-;;
-(def-package! evil-args
- :commands (evil-inner-arg evil-outer-arg
- evil-forward-arg evil-backward-arg
- evil-jump-out-args))
-
-
-(def-package! evil-indent-plus
- :commands (evil-indent-plus-i-indent
- evil-indent-plus-a-indent
- evil-indent-plus-i-indent-up
- evil-indent-plus-a-indent-up
- evil-indent-plus-i-indent-up-down
- evil-indent-plus-a-indent-up-down))
-
-
-(def-package! evil-textobj-anyblock
- :commands (evil-textobj-anyblock-inner-block evil-textobj-anyblock-a-block))
-
-
-;;
-;; Multiple cursors compatibility (for the plugins that use it)
-;;
-
-;; mc doesn't play well with evil, this attempts to assuage some of its problems
-;; so that any plugins that depend on multiple-cursors (which I have no control
-;; over) can still use it in relative safety.
-(after! multiple-cursors-core
- (map! :map mc/keymap :ne "" #'mc/keyboard-quit)
-
- (defvar +evil--mc-compat-evil-prev-state nil)
- (defvar +evil--mc-compat-mark-was-active nil)
-
- (defsubst +evil--visual-or-normal-p ()
- "True if evil mode is enabled, and we are in normal or visual mode."
- (and (bound-and-true-p evil-mode)
- (not (memq evil-state '(insert emacs)))))
-
- (defun +evil|mc-compat-switch-to-emacs-state ()
- (when (+evil--visual-or-normal-p)
- (setq +evil--mc-compat-evil-prev-state evil-state)
- (when (region-active-p)
- (setq +evil--mc-compat-mark-was-active t))
- (let ((mark-before (mark))
- (point-before (point)))
- (evil-emacs-state 1)
- (when (or +evil--mc-compat-mark-was-active (region-active-p))
- (goto-char point-before)
- (set-mark mark-before)))))
-
- (defun +evil|mc-compat-back-to-previous-state ()
- (when +evil--mc-compat-evil-prev-state
- (unwind-protect
- (case +evil--mc-compat-evil-prev-state
- ((normal visual) (evil-force-normal-state))
- (t (message "Don't know how to handle previous state: %S"
- +evil--mc-compat-evil-prev-state)))
- (setq +evil--mc-compat-evil-prev-state nil)
- (setq +evil--mc-compat-mark-was-active nil))))
-
- (add-hook 'multiple-cursors-mode-enabled-hook '+evil|mc-compat-switch-to-emacs-state)
- (add-hook 'multiple-cursors-mode-disabled-hook '+evil|mc-compat-back-to-previous-state)
-
- (defun +evil|mc-evil-compat-rect-switch-state ()
- (if rectangular-region-mode
- (+evil|mc-compat-switch-to-emacs-state)
- (setq +evil--mc-compat-evil-prev-state nil)))
-
- ;; When running edit-lines, point will return (position + 1) as a
- ;; result of how evil deals with regions
- (defadvice mc/edit-lines (before change-point-by-1 activate)
- (when (+evil--visual-or-normal-p)
- (if (> (point) (mark))
- (goto-char (1- (point)))
- (push-mark (1- (mark))))))
-
- (add-hook 'rectangular-region-mode-hook '+evil|mc-evil-compat-rect-switch-state)
-
- (defvar mc--default-cmds-to-run-once nil))
+(def-package! exato
+ :commands (evil-outer-xml-attr evil-inner-xml-attr))
diff --git a/modules/feature/evil/packages.el b/modules/feature/evil/packages.el
index 4194b9bd1..050378306 100644
--- a/modules/feature/evil/packages.el
+++ b/modules/feature/evil/packages.el
@@ -10,11 +10,20 @@
(package! evil-exchange)
(package! evil-indent-plus)
(package! evil-matchit)
-(package! evil-mc)
-(package! evil-multiedit)
(package! evil-numbers)
(package! evil-textobj-anyblock)
(package! evil-snipe)
(package! evil-surround)
-(package! evil-vimish-fold)
(package! evil-visualstar)
+(package! exato)
+
+
+;;
+(when (featurep! +everywhere)
+ ;; `evil-collection-neotree' uses the `neotree-make-executor' macro, but this
+ ;; requires neotree be available during byte-compilation (while installing).
+ (when (featurep! :ui neotree)
+ (package! neotree)
+ (autoload 'neotree-make-executor "neotree" nil nil 'macro))
+
+ (package! evil-collection))
diff --git a/modules/feature/evil/test/autoload-files.el b/modules/feature/evil/test/autoload-files.el
deleted file mode 100644
index b14185cd1..000000000
--- a/modules/feature/evil/test/autoload-files.el
+++ /dev/null
@@ -1,46 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; feature/evil/test/autoload-files.el
-
-(defmacro with-temp-files!! (src dest &rest body)
- "Run FORMS in the context of a temporary package setup (as in, it won't
-affects your Emacs packages)."
- (declare (indent 2) (doc-string 3))
- `(let ((it ,src)
- (other ,dest))
- (with-temp-file it
- (insert "Hello world"))
- (unwind-protect
- (progn
- (should (file-exists-p it))
- (find-file-literally it)
- (should (equal (buffer-string) "Hello world"))
- (should (equal (buffer-file-name) it))
- (let ((inhibit-message (not doom-debug-mode)))
- ,@body))
- (ignore-errors (delete-file it))
- ,(if dest `(ignore-errors (delete-file other))))))
-
-;;
-(def-test! move-this-file
- ":mv"
- (with-temp-files!! "/tmp/doom-buffer" "/tmp/doom-buffer-new"
- (should-error (+evil:move-this-file it))
- (should (+evil:move-this-file other t))
- (should (file-exists-p other))
- (should (not (file-exists-p it)))))
-
-(def-test! copy-this-file
- ":cp"
- (with-temp-files!! "/tmp/doom-buffer-2" "/tmp/doom-buffer-2-new"
- (should-error (+evil:copy-this-file it))
- (should (+evil:copy-this-file other t))
- (should (file-exists-p other))
- (should (file-exists-p it))))
-
-(def-test! delete-this-file
- ":rm"
- (with-temp-files!! "/tmp/doom-buffer-3" nil
- (should-error (+evil:delete-this-file "this-file-does-not-exist"))
- (should (+evil:delete-this-file nil t))
- (should (not (file-exists-p it)))))
-
diff --git a/modules/feature/evil/test/evil.el b/modules/feature/evil/test/evil.el
deleted file mode 100644
index ffe6f92a1..000000000
--- a/modules/feature/evil/test/evil.el
+++ /dev/null
@@ -1,22 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; feature/evil/test/evil.el
-
-(require! :feature evil)
-
-;;
-;; `evil-ex-replace-special-filenames'
-;; NOTE The majority of this function is tested in core/test/core-lib.el, this
-;; only tests the evil-mode-specific functionality.
-(def-test! file-modifiers
- (cl-flet ((do-it #'evil-ex-replace-special-filenames))
- (let ((buffer-file-name "~/.emacs.d/test/modules/feature/test-evil.el")
- (default-directory "~/.emacs.d/test/modules/"))
- (should (equal (do-it "%:s?e?x?") "fxature/test-evil.el"))
- (should (equal (do-it "%:gs?e?x?") "fxaturx/txst-xvil.xl")))))
-
-(def-test! empty-file-modifiers
- (cl-flet ((do-it #'evil-ex-replace-special-filenames))
- (let (buffer-file-name default-directory)
- (should (equal (do-it "%:s?e?x?") ""))
- (should (equal (do-it "%:gs?e?x?") "")))))
-
diff --git a/modules/feature/evil/test/test-evil.el b/modules/feature/evil/test/test-evil.el
new file mode 100644
index 000000000..41fe1b3a9
--- /dev/null
+++ b/modules/feature/evil/test/test-evil.el
@@ -0,0 +1,70 @@
+;; -*- no-byte-compile: t; -*-
+;;; feature/evil/test/test-evil.el
+
+(describe "feature/evil"
+ :var (resv project-root)
+ (before-all
+ (require! :feature evil)
+ (require 'evil)
+ (load! "../autoload/evil"))
+ (after-all
+ (unload-feature 'evil t))
+ (before-each
+ (fset 'resv #'+evil*resolve-vim-path)
+ (spy-on 'doom-project-root :and-call-fake (lambda () project-root)))
+
+ ;; `evil-ex-replace-special-filenames' / `+evil*resolve-vim-path'
+ (describe "file modifiers"
+ (it "supports basic vim file modifiers"
+ (let ((buffer-file-name "~/.emacs.d/test/modules/feature/test-evil.el")
+ (default-directory "~/.emacs.d/test/modules/")
+ (project-root "~/.emacs.d/"))
+ (expect (resv "%") :to-equal "feature/test-evil.el")
+ (expect (resv "%:r") :to-equal "feature/test-evil")
+ (expect (resv "%:r.elc") :to-equal "feature/test-evil.elc")
+ (expect (resv "%:e") :to-equal "el")
+ (expect (resv "%:p") :to-equal (expand-file-name buffer-file-name))
+ (expect (resv "%:h") :to-equal "feature")
+ (expect (resv "%:t") :to-equal "test-evil.el")
+ (expect (resv "%:.") :to-equal "feature/test-evil.el")
+ (expect (resv "%:~") :to-equal "~/.emacs.d/test/modules/feature/test-evil.el")
+ (expect (file-truename (resv "%:p"))
+ :to-equal (file-truename buffer-file-name))))
+
+ (it "supports nested vim file modifiers"
+ (let ((buffer-file-name "~/vim/src/version.c")
+ (default-directory "~/vim/")
+ (project-root "~/vim/"))
+ (expect (resv "%:p") :to-equal (expand-file-name "~/vim/src/version.c"))
+ (expect (resv "%:p:.") :to-equal "src/version.c")
+ (expect (resv "%:p:~") :to-equal "~/vim/src/version.c")
+ (expect (resv "%:h") :to-equal "src")
+ (expect (resv "%:p:h") :to-equal (expand-file-name "~/vim/src"))
+ (expect (resv "%:p:h:h") :to-equal (expand-file-name "~/vim"))
+ (expect (resv "%:t") :to-equal "version.c")
+ (expect (resv "%:p:t") :to-equal "version.c")
+ (expect (resv "%:r") :to-equal "src/version")
+ (expect (resv "%:p:r") :to-equal (expand-file-name "~/vim/src/version"))
+ (expect (resv "%:t:r") :to-equal "version")))
+
+ (it "cleans up empty file modifiers"
+ (let (buffer-file-name default-directory)
+ (expect (resv "%") :to-equal "")
+ (expect (resv "%:r") :to-equal "")
+ (expect (resv "%:e") :to-equal "")
+ (expect (resv "%:h") :to-equal "")
+ (expect (resv "%:t") :to-equal "")
+ (expect (resv "%:.") :to-equal "")
+ (expect (resv "%:~") :to-equal "")
+ (expect (resv "%:P") :to-equal "")))
+
+ (it "supports substitution modifiers"
+ (let ((buffer-file-name "~/.emacs.d/test/modules/feature/test-evil.el")
+ (default-directory "~/.emacs.d/test/modules/"))
+ (expect (resv "%:s?e?x?") :to-equal "fxature/test-evil.el")
+ (expect (resv "%:gs?e?x?") :to-equal "fxaturx/txst-xvil.xl")))
+
+ (it "cleans up empty substitution modifiers"
+ (let (buffer-file-name default-directory)
+ (expect (resv "%:s?e?x?") :to-equal "")
+ (expect (resv "%:gs?e?x?") :to-equal "")))))
diff --git a/modules/feature/file-templates/README.org b/modules/feature/file-templates/README.org
new file mode 100644
index 000000000..8e47464eb
--- /dev/null
+++ b/modules/feature/file-templates/README.org
@@ -0,0 +1,79 @@
+#+TITLE: feature/file-templates
+#+DATE: February 11, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC_2:noexport:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Prerequisites][Prerequisites]]
+- [[Usage][Usage]]
+ - [[Inserting OSS licenses][Inserting OSS licenses]]
+- [[Configuration][Configuration]]
+ - [[Registering a new file template][Registering a new file template]]
+ - [[Changing existing file templates][Changing existing file templates]]
+ - [[Adding new OSS licenses][Adding new OSS licenses]]
+- [[Troubleshooting][Troubleshooting]]
+- [[Appendix][Appendix]]
+ - [[API][API]]
+ - [[Commands][Commands]]
+ - [[Variables][Variables]]
+
+* Description
+This module adds file templates for blank files, powered by yasnippet.
+
+** Module Flags
+This module provides no flags.
+
+** Plugins
+This module installs no plugins.
+
+* Prerequisites
+This module has no prerequisites.
+
+* Usage
+File templates are automatically expanded when opening empty files.
+
+They are also regular yasnippet snippets, which can be expanded by typing their
+trigger and pressing =TAB=. By convention, the triggers for file templates are
+prefixed with two underscores ~__~.
+
+** Inserting OSS licenses
+A special command is available for inserting software licenses: ~M-x
++file-templates/insert-license~.
+
+#+begin_quote
+Licenses with a ~-bp~ suffix are boilerplate templates; shorter versions meant
+for comment headers in code.
+#+end_quote
+
+* Configuration
+** TODO Registering a new file template
+** TODO Changing existing file templates
+** Adding new OSS licenses
+The ~+file-templates/insert-license~ command searches for snippets under
+~text-mode~ that are named ~__license-ABC~, where ABC is the short name of the
+license. e.g. ~__license-mit~.
+
+So long as these files exist, ~+file-templates/insert-license~ will recognize
+them.
+
+* Troubleshooting
+If a file template isn't expanding where you expect it to, run ~M-x
++file-templates/debug~. This will report to you what file template rule would
+apply for the correct file.
+
+* Appendix
+** API
++ ~set-file-template! PRED &rest PLIST~
++ ~set-file-templates! &rest TEMPLATES~
+
+** Commands
++ ~+file-templates/insert-license~
++ ~+file-templates/debug~
+
+** Variables
++ ~+file-templates-dir~
++ ~+file-templates-default-trigger~
++ ~+file-templates-alist~
diff --git a/modules/feature/file-templates/autoload.el b/modules/feature/file-templates/autoload.el
new file mode 100644
index 000000000..4bcd8a1ad
--- /dev/null
+++ b/modules/feature/file-templates/autoload.el
@@ -0,0 +1,129 @@
+;;; feature/file-templates/autoload.el -*- lexical-binding: t; -*-
+
+(defun +file-templates--set (pred plist)
+ (if (null (car-safe plist))
+ (setq +file-templates-alist
+ (delq (assoc pred +file-templates-alist)
+ +file-templates-alist))
+ (push `(,pred ,@plist) +file-templates-alist)))
+
+;;;###autodef
+(defun set-file-template! (pred &rest plist)
+ "Register a file template.
+
+PRED can either be a regexp string or a major mode symbol. PLIST may contain
+these properties:
+
+ :when FUNCTION
+ Provides a secondary predicate. This function takes no arguments and is
+ executed from within the target buffer. If it returns nil, this rule will be
+ skipped over.
+ :trigger STRING|FUNCTION
+ If a string, this is the yasnippet trigger keyword used to trigger the
+ target snippet.
+ If a function, this function will be run in the context of the buffer to
+ insert a file template into. It is given no arguments and must insert text
+ into the current buffer manually.
+ If omitted, `+file-templates-default-trigger' is used.
+ :mode SYMBOL
+ What mode to get the yasnippet snippet from. If omitted, either PRED (if
+ it's a major-mode symbol) or the mode of the buffer is used.
+ :project BOOL
+ If non-nil, ignore this template if this buffer isn't in a project.
+ :ignore BOOL
+ If non-nil, don't expand any template for this file and don't test any other
+ file template rule against this buffer.
+
+\(fn PRED &key WHEN TRIGGER MODE PROJECT IGNORE)"
+ (declare (indent defun))
+ (defer-until! (boundp '+file-templates-alist)
+ (+file-templates--set pred plist)))
+
+;;;###autodef
+(defun set-file-templates! (&rest templates)
+ "Like `set-file-templates!', but can register multiple file templates at once.
+
+\(fn &rest (PRED &key WHEN TRIGGER MODE PROJECT IGNORE))"
+ (defer-until! (boundp '+file-templates-alist)
+ (dolist (template templates)
+ (+file-templates--set (car template) (cdr template)))))
+
+
+;;
+;; Library
+
+;;;###autoload
+(cl-defun +file-templates--expand (pred &key project mode trigger ignore _when)
+ "Auto insert a yasnippet snippet into current file and enter insert mode (if
+evil is loaded and enabled)."
+ (when (and pred (not ignore))
+ (when (if project (doom-project-p) t)
+ (unless mode
+ (setq mode (if (symbolp pred) pred major-mode)))
+ (unless mode
+ (user-error "Couldn't determine mode for %s file template" pred))
+ (unless trigger
+ (setq trigger +file-templates-default-trigger))
+ (if (functionp trigger)
+ (funcall trigger)
+ (require 'yasnippet)
+ (unless yas-minor-mode
+ (yas-minor-mode-on))
+ (when (and yas-minor-mode
+ (when-let*
+ ((template (cl-find trigger (yas--all-templates (yas--get-snippet-tables mode))
+ :key #'yas--template-key :test #'equal)))
+ (yas-expand-snippet (yas--template-content template)))
+ (and (featurep 'evil) evil-mode)
+ (and yas--active-field-overlay
+ (overlay-buffer yas--active-field-overlay)
+ (overlay-get yas--active-field-overlay 'yas--field)))
+ (evil-initialize-state 'insert))))))
+
+;;;###autoload
+(defun +file-templates-get-short-path ()
+ "Fetches a short file path for the header in Doom module templates."
+ (let ((path (file-truename (or buffer-file-name default-directory))))
+ (cond ((string-match "/modules/\\(.+\\)$" path)
+ (match-string 1 path))
+ ((file-in-directory-p path doom-emacs-dir)
+ (file-relative-name path doom-emacs-dir))
+ ((abbreviate-file-name path)))))
+
+;;;###autoload
+(defun +file-template-p (rule)
+ "Return t if RULE applies to the current buffer."
+ (let ((pred (car rule))
+ (plist (cdr rule)))
+ (and (cond ((and (stringp pred) buffer-file-name) (string-match-p pred buffer-file-name))
+ ((symbolp pred) (eq major-mode pred)))
+ (or (not (plist-member plist :when))
+ (funcall (plist-get plist :when) buffer-file-name))
+ rule)))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun +file-templates/insert-license ()
+ "Insert a license file template into the current file."
+ (interactive)
+ (require 'yasnippet)
+ (let* ((templates
+ (let ((yas-choose-tables-first nil) ; avoid prompts
+ (yas-choose-keys-first nil))
+ (cl-loop for tpl in (yas--all-templates (yas--get-snippet-tables 'text-mode))
+ for uuid = (yas--template-uuid tpl)
+ if (string-prefix-p "__license-" uuid)
+ collect (cons (string-remove-prefix "__license-" uuid) tpl))))
+ (uuid (yas-choose-value (mapcar #'car templates))))
+ (when uuid
+ (yas-expand-snippet (cdr (assoc uuid templates))))))
+
+;;;###autoload
+(defun +file-templates/debug ()
+ "Tests the current buffer and outputs the file template rule most appropriate
+for it. This is used for testing."
+ (interactive)
+ (message "Found %s" (cl-find-if #'+file-template-p +file-templates-alist)))
diff --git a/modules/feature/file-templates/config.el b/modules/feature/file-templates/config.el
index db8feaafe..f058d6c9c 100644
--- a/modules/feature/file-templates/config.el
+++ b/modules/feature/file-templates/config.el
@@ -1,123 +1,135 @@
;;; feature/file-templates/config.el -*- lexical-binding: t; -*-
-(require! :feature snippets)
-
(defvar +file-templates-dir
(expand-file-name "templates/" (file-name-directory load-file-name))
"The path to a directory of yasnippet folders to use for file templates.")
+(defvar +file-templates-default-trigger "__"
+ "The default yasnippet trigger key (a string) for file template rules that
+don't have a :trigger property in `+file-templates-alist'.")
+
+(defvar +file-templates-alist
+ `(;; General
+ (gitignore-mode)
+ (dockerfile-mode)
+ ("/docker-compose\\.yml$" :mode yaml-mode)
+ ("/Makefile$" :mode makefile-gmake-mode)
+ ;; elisp
+ ("/.dir-locals.el$")
+ ("/packages\\.el$" :when +file-templates-in-emacs-dirs-p
+ :trigger "__doom-packages"
+ :mode emacs-lisp-mode)
+ ("/doctor\\.el$" :when +file-templates-in-emacs-dirs-p
+ :trigger "__doom-doctor"
+ :mode emacs-lisp-mode)
+ ("/test/.+\\.el$" :when +file-templates-in-emacs-dirs-p
+ :trigger "__doom-test"
+ :mode emacs-lisp-mode)
+ ("\\.el$" :when +file-templates-in-emacs-dirs-p
+ :trigger "__doom-module"
+ :mode emacs-lisp-mode)
+ ("-test\\.el$" :mode emacs-ert-mode)
+ (emacs-lisp-mode :trigger "__initfile")
+ (snippet-mode)
+ ;; C/C++
+ ("/main\\.c\\(?:c\\|pp\\)$" :trigger "__main.cpp" :mode c++-mode)
+ ("/win32_\\.c\\(?:c\\|pp\\)$" :trigger "__winmain.cpp" :mode c++-mode)
+ ("\\.c\\(?:c\\|pp\\)$" :trigger "__cpp" :mode c++-mode)
+ ("\\.h\\(?:h\\|pp\\|xx\\)$" :trigger "__hpp" :mode c++-mode)
+ ("\\.h$" :trigger "__h" :mode c-mode)
+ (c-mode :trigger "__c")
+ ;; go
+ ("/main\\.go$" :trigger "__main.go" :mode go-mode :project t)
+ (go-mode :trigger "__.go")
+ ;; web-mode
+ ("/normalize\\.scss$" :trigger "__normalize.scss" :mode scss-mode)
+ ("/master\\.scss$" :trigger "__master.scss" :mode scss-mode)
+ ("\\.html$" :trigger "__.html" :mode web-mode)
+ (scss-mode)
+ ;; java
+ ("/main\\.java$" :trigger "__main" :mode java-mode)
+ ("/build\\.gradle$" :trigger "__build.gradle" :mode android-mode)
+ ("/src/.+\\.java$" :mode java-mode)
+ ;; javascript
+ ("/package\\.json$" :trigger "__package.json" :mode json-mode)
+ ("/bower\\.json$" :trigger "__bower.json" :mode json-mode)
+ ("/gulpfile\\.js$" :trigger "__gulpfile.js" :mode js-mode)
+ ("/webpack\\.config\\.js$" :trigger "__webpack.config.js" :mode js-mode)
+ ("\\.js\\(?:on\\|hintrc\\)$" :mode json-mode)
+ ;; Lua
+ ("/main\\.lua$" :trigger "__main.lua" :mode love-mode)
+ ("/conf\\.lua$" :trigger "__conf.lua" :mode love-mode)
+ ;; Markdown
+ (markdown-mode)
+ ;; Org
+ ("/README\\.org$"
+ :when +file-templates-in-emacs-dirs-p
+ :trigger "__doom-readme"
+ :mode org-mode)
+ ("\\.org$" :trigger "__" :mode org-mode)
+ ;; PHP
+ ("\\.class\\.php$" :trigger "__.class.php" :mode php-mode)
+ (php-mode)
+ ;; Python
+ ;; TODO ("tests?/test_.+\\.py$" :trigger "__" :mode nose-mode)
+ ;; TODO ("/setup\\.py$" :trigger "__setup.py" :mode python-mode)
+ (python-mode)
+ ;; Ruby
+ ("/lib/.+\\.rb$" :trigger "__module" :mode ruby-mode :project t)
+ ("/spec_helper\\.rb$" :trigger "__helper" :mode rspec-mode :project t)
+ ("_spec\\.rb$" :mode rspec-mode :project t)
+ ("/\\.rspec$" :trigger "__.rspec" :mode rspec-mode :project t)
+ ("\\.gemspec$" :trigger "__.gemspec" :mode ruby-mode :project t)
+ ("/Gemfile$" :trigger "__Gemfile" :mode ruby-mode :project t)
+ ("/Rakefile$" :trigger "__Rakefile" :mode ruby-mode :project t)
+ (ruby-mode)
+ ;; Rust
+ ("/Cargo.toml$" :trigger "__Cargo.toml" :mode rust-mode)
+ ("/main\\.rs$" :trigger "__main.rs" :mode rust-mode)
+ ;; Slim
+ ("/\\(?:index\\|main\\)\\.slim$" :mode slim-mode)
+ ;; Shell scripts
+ ("\\.zunit$" :trigger "__zunit" :mode sh-mode)
+ (fish-mode)
+ (sh-mode)
+ ;; Solidity
+ (solidity-mode :trigger "__sol"))
+ "An alist of file template rules. The CAR of each rule is either a major mode
+symbol or regexp string. The CDR is a plist. See `set-file-template!' for more
+information.")
+
;;
-;; Plugins
+;; Library
+
+(defun +file-templates-in-emacs-dirs-p (file)
+ "Returns t if FILE is in Doom or your private directory."
+ (or (file-in-directory-p file doom-private-dir)
+ (file-in-directory-p file doom-emacs-dir)))
+
+(defun +file-templates|check ()
+ "Check if the current buffer is a candidate for file template expansion. It
+must be non-read-only, empty, and there must be a rule in
+`+file-templates-alist' that applies to it."
+ (when (and (not buffer-read-only)
+ (bobp) (eobp)
+ (not (string-match-p "^ *\\*" (buffer-name))))
+ (when-let* ((rule (cl-find-if #'+file-template-p +file-templates-alist)))
+ (apply #'+file-templates--expand rule))))
+
+
;;
+;; Bootstrap
-(def-package! autoinsert ; built-in
- :defer 1
- :init
- (setq auto-insert-query nil ; Don't prompt before insertion
- auto-insert-alist nil) ; Tabula rasa
- (after! yasnippet
- (push '+file-templates-dir yas-snippet-dirs))
+(after! yasnippet
+ (if (featurep! :feature snippets)
+ (add-to-list 'yas-snippet-dirs '+file-templates-dir 'append #'eq)
+ (setq yas-prompt-functions (delq #'yas-dropdown-prompt yas-prompt-functions)
+ yas-snippet-dirs '(+file-templates-dir))
+ ;; Exit snippets on ESC from normal mode
+ (add-hook 'doom-escape-hook #'yas-abort-snippet)
+ ;; Ensure file templates in `+file-templates-dir' are visible
+ (yas-reload-all)))
- :config
- (auto-insert-mode 1)
-
- (defun +file-templates--expand (key &optional mode project-only)
- "Auto insert a yasnippet snippet into the blank file."
- (when (if project-only (doom-project-p) t)
- (require 'yasnippet)
- (unless yas-minor-mode
- (yas-minor-mode-on))
- (when (and yas-minor-mode
- (yas-expand-snippet
- (yas--template-content
- (cl-find key (yas--all-templates (yas--get-snippet-tables mode))
- :key #'yas--template-key :test #'equal)))
- (and (featurep 'evil) evil-mode)
- (and yas--active-field-overlay
- (overlay-buffer yas--active-field-overlay)
- (overlay-get yas--active-field-overlay 'yas--field)))
- (evil-initialize-state 'insert))))
-
- (defun +file-templates-add (args)
- (cl-destructuring-bind (regexp trigger &optional mode project-only-p) args
- (define-auto-insert
- regexp
- (if trigger
- (vector
- `(lambda () (+file-templates--expand ,trigger ',mode ,project-only-p)))
- #'ignore))))
-
- (mapc #'+file-templates-add
- ;; General
- '(("/\\.gitignore$" "__" gitignore-mode)
- ("/Dockerfile$" "__" dockerfile-mode)
- ("/docker-compose.yml$" "__" yaml-mode)
- ;; C/C++
- ("\\.h$" "__h" c-mode)
- ("\\.c$" "__c" c-mode)
- ("\\.h\\(h\\|pp|xx\\)$" "__hpp" c++-mode)
- ("\\.\\(cc\\|cpp\\)$" "__cpp" c++-mode)
- ("/main\\.\\(cc\\|cpp\\)$" "__main.cpp" c++-mode)
- ("/win32_\\.\\(cc\\|cpp\\)$" "__winmain.cpp" c++-mode)
- ("/Makefile$" "__" makefile-gmake-mode)
- ;; Elisp
- ("\\.el$" "__initfile" emacs-lisp-mode)
- ("/.dir-locals.el$" nil)
- ("-test\\.el$" "__" emacs-ert-mode)
- ("/\\(?:.emacs.d\\|doom-emacs\\)?/.+\\.el$" "__doom-module" emacs-lisp-mode)
- ("/\\(?:.emacs.d\\|doom-emacs\\)?/.+/packages\\.el$" "__doom-packages" emacs-lisp-mode)
- ("/\\(?:.emacs.d\\|doom-emacs\\)?/.+/test/.+\\.el$" "__doom-test" emacs-lisp-mode)
- (snippet-mode "__" snippet-mode)
- ;; Go
- ("\\.go$" "__.go" go-mode)
- ("/main\\.go$" "__main.go" go-mode t)
- ;; HTML
- ("\\.html$" "__.html" web-mode)
- ;; java
- ("/src/.+/.+\\.java$" "__" java-mode)
- ("/main\\.java$" "__main" java-mode)
- ("/build\\.gradle$" "__build.gradle" android-mode)
- ;; Javascript
- ("\\.\\(json\\|jshintrc\\)$" "__" json-mode)
- ("/package\\.json$" "__package.json" json-mode)
- ("/bower\\.json$" "__bower.json" json-mode)
- ("/gulpfile\\.js$" "__gulpfile.js" js-mode)
- ("/webpack\\.config\\.js$" "__webpack.config.js" js-mode)
- ;; Lua
- ("/main\\.lua$" "__main.lua" love-mode)
- ("/conf\\.lua$" "__conf.lua" love-mode)
- ;; Markdown
- ("\\.md$" "__" markdown-mode)
- ;; Org
- ("\\.org$" "__" org-mode)
- ("/\\(?:.emacs.d\\|doom-emacs\\)?/.+/README\\.org$" "__doom-readme" org-mode)
- ;; PHP
- ("\\.php$" "__" php-mode)
- ("\\.class\\.php$" "__.class.php" php-mode)
- ;; Python
- ;;("tests?/test_.+\\.py$" "__" nose-mode)
- ;;("/setup\\.py$" "__setup.py" python-mode)
- ("\\.py$" "__" python-mode)
- ;; Ruby
- ("\\.rb$" "__" ruby-mode)
- ("/Rakefile$" "__Rakefile" ruby-mode t)
- ("/Gemfile$" "__Gemfile" ruby-mode t)
- ("/\\.rspec$" "__.rspec" rspec-mode)
- ("\\.gemspec$" "__.gemspec" ruby-mode t)
- ("/spec_helper\\.rb$" "__helper" rspec-mode t)
- ("/lib/.+\\.rb$" "__module" ruby-mode t)
- ("_spec\\.rb$" "__" rspec-mode t)
- ;; Rust
- ("/main\\.rs$" "__main.rs" rust-mode)
- ("/Cargo.toml$" "__Cargo.toml" rust-mode)
- ;; SCSS
- ("\\.scss$" "__" scss-mode)
- ("/master\\.scss$" "__master.scss" scss-mode)
- ("/normalize\\.scss$" "__normalize.scss" scss-mode)
- ;; Slim
- ("/\\(index\\|main\\)\\.slim$" "__" slim-mode)
- ;; Shell scripts
- ("\\.z?sh$" "__" sh-mode)
- ("\\.fish$" "__" fish-mode)
- ("\\.zunit$" "__zunit" sh-mode))))
+;;
+(add-hook 'find-file-hook #'+file-templates|check)
diff --git a/modules/feature/file-templates/packages.el b/modules/feature/file-templates/packages.el
index 8259e198e..0c62823ab 100644
--- a/modules/feature/file-templates/packages.el
+++ b/modules/feature/file-templates/packages.el
@@ -1,5 +1,5 @@
;; -*- no-byte-compile: t; -*-
;;; feature/file-templates/packages.el
-(depends-on! :feature snippets)
+(package! yasnippet)
diff --git a/modules/feature/file-templates/templates/dockerfile-mode/__ b/modules/feature/file-templates/templates/dockerfile-mode/__
index 0bd4b7a1a..9d1f81ed9 100644
--- a/modules/feature/file-templates/templates/dockerfile-mode/__
+++ b/modules/feature/file-templates/templates/dockerfile-mode/__
@@ -1,4 +1,4 @@
FROM ${1:phusion/baseimage:latest}
-MAINTAINER ${2:Henrik Lissner }
+MAINTAINER ${2:`user-full-name` <`user-mail-address`>}
$0
\ No newline at end of file
diff --git a/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-doctor b/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-doctor
new file mode 100644
index 000000000..af30ef14b
--- /dev/null
+++ b/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-doctor
@@ -0,0 +1,3 @@
+;;; `(+file-templates-get-short-path)` -*- lexical-binding: t; -*-
+
+$0
diff --git a/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-module b/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-module
index 4f5468293..af30ef14b 100644
--- a/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-module
+++ b/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-module
@@ -1,3 +1,3 @@
-;;; `(file-relative-name buffer-file-truename doom-modules-dir)` -*- lexical-binding: t; -*-
+;;; `(+file-templates-get-short-path)` -*- lexical-binding: t; -*-
$0
diff --git a/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-packages b/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-packages
index a87f1eae8..bf02a0423 100644
--- a/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-packages
+++ b/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-packages
@@ -1,4 +1,4 @@
;; -*- no-byte-compile: t; -*-
-;;; `(file-relative-name buffer-file-truename doom-modules-dir)`
+;;; `(+file-templates-get-short-path)`
$0
diff --git a/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-test b/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-test
index af428094a..054819720 100644
--- a/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-test
+++ b/modules/feature/file-templates/templates/emacs-lisp-mode/__doom-test
@@ -1,4 +1,4 @@
;; -*- no-byte-compile: t; -*-
-;;; `(file-relative-name buffer-file-truename doom-modules-dir)`
+;;; `(+file-templates-get-short-path)`
$0
\ No newline at end of file
diff --git a/modules/feature/file-templates/templates/java-mode/.yas-setup.el b/modules/feature/file-templates/templates/java-mode/.yas-setup.el
index f4143072e..b00feed7e 100644
--- a/modules/feature/file-templates/templates/java-mode/.yas-setup.el
+++ b/modules/feature/file-templates/templates/java-mode/.yas-setup.el
@@ -1,11 +1,9 @@
(defun yas-java-project-package ()
- (if (eq major-mode 'java-mode)
- (s-chop-suffix "." (s-replace "/" "." (f-dirname (f-relative (buffer-file-name)
- (concat (narf/project-root) "/src/")))))
- ""))
+ (or (and (eq major-mode 'java-mode)
+ (+java-current-package))
+ ""))
(defun yas-java-class-name ()
- (if (eq major-mode 'java-mode)
- (f-no-ext (f-base (buffer-file-name)))
- ""))
-
+ (or (and (eq major-mode 'java-mode)
+ (+java-current-class))
+ ""))
diff --git a/modules/feature/file-templates/templates/js-mode/__webpack.config.js b/modules/feature/file-templates/templates/js-mode/__webpack.config.js
index 267eae732..d7998e157 100644
--- a/modules/feature/file-templates/templates/js-mode/__webpack.config.js
+++ b/modules/feature/file-templates/templates/js-mode/__webpack.config.js
@@ -7,9 +7,15 @@ module.exports = {
filename: "app.bundle.js"
},
module: {
- loaders: [
- { test: /\.js$/, include: __dirname + '/app', loader: 'babel-loader' }$0
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: {
+ loader: "babel-loader"
+ }
+ }
]
- }
+ },
// plugins: []
-};
\ No newline at end of file
+};
diff --git a/modules/feature/file-templates/templates/org-mode/__doom-readme b/modules/feature/file-templates/templates/org-mode/__doom-readme
index bd4c3fb91..8e8af8fa6 100644
--- a/modules/feature/file-templates/templates/org-mode/__doom-readme
+++ b/modules/feature/file-templates/templates/org-mode/__doom-readme
@@ -1,48 +1,42 @@
# -*- mode: snippet -*-
# name: Doom module readme
# --
-#+TITLE: ${1:`(progn (string-match "modules/\\([^/]+\\)/\\([^/]+\\)/.+" buffer-file-name)
- (format ":%s %s"
- (match-string 1 buffer-file-name)
- (match-string 2 buffer-file-name)))`}
-
-${2:A short summary about what this module does.}
+#+TITLE: ${1:`(if (string-match "modules/\\([^/]+\\)/\\([^/]+\\)/.+" buffer-file-name)
+ (format "%s/%s"
+ (match-string 1 buffer-file-name)
+ (match-string 2 buffer-file-name))
+ "")`}
+#+DATE: `(format (format-time-string "%B %%s, %Y") (string-to-number (format-time-string "%d")))`
+#+SINCE: ${2:{replace with next tagged release version}}
+#+STARTUP: inlineimages
-${3:If necessary, include a longer description below it that goes into more detail. This may be as long as you like.
+* Table of Contents :TOC_3:noexport:
-+ If possible, include a list of features
-+ Include links to major plugins that the module uses, if applicable
-+ Use links whenever you can
-+ Mention dependencies on other modules here}
+* Description
+${3:A summary of what this module does.}
-* Table of Contents :TOC:
++ If possible, include a brief list of feature highlights here
++ Like code completion, syntax checking or available snippets
++ Include links to packages & external things where possible
-* Install
-** Main dependencies
-*** MacOS
-#+BEGIN_SRC sh :tangle (if (doom-system-os 'macos) "yes")
-brew install x
-#+END_SRC
+** Module Flags
+This module provides no flags.
-*** Arch Linux
-#+BEGIN_SRC sh :dir /sudo:: :tangle (if (doom-system-os 'arch) "yes")
-sudo pacman --needed --noconfirm -S X
-#+END_SRC
+** Plugins
+{A list of linked plugins}
-** Extra Dependencies
-+ A
-+ B
-+ C
+** Hacks
+{A list of internal modifications to included packages}
-#+BEGIN_SRC sh
-Y install A B C
-#+END_SRC
+* Prerequisites
+This module has no prereqisites.
+
+* Features
+An in-depth list of features, how to use them, and their dependencies.
* Configuration
+How to configure this module, including common problems and how to address them.
-* Usage
-
-* Appendix
-** Commands
-** Hacks
+* Troubleshooting
+Common issues and their solution, or places to look for help.
$0
\ No newline at end of file
diff --git a/modules/feature/file-templates/templates/solidity-mode/__sol b/modules/feature/file-templates/templates/solidity-mode/__sol
new file mode 100644
index 000000000..46facfdb9
--- /dev/null
+++ b/modules/feature/file-templates/templates/solidity-mode/__sol
@@ -0,0 +1,10 @@
+# -*- mode: snippet -*-
+# group: file templates
+# contributor: Edmund Miller
+# name: solidity template
+# --
+pragma solidity ^0.4.22;
+
+contract $0 {
+
+}
\ No newline at end of file
diff --git a/modules/feature/file-templates/templates/text-mode/__license b/modules/feature/file-templates/templates/text-mode/__license
new file mode 100644
index 000000000..b22a289bb
--- /dev/null
+++ b/modules/feature/file-templates/templates/text-mode/__license
@@ -0,0 +1,5 @@
+# -*- mode: snippet -*-
+# name: Insert a license
+# type: command
+# --
+(+file-templates/insert-license)
\ No newline at end of file
diff --git a/modules/feature/file-templates/templates/text-mode/__license-apache b/modules/feature/file-templates/templates/text-mode/__license-apache
index 93184c002..a50a612a5 100644
--- a/modules/feature/file-templates/templates/text-mode/__license-apache
+++ b/modules/feature/file-templates/templates/text-mode/__license-apache
@@ -1,5 +1,8 @@
# -*- mode: snippet -*-
-# name: Apache 2.0 License
+# name: Apache License 2.0
+# uuid: __license-apache
+# group: Licenses
+# contributor: https://choosealicense.com/licenses/apache-2.0/
# --
Apache License
Version 2.0, January 2004
diff --git a/modules/feature/file-templates/templates/text-mode/__license-apache-bp b/modules/feature/file-templates/templates/text-mode/__license-apache-bp
new file mode 100644
index 000000000..95d9d8bc5
--- /dev/null
+++ b/modules/feature/file-templates/templates/text-mode/__license-apache-bp
@@ -0,0 +1,19 @@
+# -*- mode: snippet -*-
+# name: Apache License 2.0 (boilerplate)
+# uuid: __license-apache-bp
+# group: Licenses
+# contributor: https://choosealicense.com/licenses/apache-2.0/
+# --
+Copyright ${1:`(format-time-string "%Y")` `user-full-name`}
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/modules/feature/file-templates/templates/text-mode/__license-bsd2 b/modules/feature/file-templates/templates/text-mode/__license-bsd2
index a1a0d0094..bbff2e37d 100644
--- a/modules/feature/file-templates/templates/text-mode/__license-bsd2
+++ b/modules/feature/file-templates/templates/text-mode/__license-bsd2
@@ -1,17 +1,9 @@
# -*- mode: snippet -*-
-# name: BSD 2-clause License
+# name: 2-clause BSD License
+# uuid: __license-bsd2
+# group: Licenses
# --
-${1:}
-${2:}
-
-In the original BSD license, the occurrence of "copyright holder" in the 3rd
-clause read "ORGANIZATION", placeholder for "University of California". In the
-original BSD license, both occurrences of the phrase "COPYRIGHT HOLDERS AND
-CONTRIBUTORS" in the disclaimer read "REGENTS AND CONTRIBUTORS".
-
-Here is the license template:
-
-Copyright (c) $2, $1
+Copyright (c) ${1:`(format-time-string "%Y")`} ${2:`user-full-name`}
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
diff --git a/modules/feature/file-templates/templates/text-mode/__license-bsd3 b/modules/feature/file-templates/templates/text-mode/__license-bsd3
index 8cfd0b1ea..dc3d0e843 100644
--- a/modules/feature/file-templates/templates/text-mode/__license-bsd3
+++ b/modules/feature/file-templates/templates/text-mode/__license-bsd3
@@ -1,18 +1,8 @@
# -*- mode: snippet -*-
-# name: BSD 3-clause License
+# name: 3-clause BSD License
+# uuid: __license-bsd3
# --
-${1:}
-${2:}
-${3:}
-
-In the original BSD license, the occurrence of "copyright holder" in the 3rd
-clause read "ORGANIZATION", placeholder for "University of California". In the
-original BSD license, both occurrences of the phrase "COPYRIGHT HOLDERS AND
-CONTRIBUTORS" in the disclaimer read "REGENTS AND CONTRIBUTORS".
-
-Here is the license template:
-
-Copyright (c) $3, $1
+Copyright (c) ${1:`(format-time-string "%Y")`} ${2:`user-full-name`}
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
diff --git a/modules/feature/file-templates/templates/text-mode/__license-gpl3 b/modules/feature/file-templates/templates/text-mode/__license-gpl3
index 98a61181a..197d810c1 100644
--- a/modules/feature/file-templates/templates/text-mode/__license-gpl3
+++ b/modules/feature/file-templates/templates/text-mode/__license-gpl3
@@ -1,303 +1,628 @@
# -*- mode: snippet -*-
-# name: GPL 3.0 License
+# name: GNU GPL 3.0 License
+# uuid: __license-gpl3
+# group: Licenses
+# contributor: https://choosealicense.com/licenses/gpl-3.0/
# --
-${1:Software Headline}
-Copyright (C) `(format-time-string "%Y")` `user-full-name`
-
-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 2 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, write to the Free Software Foundation, Inc.,
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
-
GNU GENERAL PUBLIC LICENSE
- Version 2, June 1991
+ Version 3, 29 June 2007
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
- The licenses for most software are designed to take away your
-freedom to share and change it. By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users. This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it. (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.) You can apply it to
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
- To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have. You must make sure that they, too, receive or can get the
-source code. And you must show them these terms so they know their
-rights.
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
- We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
- Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software. If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
- Finally, any free program is threatened constantly by software
-patents. We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary. To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
- GNU GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+ TERMS AND CONDITIONS
- 0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License. The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language. (Hereinafter, translation is included without limitation in
-the term "modification".) Each licensee is addressed as "you".
+ 0. Definitions.
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope. The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
+ "This License" refers to version 3 of the GNU General Public License.
- 1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
- 2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
- a) You must cause the modified files to carry prominent notices
- stating that you changed the files and the date of any change.
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
- b) You must cause any work that you distribute or publish, that in
- whole or in part contains or is derived from the Program or any
- part thereof, to be licensed as a whole at no charge to all third
- parties under the terms of this License.
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
- c) If the modified program normally reads commands interactively
- when run, you must cause it, when started running for such
- interactive use in the most ordinary way, to print or display an
- announcement including an appropriate copyright notice and a
- notice that there is no warranty (or else, saying that you provide
- a warranty) and that users may redistribute the program under
- these conditions, and telling the user how to view a copy of this
- License. (Exception: if the Program itself is interactive but
- does not normally print such an announcement, your work based on
- the Program is not required to print an announcement.)
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
-These requirements apply to the modified work as a whole. If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works. But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
+ 1. Source Code.
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
- 3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
- a) Accompany it with the complete corresponding machine-readable
- source code, which must be distributed under the terms of Sections
- 1 and 2 above on a medium customarily used for software interchange; or,
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
- b) Accompany it with a written offer, valid for at least three
- years, to give any third party, for a charge no more than your
- cost of physically performing source distribution, a complete
- machine-readable copy of the corresponding source code, to be
- distributed under the terms of Sections 1 and 2 above on a medium
- customarily used for software interchange; or,
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
- c) Accompany it with the information you received as to the offer
- to distribute corresponding source code. (This alternative is
- allowed only for noncommercial distribution and only if you
- received the program in object code or executable form with such
- an offer, in accord with Subsection b above.)
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
-The source code for a work means the preferred form of the work for
-making modifications to it. For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable. However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
+ The Corresponding Source for a work in source code form is that
+same work.
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
+ 2. Basic Permissions.
- 4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License. Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
- 5. You are not required to accept this License, since you have not
-signed it. However, nothing else grants you permission to modify or
-distribute the Program or its derivative works. These actions are
-prohibited by law if you do not accept this License. Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
- 6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions. You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
this License.
- 7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all. For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
+ 13. Use with the GNU Affero General Public License.
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices. Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
+ 14. Revised Versions of this License.
- 8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded. In such case, this License incorporates
-the limitation as if written in the body of this License.
-
- 9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time. Such new versions will
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
-Each version is given a distinguishing version number. If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation. If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
- 10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission. For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this. Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
- NO WARRANTY
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
+ 15. Disclaimer of Warranty.
- 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
@@ -309,15 +634,15 @@ free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
+state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
- {description}
- Copyright (C) {year} {fullname}
+
+ Copyright (C)
- This program is free software; you can redistribute it and/or modify
+ 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 2 of the License, or
+ 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,
@@ -325,37 +650,31 @@ the "copyright" line and a pointer to where the full notice is found.
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, write to the Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
- Gnomovision version 69, Copyright (C) year name of author
- Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type \`show w'.
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
- under certain conditions; type \`show c' for details.
+ under certain conditions; type `show c' for details.
-The hypothetical commands \`show w' and \`show c' should show the appropriate
-parts of the General Public License. Of course, the commands you use may
-be called something other than \`show w' and \`show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary. Here is a sample; alter the names:
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
- Yoyodyne, Inc., hereby disclaims all copyright interest in the program
- \`Gnomovision' (which makes passes at compilers) written by James Hacker.
-
- {signature of Ty Coon}, 1 April 1989
- Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs. If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/modules/feature/file-templates/templates/text-mode/__license-gpl3-bp b/modules/feature/file-templates/templates/text-mode/__license-gpl3-bp
new file mode 100644
index 000000000..4e8ff3c21
--- /dev/null
+++ b/modules/feature/file-templates/templates/text-mode/__license-gpl3-bp
@@ -0,0 +1,21 @@
+# -*- mode: snippet -*-
+# name: GNU GPL 3.0 License (boilerplate)
+# uuid: __license-gpl3-bp
+# group: Licenses
+# contributor: https://choosealicense.com/licenses/lgpl-3.0
+# --
+${1:}
+Copyright (C) ${2:`(format-time-string "%Y")`} ${3:`user-full-name`}
+
+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 .
\ No newline at end of file
diff --git a/modules/feature/file-templates/templates/text-mode/__license-lgpl3 b/modules/feature/file-templates/templates/text-mode/__license-lgpl3
new file mode 100644
index 000000000..8aefb505d
--- /dev/null
+++ b/modules/feature/file-templates/templates/text-mode/__license-lgpl3
@@ -0,0 +1,171 @@
+# -*- mode: snippet -*-
+# name: GNU LGPL v3 License
+# uuid: __license-lgpl3
+# group: Licenses
+# contribuer: https://choosealicense.com/licenses/lgpl-3.0/
+# --
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/modules/feature/file-templates/templates/text-mode/__license-mit b/modules/feature/file-templates/templates/text-mode/__license-mit
index 3807c0730..dd041f86d 100644
--- a/modules/feature/file-templates/templates/text-mode/__license-mit
+++ b/modules/feature/file-templates/templates/text-mode/__license-mit
@@ -1,9 +1,12 @@
# -*- mode: snippet -*-
# name: MIT License
+# uuid: __license-mit
+# group: Licenses
+# contributor: https://choosealicense.com/licenses/mit/
# --
The MIT License (MIT)
-Copyright (c) `(format-time-string "%Y")` `user-full-name`.
+Copyright (c) ${1:`(format-time-string "%Y")`} ${2:`user-full-name`}
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/modules/feature/file-templates/templates/text-mode/__license-mozilla b/modules/feature/file-templates/templates/text-mode/__license-mozilla
new file mode 100644
index 000000000..bba13b752
--- /dev/null
+++ b/modules/feature/file-templates/templates/text-mode/__license-mozilla
@@ -0,0 +1,380 @@
+# -*- mode: snippet -*-
+# name: Mozilla Public License 2.0
+# uuid: __license-mozilla
+# group: Licenses
+# contributor: https://choosealicense.com/licenses/mpl-2.0
+# --
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
+
diff --git a/modules/feature/file-templates/templates/text-mode/__license-mozilla-bp b/modules/feature/file-templates/templates/text-mode/__license-mozilla-bp
new file mode 100644
index 000000000..7a7fc422f
--- /dev/null
+++ b/modules/feature/file-templates/templates/text-mode/__license-mozilla-bp
@@ -0,0 +1,9 @@
+# -*- mode: snippet -*-
+# name: Mozilla Public License 2.0 (boilerplate)
+# uuid: __license-mozilla-bp
+# group: Licenses
+# contributor: https://choosealicense.com/licenses/mpl-2.0
+# --
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at http://mozilla.org/MPL/2.0/.
\ No newline at end of file
diff --git a/modules/feature/file-templates/templates/text-mode/__license-unlicense b/modules/feature/file-templates/templates/text-mode/__license-unlicense
new file mode 100644
index 000000000..6eb992707
--- /dev/null
+++ b/modules/feature/file-templates/templates/text-mode/__license-unlicense
@@ -0,0 +1,30 @@
+# -*- mode: snippet -*-
+# name: The Unlicense
+# uuid: __license-unlicense
+# group: Licenses
+# contributor: https://choosealicense.com/licenses/unlicense
+# --
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
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..d42b753f7
--- /dev/null
+++ b/modules/feature/lookup/README.org
@@ -0,0 +1,189 @@
+#+TITLE: feature/lookup
+#+DATE: January 4, 2018
+#+SINCE: v2.0.9
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Install][Install]]
+ - [[Module flags][Module flags]]
+ - [[Dependencies][Dependencies]]
+- [[Features][Features]]
+ - [[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]]
+
+* Description
+Integrates with code navigation and documentation tools to help you quickly look
+up definitions, references and documentation.
+
++ Jump-to-definition and find-references implementations that just work.
++ Powerful xref integration for languages that support it.
++ Documentation lookup for a variety of online sources (like devdocs.io,
+ stackoverflow or youtube).
++ Integration with Dash.app docsets.
+
+** Module Flags
++ ~+docsets~ Enable integration with Dash.app docsets.
+
+** Plugins
++ [[https://github.com/jacktasia/dumb-jump][dumb-jump]]
++ [[https://github.com/alexmurray/ivy-xref][ivy-xref]] or [[https://github.com/brotzeit/helm-xref][helm-xref]]
++ [[https://github.com/nathankot/counsel-dash][counsel-dash]] or [[https://github.com/areina/helm-dash][helm-dash]]
+
+* 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.
+
+** Dependencies
+This module has several soft dependencies:
+
++ ~the_silver_searcher~ and/or ~ripgrep~ as a last-resort fallback for
+ jump-to-definition/find-references.
++ Optionally, ~sqlite3~ for Dash docset support.
+
+*** MacOS
+#+BEGIN_SRC sh
+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
+sudo pacman -S sqlite the_silver_searcher ripgrep
+#+END_SRC
+
+* Features
+** 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 =K= 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 two setters:
+
+*** ~set-lookup-handlers! 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~.
++ ~:file FN~ :: Run when looking up the file for a symbol/string. Typically a
+ file path. Used by ~+lookup/file~.
++ ~: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-handlers! '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-handlers! 'js2-mode :xref-backend #'xref-js2-xref-backend)
+;; NOTE: xref doesn't provide a :documentation backend.
+#+END_SRC
+
+*** ~set-docsets! MODES &rest DOCSETS~
+Registers DOCSETS (one string or list of strings) for MODES (one major mode
+symbol or a list of them). It is used by ~+lookup/in-docsets~ and
+~+lookup/documentation~.
+
+#+BEGIN_SRC emacs-lisp
+(set-docsets! 'js2-mode "JavaScript" "JQuery")
+;; Add docsets to minor modes by starting DOCSETS with :add
+(set-docsets! 'rjsx-mode :add "React")
+;; Or remove docsets from minor modes
+(set-docsets! 'nodejs-mode :remove "JQuery")
+#+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/docsets.el b/modules/feature/lookup/autoload/docsets.el
new file mode 100644
index 000000000..405541211
--- /dev/null
+++ b/modules/feature/lookup/autoload/docsets.el
@@ -0,0 +1,100 @@
+;;; feature/lookup/autoload/docsets.el -*- lexical-binding: t; -*-
+;;;###if (featurep! +docsets)
+
+(defvar +lookup-docset-alist nil
+ "An alist mapping major and minor modes to lists of Dash docsets.
+
+Entries are added by `set-docsets!' and used by `+lookup-docsets-for-buffer' to
+assemble a list of installed & active docsets.")
+
+;;;###autodef
+(defun set-docsets! (modes &rest docsets)
+ "Registers a list of DOCSETS for MODES.
+
+MODES can be one major mode, or a list thereof.
+
+DOCSETS can be strings, each representing a dash docset, or a vector with the
+structure [DOCSET FORM]. If FORM evaluates to nil, the DOCSET is omitted. If it
+is non-nil, (format DOCSET FORM) is used as the docset.
+
+The first element in DOCSETS can be :add or :remove, making it easy for users to
+add to or remove default docsets from modes.
+
+DOCSETS can also contain sublists, which will be flattened.
+
+Example:
+
+ (set-docsets! '(js2-mode rjsx-mode) \"JavaScript\"
+ [\"React\" (eq major-mode 'rjsx-mode)]
+ [\"TypeScript\" (bound-and-true-p tide-mode)])
+
+Used by `+lookup/in-docsets' and `+lookup/documentation'."
+ (declare (indent defun))
+ (dolist (mode (doom-enlist modes))
+ (if (null docsets)
+ (setq +lookup-docset-alist
+ (delq (assq mode +lookup-docset-alist)
+ +lookup-docset-alist))
+ (let ((action (if (keywordp (car docsets)) (pop docsets)))
+ (docsets (mapcan #'doom-enlist docsets))) ; flatten list
+ (setf (alist-get mode +lookup-docset-alist)
+ (pcase action
+ (:add (append docsets (alist-get mode +lookup-docset-alist)))
+ (:remove (cl-set-difference (alist-get mode +lookup-docset-alist) docsets))
+ (_ docsets)))))))
+
+
+;;
+;; Library
+
+;;;###autoload
+(defun +lookup-docsets-for-buffer ()
+ "Return list of installed & selected docsets for the current major mode.
+
+This list is built from `+lookup-docset-alist'."
+ (cl-loop for docset in (cdr (assq major-mode +lookup-docset-alist))
+ when (or (stringp docset)
+ (and (vectorp docset)
+ (eval (aref docset 1) t)))
+ collect docset))
+
+;;;###autoload
+(defun +lookup-docset-installed-p (docset)
+ "Return t if DOCSET is installed."
+ (let ((path (helm-dash-docsets-path)))
+ (file-directory-p
+ (expand-file-name (format "%s.docset" docset)
+ path))))
+
+;;;###autoload
+(autoload 'helm-dash-installed-docsets "helm-dash")
+
+;;;###autoload
+(autoload 'helm-dash-docset-installed-p "helm-dash")
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defalias '+lookup/install-docset #'helm-dash-install-docset)
+
+(defvar counsel-dash-docsets)
+(defvar helm-dash-docsets)
+;;;###autoload
+(defun +lookup/in-docsets (&optional query docsets)
+ "Lookup QUERY in dash DOCSETS.
+
+QUERY is a string and docsets in an array of strings, each a name of a Dash
+docset. Requires either helm or ivy.
+
+Use `+lookup/install-docset' to install docsets."
+ (interactive)
+ (let* ((counsel-dash-docsets (or docsets (+lookup-docsets-for-buffer)))
+ (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))
+ ((user-error "No dash backend is installed, enable ivy or helm.")))))
diff --git a/modules/feature/lookup/autoload/evil.el b/modules/feature/lookup/autoload/evil.el
new file mode 100644
index 000000000..16084bba4
--- /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)
+ "Look up QUERY in your dash docsets. If BANG, prompt to select a docset (and
+install it if necessary)."
+ (interactive "")
+ (let (selected)
+ (when bang
+ (setq selected (helm-dash-read-docset "Select docset" (helm-dash-official-docsets)))
+ (unless (+lookup-docset-installed-p selected)
+ (+lookup/install-docset selected)))
+ (+lookup/in-docsets query (or selected (+lookup-docsets-for-buffer)))))
diff --git a/modules/feature/lookup/autoload/lookup.el b/modules/feature/lookup/autoload/lookup.el
new file mode 100644
index 000000000..af4d30c85
--- /dev/null
+++ b/modules/feature/lookup/autoload/lookup.el
@@ -0,0 +1,329 @@
+;;; feature/lookup/autoload/lookup.el -*- lexical-binding: t; -*-
+
+(defvar +lookup--handler-alist nil)
+
+;;;###autodef
+(cl-defun set-lookup-handlers!
+ (modes &rest plist &key definition references documentation file xref-backend async)
+ "Define a jump target for major MODES.
+
+This overwrites previously defined handlers for MODES. If used on minor modes,
+they are combined with handlers defined for other minor modes or the major mode
+it's activated in.
+
+This can be passed nil as its second argument to unset handlers for MODES. e.g.
+
+ (set-lookup-handlers! 'python-mode nil)
+
+Otherwise, these properties are available to be set:
+
+: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'.
+:file FN
+ Run when looking up the file for a symbol/string. Typically a file path.
+ Used by `+lookup/file'.
+:xref-backend FN
+ Defines an xref backend for a major-mode. If you define :definition and
+ :references along with :xref-backend, those will have higher precedence.
+:async BOOL
+ Indicates that the supplied handlers *after* this property are asynchronous.
+ Note: async handlers do not fall back to the default handlers, due to their
+ nature. To get around this, you must write specialized wrappers to wait for
+ the async response and return 'fallback."
+ (declare (indent defun))
+ (dolist (mode (doom-enlist modes))
+ (let ((hook (intern (format "%s-hook" mode)))
+ (fn (intern (format "+lookup|init-%s" mode))))
+ (cond ((null (car plist))
+ (remove-hook hook fn)
+ (delq! mode +lookup--handler-alist 'assq)
+ (unintern fn nil))
+ ((fset fn
+ (lambda ()
+ (when (or (eq major-mode mode)
+ (and (boundp mode)
+ (symbol-value mode)))
+ (cl-mapc #'+lookup--set-handler
+ (list definition
+ references
+ documentation
+ file
+ xref-backend)
+ (list '+lookup-definition-functions
+ '+lookup-references-functions
+ '+lookup-documentation-functions
+ '+lookup-file-functions
+ 'xref-backend-functions)))))
+ (add-hook hook fn))))))
+
+
+;;
+;;; Helpers
+
+(defun +lookup--set-handler (spec functions-var &optional async)
+ (when spec
+ (cl-destructuring-bind (fn . plist)
+ (doom-enlist spec)
+ (put fn '+lookup-plist (plist-put plist :async async))
+ (add-hook functions-var fn nil t))))
+
+(defun +lookup--symbol-or-region (&optional initial)
+ (cond ((stringp initial)
+ initial)
+ ((use-region-p)
+ (buffer-substring-no-properties (region-beginning)
+ (region-end)))
+ ((require 'xref nil t)
+ (xref-backend-identifier-at-point (xref-find-backend)))))
+
+(defun +lookup--run-handler (handler identifier)
+ (if (commandp handler)
+ (call-interactively handler)
+ (funcall handler identifier)))
+
+(defun +lookup--run-handlers (handler identifier origin &optional other-window)
+ (doom-log "Looking up '%s' with '%s'" identifier handler)
+ (condition-case e
+ (let ((plist (get handler '+lookup-plist)))
+ (cond ((plist-get plist :direct)
+ (+lookup--run-handler handler identifier)
+ t)
+ ((plist-get plist :async)
+ (when other-window
+ ;; If async, we can't catch the window change or destination
+ ;; buffer reliably, so we set up the new window ahead of time.
+ (switch-to-buffer-other-window (current-buffer))
+ (goto-char (marker-position origin)))
+ (+lookup--run-handler handler identifier)
+ t)
+ ((save-window-excursion
+ (and (or (+lookup--run-handler handler identifier)
+ (null origin)
+ (/= (point-marker) origin))
+ (point-marker))))))
+ ((error user-error debug)
+ (message "Lookup handler %S: %s" handler e)
+ nil)))
+
+(defun +lookup--jump-to (prop identifier &optional other-window)
+ (let ((result
+ (run-hook-wrapped
+ (plist-get (list :definition '+lookup-definition-functions
+ :references '+lookup-references-functions
+ :documentation '+lookup-documentation-functions
+ :file '+lookup-file-functions)
+ prop)
+ #'+lookup--run-handlers
+ identifier
+ (point-marker)
+ other-window)))
+ (if (not (markerp result))
+ (ignore (message "No lookup handler could find %S" identifier))
+ (funcall (if other-window
+ #'switch-to-buffer-other-window
+ #'switch-to-buffer)
+ (marker-buffer result))
+ (goto-char result)
+ (recenter)
+ result)))
+
+
+;;
+;;; Lookup backends
+
+(defun +lookup-xref-definitions-backend (identifier)
+ "Non-interactive wrapper for `xref-find-definitions'"
+ (xref-find-definitions identifier))
+
+(defun +lookup-xref-references-backend (identifier)
+ "Non-interactive wrapper for `xref-find-references'"
+ (xref-find-references identifier))
+
+(defun +lookup-dumb-jump-backend (_identifier)
+ "Look up the symbol at point (or selection) with `dumb-jump', which conducts a
+project search with ag, rg, pt, or git-grep, combined with extra heuristics to
+reduce false positives.
+
+This backend prefers \"just working\" over accuracy."
+ (when (require 'dumb-jump nil t)
+ ;; dumb-jump doesn't tell us if it succeeded or not
+ (plist-get (dumb-jump-go) :results)))
+
+(defun +lookup-project-search-backend (identifier)
+ "Conducts a simple project text search for IDENTIFIER.
+
+Uses and requires `+ivy-file-search' or `+helm-file-search'. Will return nil if
+neither is available. These search backends will use ag, rg, or pt (in an order
+dictated by `+ivy-project-search-engines' or `+helm-project-search-engines',
+falling back to git-grep)."
+ (unless identifier
+ (let ((query (rxt-quote-pcre identifier)))
+ (ignore-errors
+ (cond ((featurep! :completion ivy)
+ (+ivy-file-search nil :query query)
+ t)
+ ((featurep! :completion helm)
+ (+helm-file-search nil :query query)
+ t))))))
+
+(defun +lookup-evil-goto-definition-backend (_identifier)
+ "Uses `evil-goto-definition' to conduct a text search for IDENTIFIER in the
+current buffer."
+ (and (fboundp 'evil-goto-definition)
+ (ignore-errors
+ (cl-destructuring-bind (beg . end)
+ (bounds-of-thing-at-point 'symbol)
+ (evil-goto-definition)
+ (let ((pt (point)))
+ (not (and (>= pt beg)
+ (< pt end))))))))
+
+(defun +lookup-dash-docsets-backend (identifier)
+ "Looks up IDENTIFIER in available Dash docsets, if any are installed.
+
+Docsets must be installed with `+lookup/install-docset'. These can also be
+accessed via `+lookup/in-docsets'."
+ (and (featurep! +docsets)
+ (or (require 'counsel-dash nil t)
+ (require 'helm-dash nil t))
+ (let ((docsets (+lookup-docsets-for-buffer)))
+ (when (cl-some #'+lookup-docset-installed-p docsets)
+ (+lookup/in-docsets identifier docsets)
+ t))))
+
+
+;;
+;;; Main commands
+
+;;;###autoload
+(defun +lookup/definition (identifier &optional other-window)
+ "Jump to the definition of IDENTIFIER (defaults to the symbol at point).
+
+If OTHER-WINDOW (universal argument), open the result in another window.
+
+Each function in `+lookup-definition-functions' is tried until one changes the
+point or current buffer. Falls back to dumb-jump, naive
+ripgrep/the_silver_searcher text search, then `evil-goto-definition' if
+evil-mode is active."
+ (interactive
+ (list (+lookup--symbol-or-region)
+ current-prefix-arg))
+ (cond ((null identifier) (user-error "Nothing under point"))
+
+ ((+lookup--jump-to :definition identifier other-window))
+
+ ((error "Couldn't find the definition of '%s'" identifier))))
+
+;;;###autoload
+(defun +lookup/references (identifier &optional other-window)
+ "Show a list of usages of IDENTIFIER (defaults to the symbol at point)
+
+Tries each function in `+lookup-references-functions' until one changes the
+point and/or current buffer. Falls back to a naive ripgrep/the_silver_searcher
+search otherwise."
+ (interactive
+ (list (+lookup--symbol-or-region)
+ current-prefix-arg))
+ (cond ((null identifier) (user-error "Nothing under point"))
+
+ ((+lookup--jump-to :references identifier other-window))
+
+ ((error "Couldn't find references of '%s'" identifier))))
+
+;;;###autoload
+(defun +lookup/documentation (identifier &optional _arg)
+ "Show documentation for IDENTIFIER (defaults to symbol at point or selection.
+
+First attempts the :documentation handler specified with `set-lookup-handlers!'
+for the current mode/buffer (if any), then falls back to the backends in
+`+lookup-documentation-functions'."
+ (interactive
+ (list (+lookup--symbol-or-region)
+ current-prefix-arg))
+ (cond ((+lookup--jump-to :documentation identifier t))
+
+ ((user-error "Couldn't find documentation for '%s'" identifier))))
+
+(defvar ffap-file-finder)
+;;;###autoload
+(defun +lookup/file (path)
+ "Figure out PATH from whatever is at point and open it.
+
+Each function in `+lookup-file-functions' is tried until one changes the point
+or the current buffer.
+
+Otherwise, falls back on `find-file-at-point'."
+ (interactive
+ (progn
+ (require 'ffap)
+ (list
+ (or (ffap-guesser)
+ (ffap-read-file-or-url
+ (if ffap-url-regexp "Find file or URL: " "Find file: ")
+ (+lookup--symbol-or-region))))))
+ (require 'ffap)
+ (cond ((not path)
+ (call-interactively #'find-file-at-point))
+
+ ((ffap-url-p path)
+ (find-file-at-point path))
+
+ ((not (+lookup--jump-to :file path))
+ (let ((fullpath (expand-file-name path)))
+ (when (and buffer-file-name (file-equal-p fullpath buffer-file-name))
+ (user-error "Already here"))
+ (let* ((insert-default-directory t)
+ (project-root (doom-project-root))
+ (ffap-file-finder
+ (cond ((not (file-directory-p fullpath))
+ #'find-file)
+ ((file-in-directory-p fullpath project-root)
+ (lambda (dir)
+ (let ((default-directory dir))
+ (without-project-cache!
+ (let ((file (projectile-completing-read "Find file: "
+ (projectile-current-project-files)
+ :initial-input path)))
+ (find-file (expand-file-name file (doom-project-root)))
+ (run-hooks 'projectile-find-file-hook))))))
+ (#'doom-project-browse))))
+ (find-file-at-point path))))))
+
+
+;;
+;;; Source-specific commands
+
+(defvar counsel-dash-docsets)
+(defvar helm-dash-docsets)
+;;;###autoload
+(defun +lookup/in-docsets (&optional query docsets)
+ "Looks up QUERY (a string) in available Dash docsets for the current buffer.
+
+DOCSETS is a list of docset strings. Docsets can be installed with
+`+lookup/install-docset'."
+ (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))
+ ((user-error "No dash backend is installed, enable ivy or helm.")))))
+
+(after! evil
+ (evil-set-command-property '+lookup/definition :jump t)
+ (evil-set-command-property '+lookup/references :jump t)
+ (evil-set-command-property '+lookup/documentation :jump t)
+ (evil-set-command-property '+lookup/file :jump t))
diff --git a/modules/feature/lookup/autoload/online.el b/modules/feature/lookup/autoload/online.el
new file mode 100644
index 000000000..8d088480f
--- /dev/null
+++ b/modules/feature/lookup/autoload/online.el
@@ -0,0 +1,65 @@
+;;; feature/lookup/autoload/online.el -*- lexical-binding: t; -*-
+
+(defvar +lookup--last-provider nil)
+
+(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)))
+ (setf (alist-get key +lookup--last-provider) provider)
+ provider))))
+
+(defun +lookup-online-backend (identifier)
+ "Opens the browser and searches for IDENTIFIER online.
+
+Will prompt for which search engine to use the first time (or if the universal
+argument is non-nil)."
+ (+lookup/online
+ identifier
+ (+lookup--online-provider (not current-prefix-arg))))
+
+;;;###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
+ (let ((provider (+lookup--online-provider current-prefix-arg)))
+ (list (or (and (use-region-p)
+ (buffer-substring-no-properties (region-beginning)
+ (region-end)))
+ (read-string (format "Search for (on %s): " provider)
+ (thing-at-point 'symbol t)))
+ provider)))
+ (condition-case-unless-debug e
+ (let ((url (cdr (assoc provider +lookup-provider-url-alist))))
+ (unless url
+ (user-error "'%s' is an invalid search engine" provider))
+ (when (or (functionp url) (symbolp url))
+ (setq url (funcall url)))
+ (cl-assert (stringp 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
+ (delq (assq major-mode +lookup--last-provider)
+ +lookup--last-provider))
+ (signal (car e) (cdr e)))))
+
+;;;###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..701d98959
--- /dev/null
+++ b/modules/feature/lookup/config.el
@@ -0,0 +1,144 @@
+;;; 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/file': open the file referenced at point
+;; + `+lookup/online'; look up a symbol on online resources
+;; + `+lookup/in-docsets': look up in Dash docsets
+;;
+;; 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-definition-functions
+ '(+lookup-xref-definitions-backend
+ +lookup-dumb-jump-backend
+ +lookup-project-search-backend
+ +lookup-evil-goto-definition-backend)
+ "Functions for `+lookup/definition' to try, before resorting to `dumb-jump'.
+Stops at the first function to return non-nil or change the current
+window/point.
+
+If the argument is interactive (satisfies `commandp'), it is called with
+`call-interactively' (with no arguments). Otherwise, it is called with one
+argument: the identifier at point.")
+
+(defvar +lookup-references-functions
+ '(+lookup-xref-references-backend
+ +lookup-project-search-backend)
+ "Functions for `+lookup/references' to try, before resorting to `dumb-jump'.
+Stops at the first function to return non-nil or change the current
+window/point.
+
+If the argument is interactive (satisfies `commandp'), it is called with
+`call-interactively' (with no arguments). Otherwise, it is called with one
+argument: the identifier at point.")
+
+(defvar +lookup-documentation-functions
+ '(+lookup-dash-docsets-backend
+ +lookup-online-backend)
+ "Functions for `+lookup/documentation' to try, before resorting to
+`dumb-jump'. Stops at the first function to return non-nil or change the current
+window/point.
+
+If the argument is interactive (satisfies `commandp'), it is called with
+`call-interactively' (with no arguments). Otherwise, it is called with one
+argument: the identifier at point.")
+
+(defvar +lookup-file-functions ()
+ "Function for `+lookup/file' to try, before restoring to `find-file-at-point'.
+Stops at the first function to return non-nil or change the current
+window/point.
+
+If the argument is interactive (satisfies `commandp'), it is called with
+`call-interactively' (with no arguments). Otherwise, it is called with one
+argument: the identifier at point.")
+
+;; 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-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)
+ ('popup))))
+
+
+;;
+;;; 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))
+
+;; ...however, it breaks `projectile-find-tag', unless we put it back.
+(defun +lookup*projectile-find-tag (orig-fn)
+ (let ((xref-backend-functions '(etags--xref-backend t)))
+ (funcall orig-fn)))
+(advice-add #'projectile-find-tag :around #'+lookup*projectile-find-tag)
+
+
+(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
+
+;; Both packages depend on helm-dash, for now
+(def-package! helm-dash
+ :when (featurep! +docsets)
+ :defer t
+ :init
+ (setq helm-dash-enable-debugging doom-debug-mode
+ helm-dash-browser-func #'eww)
+ :config
+ (unless (file-directory-p helm-dash-docsets-path)
+ (setq helm-dash-docsets-path (concat doom-etc-dir "docsets/")))
+ (unless (file-directory-p helm-dash-docsets-path)
+ (make-directory helm-dash-docsets-path t)))
+
+(def-package! counsel-dash
+ :when (and (featurep! +docsets)
+ (featurep! :completion ivy))
+ :commands counsel-dash-install-docset
+ :config (setq counsel-dash-min-length 2))
diff --git a/modules/feature/lookup/packages.el b/modules/feature/lookup/packages.el
new file mode 100644
index 000000000..c127ebbfc
--- /dev/null
+++ b/modules/feature/lookup/packages.el
@@ -0,0 +1,21 @@
+;; -*- no-byte-compile: t; -*-
+;;; feature/lookup/packages.el
+
+;; `dumb-jump' uses the `helm-build-sync-source' macro, but this requires helm
+;; be loaded before it is byte-compiled during installation. To ensure this, we
+;; declare helm before dumb-jump.
+(when (featurep! :completion helm)
+ (package! helm))
+
+;;
+(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)))
diff --git a/modules/feature/services/config.el b/modules/feature/services/config.el
deleted file mode 100644
index 6430a8864..000000000
--- a/modules/feature/services/config.el
+++ /dev/null
@@ -1,50 +0,0 @@
-;;; feature/services/config.el -*- lexical-binding: t; -*-
-
-(def-setting! :service (&rest plist)
- "TODO"
- `(after! prodigy
- (prodigy-define-service ,@plist)))
-
-
-;;
-;; Plugins
-;;
-
-(def-package! prodigy
- :commands (prodigy prodigy-view-mode prodigy-add-filter)
- :config
- (set! :evil-state 'prodigy-mode 'emacs)
-
- ;; Make services, etc persistent between Emacs sessions
- (setq prodigy-services (persistent-soft-fetch 'prodigy-services "prodigy")
- prodigy-tags (persistent-soft-fetch 'prodigy-tags "prodigy")
- prodigy-filters (persistent-soft-fetch 'prodigy-filters "prodigy"))
-
- (defun +services|save ()
- "Save all services, tags and filters to files."
- (+services/cleanup)
- (cl-loop for sym in '(prodigy-services prodigy-tags prodigy-filters)
- do (persistent-soft-store sym (symbol-value sym) "prodigy")))
- (add-hook 'kill-emacs-hook #'+services|save)
-
- (defun +services*prodigy-services (orig-fn &rest args)
- "Adds a new :project property to prodigy services, which hides the service
-unless invoked from the relevant project."
- (let ((project-root (downcase (doom-project-root)))
- (services (apply orig-fn args)))
- (if current-prefix-arg
- services
- (cl-remove-if-not (lambda (service)
- (let ((project (plist-get service :project)))
- (or (not project)
- (file-in-directory-p project-root project))))
- services))))
- (advice-add #'prodigy-services :around #'+services*prodigy-services)
-
- ;; Keybindings
- (map! :map prodigy-mode-map "d" #'+services/prodigy-delete)
- (when (featurep! :feature evil)
- (map! :map prodigy-mode-map
- "j" #'prodigy-next
- "k" #'prodigy-prev)))
-
diff --git a/modules/feature/snippets/README.org b/modules/feature/snippets/README.org
index d22278f1b..7f28da666 100644
--- a/modules/feature/snippets/README.org
+++ b/modules/feature/snippets/README.org
@@ -1,12 +1,36 @@
-#+TITLE: :feature snippets
-
-This module adds snippets to Emacs, powered by yasnippet.
+#+TITLE: feature/snippets
+#+DATE: February 11, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
* Table of Contents :TOC:
-- [[#install][Install]]
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+ - [[Hacks][Hacks]]
+- [[Prerequisites][Prerequisites]]
+- [[Features][Features]]
+- [[Configuration][Configuration]]
+- [[Troubleshooting][Troubleshooting]]
-* Install
-There are no extra dependencies for this module.
+* Description
+This module adds snippets to Emacs, powered by yasnippet.
-By default, =private/default= installs a snippet library tailored exclusively
-for Doom Emacs.
+** Module Flags
+This module exposes no flags.
+
+** Plugins
++ [[https://github.com/joaotavora/yasnippet][yasnippet]]
++ [[https://github.com/abo-abo/auto-yasnippet][auto-yasnippet]]
++ [[https://github.com/hlissner/emacs-snippets][emacs-snippets]]
+
+** TODO Hacks
+
+* Prerequisites
+This module has no external dependencies.
+
+* TODO Features
+
+* TODO Configuration
+
+* TODO Troubleshooting
diff --git a/modules/feature/snippets/autoload/evil.el b/modules/feature/snippets/autoload/evil.el
index e0ef054b2..e56fffe85 100644
--- a/modules/feature/snippets/autoload/evil.el
+++ b/modules/feature/snippets/autoload/evil.el
@@ -11,8 +11,6 @@ and switches to insert mode if there are editable fields."
(cl-letf (((symbol-function 'region-beginning) (lambda () evil-visual-beginning))
((symbol-function 'region-end) (lambda () evil-visual-end)))
(yas-insert-snippet))
- (when-let* ((snippet (car-safe (yas-active-snippets))))
- (let ((fields (yas--snippet-fields snippet)))
- (evil-insert-state +1)
- (unless fields (evil-change-state 'normal)))))
+ (when (yas-active-snippets)
+ (evil-insert-state +1)))
diff --git a/modules/feature/snippets/autoload/settings.el b/modules/feature/snippets/autoload/settings.el
new file mode 100644
index 000000000..6bb99df22
--- /dev/null
+++ b/modules/feature/snippets/autoload/settings.el
@@ -0,0 +1,10 @@
+;;; feature/snippets/autoload/settings.el -*- lexical-binding: t; -*-
+
+;;;###autodef
+(defun set-yas-minor-mode! (modes)
+ "Register minor MODES (one mode symbol or a list of them) with yasnippet so it
+can have its own snippets category, if the folder exists."
+ (dolist (mode (doom-enlist modes))
+ (let ((fn (intern (format "+snippets|register-%s" mode))))
+ (fset fn (lambda () (yas-activate-extra-mode mode)))
+ (add-hook (intern (format "%s-hook" mode)) fn))))
diff --git a/modules/feature/snippets/autoload/snippets.el b/modules/feature/snippets/autoload/snippets.el
index 7c972e4ae..35f3b8ee9 100644
--- a/modules/feature/snippets/autoload/snippets.el
+++ b/modules/feature/snippets/autoload/snippets.el
@@ -1,11 +1,39 @@
;;; feature/snippets/autoload/snippets.el -*- lexical-binding: t; -*-
+(defun +snippets--remove-p (x y)
+ (and (equal (yas--template-key x) (yas--template-key y))
+ (file-in-directory-p (yas--template-get-file x) doom-emacs-dir)))
+
+;;;###autoload
+(defun +snippets-prompt-private (prompt choices &optional display-fn)
+ "Prioritize private snippets over built-in ones if there are multiple
+choices.
+
+There are two groups of snippets in Doom Emacs. The built in ones (under
+`doom-emacs-dir'; provided by Doom or its plugins) or your private snippets
+(outside of `doom-eamcs-dir').
+
+If there are multiple snippets with the same key in either camp (but not both),
+you will be prompted to select one.
+
+If there are conflicting keys across the two camps, the built-in ones are
+ignored. This makes it easy to override built-in snippets with private ones."
+ (when (memq this-command '(yas-expand +snippets/expand-on-region))
+ (let* ((gc-cons-threshold doom-gc-cons-upper-limit)
+ (choices (cl-remove-duplicates choices :test #'+snippets--remove-p)))
+ (if (cdr choices)
+ (cl-loop for fn in (cdr (memq '+snippets-prompt-private yas-prompt-functions))
+ if (funcall fn prompt choices display-fn)
+ return it)
+ (car choices)))))
+
;;;###autoload
(defun +snippets/goto-start-of-field ()
"Go to the beginning of the current field."
(interactive)
(let* ((snippet (car (yas-active-snippets)))
- (position (yas--field-start (yas--snippet-active-field snippet))))
+ (active-field (yas--snippet-active-field snippet))
+ (position (if (yas--field-p active-field) (yas--field-start active-field) -1)))
(if (= (point) position)
(move-beginning-of-line 1)
(goto-char position))))
@@ -15,7 +43,8 @@
"Go to the end of the current field."
(interactive)
(let* ((snippet (car (yas-active-snippets)))
- (position (yas--field-end (yas--snippet-active-field snippet))))
+ (active-field (yas--snippet-active-field snippet))
+ (position (if (yas--field-p active-field) (yas--field-end active-field) -1)))
(if (= (point) position)
(move-end-of-line 1)
(goto-char position))))
@@ -24,11 +53,12 @@
(defun +snippets/delete-backward-char (&optional field)
"Prevents Yas from interfering with backspace deletion."
(interactive)
- (let ((field (or field (and yas--active-field-overlay
+ (let ((field (or field (and (overlayp yas--active-field-overlay)
(overlay-buffer yas--active-field-overlay)
(overlay-get yas--active-field-overlay 'yas--field)))))
- (cond ((eq (point) (marker-position (yas--field-start field))) nil)
- (t (delete-char -1)))))
+ (unless (and (yas--field-p field)
+ (eq (point) (marker-position (yas--field-start field))))
+ (call-interactively #'delete-backward-char))))
;;;###autoload
(defun +snippets/delete-forward-char-or-field (&optional field)
@@ -38,22 +68,55 @@ buggy behavior when is pressed in an empty field."
(let ((field (or field (and yas--active-field-overlay
(overlay-buffer yas--active-field-overlay)
(overlay-get yas--active-field-overlay 'yas--field)))))
- (cond ((and field
- (not (yas--field-modified-p field))
+ (cond ((not (yas--field-p field))
+ (delete-char 1))
+ ((and (not (yas--field-modified-p field))
(eq (point) (marker-position (yas--field-start field))))
(yas--skip-and-clear field)
(yas-next-field 1))
((eq (point) (marker-position (yas--field-end field))) nil)
- (t (delete-char 1)))))
+ ((delete-char 1)))))
;;;###autoload
(defun +snippets/delete-to-start-of-field (&optional field)
"Delete to start-of-field."
(interactive)
- (let* ((field (or field (and yas--active-field-overlay
- (overlay-buffer yas--active-field-overlay)
- (overlay-get yas--active-field-overlay 'yas--field))))
- (sof (marker-position (yas--field-start field))))
- (when (and field (> (point) sof))
- (delete-region sof (point)))))
+ (unless field
+ (setq field (and (overlayp yas--active-field-overlay)
+ (overlay-buffer yas--active-field-overlay)
+ (overlay-get yas--active-field-overlay 'yas--field))))
+ (when (yas--field-p field)
+ (let ((sof (marker-position (yas--field-start field))))
+ (when (and field (> (point) sof))
+ (delete-region sof (point))))))
+
+
+;;
+;; Hooks
+
+;;;###autoload
+(defun +snippets|enable-project-modes (mode &rest _)
+ "Automatically enable snippet libraries for project minor modes defined with
+`def-project-mode!'."
+ (if (symbol-value mode)
+ (yas-activate-extra-mode mode)
+ (yas-deactivate-extra-mode mode)))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun +snippets/browse (_arg)
+ "TODO"
+ (interactive "P")
+ (doom-project-browse +snippets-dir))
+
+;;;###autoload
+(defun +snippets/find-file ()
+ "TODO"
+ (interactive)
+ (if (file-directory-p +snippets-dir)
+ (doom-project-find-file +snippets-dir)
+ (yas-visit-snippet-file)))
diff --git a/modules/feature/snippets/config.el b/modules/feature/snippets/config.el
index 68c1a9a97..50ce7fce7 100644
--- a/modules/feature/snippets/config.el
+++ b/modules/feature/snippets/config.el
@@ -1,47 +1,59 @@
;;; feature/snippets/config.el -*- lexical-binding: t; -*-
-;; Snippets! I've thrown together a few hacks to make `yasnippet' and `evil'
-;; behave together.
+(defvar +snippets-dir (expand-file-name "snippets/" doom-private-dir)
+ "Directory where `yasnippet' will search for your private snippets.")
+
+
+;;
+;; Packages
(def-package! yasnippet
- :commands (yas-minor-mode yas-minor-mode-on yas-expand yas-expand-snippet
- yas-lookup-snippet yas-insert-snippet yas-new-snippet
- yas-visit-snippet-file snippet-mode)
- :preface
- (defvar yas-minor-mode-map (make-sparse-keymap))
-
+ :commands (yas-minor-mode-on yas-expand yas-expand-snippet yas-lookup-snippet
+ yas-insert-snippet yas-new-snippet yas-visit-snippet-file)
:init
;; Ensure `yas-reload-all' is called as late as possible. Other modules could
;; have additional configuration for yasnippet. For example, file-templates.
(add-transient-hook! 'yas-minor-mode-hook (yas-reload-all))
- (add-hook! (text-mode prog-mode snippet-mode)
+ (add-hook! (text-mode prog-mode conf-mode snippet-mode)
#'yas-minor-mode-on)
:config
(setq yas-verbosity (if doom-debug-mode 3 0)
yas-also-auto-indent-first-line t
- yas-prompt-functions (delq 'yas-dropdown-prompt yas-prompt-functions)
- ;; Allow nested snippets
- yas-triggers-in-field t)
+ yas-triggers-in-field t ; Allow nested snippets
+ ;; Remove default ~/.emacs.d/snippets
+ yas-snippet-dirs (delete yas--default-user-snippets-dir yas-snippet-dirs))
- ;; Allows project-specific snippets
- (defun +snippets|enable-project-modes (mode &rest _)
- "Enable snippets for project modes."
- (if (symbol-value mode)
- (yas-activate-extra-mode mode)
- (yas-deactivate-extra-mode mode)))
+ ;; Allow private snippets in DOOMDIR/snippets
+ (add-to-list 'yas-snippet-dirs '+snippets-dir nil #'eq)
+
+ ;; Remove GUI dropdown prompt (prefer ivy/helm)
+ (setq yas-prompt-functions (delq 'yas-dropdown-prompt yas-prompt-functions))
+ ;; Prioritize private snippets in `+snippets-dir' over built-in ones if there
+ ;; are multiple choices.
+ (add-to-list 'yas-prompt-functions #'+snippets-prompt-private nil #'eq)
+
+ ;; Register `def-project-mode!' modes with yasnippet. This enables project
+ ;; specific snippet libraries (e.g. for Laravel, React or Jekyll projects).
(add-hook 'doom-project-hook #'+snippets|enable-project-modes)
- ;; fix an error caused by smartparens interfering with yasnippet bindings
- (advice-add #'yas-expand :before #'sp-remove-active-pair-overlay)
-
;; Exit snippets on ESC from normal mode
- (add-hook '+evil-esc-hook #'yas-exit-all-snippets))
+ (add-hook 'doom-escape-hook #'yas-abort-snippet)
+
+ (after! smartparens
+ ;; tell smartparens overlays not to interfere with yasnippet keybinds
+ (advice-add #'yas-expand :before #'sp-remove-active-pair-overlay))
+
+ (when (featurep! :feature evil)
+ ;; evil visual-mode integration for `yas-insert-snippet'
+ (define-key yas-minor-mode-map [remap yas-insert-snippet] #'+snippets/expand-on-region)))
-(def-package! auto-yasnippet
- :commands (aya-create aya-expand aya-open-line aya-persist-snippet)
- :config
- (setq aya-persist-snippets-dir (concat doom-local-dir "auto-snippets/")))
+;; `auto-yasnippet'
+(setq aya-persist-snippets-dir (concat doom-etc-dir "auto-snippets/"))
+
+;; default snippets library
+(def-package! emacs-snippets
+ :after yasnippet)
diff --git a/modules/feature/snippets/packages.el b/modules/feature/snippets/packages.el
index b02077c18..4ace1fb90 100644
--- a/modules/feature/snippets/packages.el
+++ b/modules/feature/snippets/packages.el
@@ -4,3 +4,7 @@
(package! yasnippet)
(package! auto-yasnippet)
+(package! emacs-snippets
+ :recipe (:fetcher github
+ :repo "hlissner/emacs-snippets"
+ :files ("*")))
diff --git a/modules/feature/spellcheck/config.el b/modules/feature/spellcheck/config.el
deleted file mode 100644
index 37e9b1cc0..000000000
--- a/modules/feature/spellcheck/config.el
+++ /dev/null
@@ -1,23 +0,0 @@
-;;; feature/spellcheck/config.el -*- lexical-binding: t; -*-
-
-(def-package! flyspell ; built-in
- :commands flyspell-mode
- :config
- (setq ispell-program-name (executable-find "aspell")
- ispell-list-command "--list"
- ispell-extr-args '("--dont-tex-check-comments")))
-
-
-(def-package! flyspell-correct
- :commands (flyspell-correct-word-generic
- flyspell-correct-previous-word-generic)
- :config
- (cond ((featurep! :completion helm)
- (require 'flyspell-correct-helm))
- ((featurep! :completion ivy)
- (require 'flyspell-correct-ivy))
- (t
- (require 'flyspell-correct-popup)
- (setq flyspell-popup-correct-delay 0.8)
- (define-key popup-menu-keymap [escape] #'keyboard-quit))))
-
diff --git a/modules/feature/syntax-checker/config.el b/modules/feature/syntax-checker/config.el
deleted file mode 100644
index 2fb58bbad..000000000
--- a/modules/feature/syntax-checker/config.el
+++ /dev/null
@@ -1,30 +0,0 @@
-;;; feature/syntax-checker/config.el -*- lexical-binding: t; -*-
-
-;; pkg-info doesn't get autoloaded when `flycheck-version' needs it, so we do
-;; it ourselves:
-(autoload 'pkg-info-version-info "pkg-info")
-
-(def-package! flycheck
- :commands (flycheck-mode flycheck-list-errors flycheck-buffer)
- :config
- ;; Emacs feels snappier without checks on idle/change
- (setq flycheck-check-syntax-automatically '(save mode-enabled))
-
- (set! :popup 'flycheck-error-list-mode :select t :autokill t)
-
- (after! evil
- ;; Flycheck buffer on ESC in normal mode.
- (defun +syntax-checkers|flycheck-buffer ()
- (when flycheck-mode
- (ignore-errors (flycheck-buffer))
- nil))
- (add-hook '+evil-esc-hook #'+syntax-checkers|flycheck-buffer t)))
-
-
-(def-package! flycheck-pos-tip
- :after flycheck
- :config
- (setq flycheck-pos-tip-timeout 10
- flycheck-display-errors-delay 0.5)
- (flycheck-pos-tip-mode +1))
-
diff --git a/modules/feature/syntax-checker/packages.el b/modules/feature/syntax-checker/packages.el
deleted file mode 100644
index 540671c12..000000000
--- a/modules/feature/syntax-checker/packages.el
+++ /dev/null
@@ -1,6 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; feature/syntax-checker/packages.el
-
-(package! flycheck)
-(package! flycheck-pos-tip)
-
diff --git a/modules/feature/version-control/+git.el b/modules/feature/version-control/+git.el
deleted file mode 100644
index 8a8770880..000000000
--- a/modules/feature/version-control/+git.el
+++ /dev/null
@@ -1,94 +0,0 @@
-;;; feature/version-control/+git.el -*- lexical-binding: t; -*-
-;;;###if (not (featurep! -git))
-
-(when (featurep! :feature evil)
- (add-hook 'git-commit-mode-hook #'evil-insert-state))
-
-
-(def-package! gitconfig-mode
- :mode "/\\.?git/?config$"
- :mode "/\\.gitmodules$")
-
-
-(def-package! gitignore-mode
- :mode "/\\.gitignore$")
-
-
-(def-package! git-gutter-fringe
- :commands git-gutter-mode
- :init
- (defun +version-control|git-gutter-maybe ()
- "Enable `git-gutter-mode' in non-remote buffers."
- (when (and (buffer-file-name)
- (not (file-remote-p (buffer-file-name))))
- (git-gutter-mode +1)))
- (add-hook! (text-mode prog-mode conf-mode) #'+version-control|git-gutter-maybe)
- :config
- (set! :popup "^\\*git-gutter.+\\*$" :regexp t :size 15 :noselect t)
-
- ;; Update git-gutter on focus (in case I was using git externally)
- (add-hook 'focus-in-hook #'git-gutter:update-all-windows)
-
- (after! evil
- (defun +version-control|update-git-gutter ()
- "Refresh git-gutter on ESC. Return nil to prevent shadowing other
-`+evil-esc-hook' hooks."
- (when git-gutter-mode
- (ignore (git-gutter))))
- (add-hook '+evil-esc-hook #'+version-control|update-git-gutter t))
-
- (def-hydra! +version-control@git-gutter
- (:body-pre (git-gutter-mode 1) :hint nil)
- "
- ╭─────────────────┐
- Movement Hunk Actions Misc. │ gg: +%-4s(car (git-gutter:statistic))/ -%-3s(cdr (git-gutter:statistic)) │
- ╭──────────────────────────────────┴─────────────────╯
- ^_g_^ [_s_] stage [_R_] set start Rev
- ^_k_^ [_r_] revert
- ^↑ ^ [_m_] mark
- ^↓ ^ [_p_] popup ╭──────────────────────
- ^_j_^ │[_q_] quit
- ^_G_^ │[_Q_] Quit and disable"
- ("j" (progn (git-gutter:next-hunk 1) (recenter)))
- ("k" (progn (git-gutter:previous-hunk 1) (recenter)))
- ("g" (progn (goto-char (point-min)) (git-gutter:next-hunk 1)))
- ("G" (progn (goto-char (point-min)) (git-gutter:previous-hunk 1)))
- ("s" git-gutter:stage-hunk)
- ("r" git-gutter:revert-hunk)
- ("m" git-gutter:mark-hunk)
- ("p" git-gutter:popup-hunk)
- ("R" git-gutter:set-start-revision)
- ("q" nil :color blue)
- ("Q" (git-gutter-mode -1) :color blue)))
-
-
-(def-package! git-timemachine
- :commands (git-timemachine git-timemachine-toggle)
- :config
- (require 'magit-blame)
-
- ;; Sometimes I forget `git-timemachine' is enabled in a buffer, so instead of
- ;; showing revision details in the minibuffer, show them in
- ;; `header-line-format', which has better visibility.
- (setq git-timemachine-show-minibuffer-details nil)
- (add-hook 'git-timemachine-mode-hook #'+vcs|init-header-line)
- (advice-add #'git-timemachine-show-revision :after #'+vcs*update-header-line)
-
- ;; Force evil to rehash keybindings for the current state
- (add-hook 'git-timemachine-mode-hook #'evil-force-normal-state))
-
-
-(def-package! magit
- :commands (magit-status magit-blame)
- :config
- (set! :evil-state 'magit-status-mode 'emacs)
- (after! evil
- ;; Switch to emacs state only while in `magit-blame-mode', then back when
- ;; its done (since it's a minor-mode).
- (add-hook! 'magit-blame-mode-hook
- (evil-local-mode (if magit-blame-mode -1 +1)))))
-
-
-(def-package! git-link
- :commands (git-link git-link-commit git-link-homepage))
-
diff --git a/modules/feature/version-control/autoload.el b/modules/feature/version-control/autoload.el
deleted file mode 100644
index 6df86c3d0..000000000
--- a/modules/feature/version-control/autoload.el
+++ /dev/null
@@ -1,70 +0,0 @@
-;;; feature/version-control/autoload.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun +vcs-root ()
- "Get git url root."
- (require 'git-link)
- (let ((remote (git-link--select-remote)))
- (if (git-link--remote-host remote)
- (format "https://%s/%s"
- (git-link--remote-host remote)
- (git-link--remote-dir remote))
- (error "Remote `%s' is unknown or contains an unsupported URL" remote))))
-
-(defvar git-link-open-in-browser)
-;;;###autoload
-(defun +vcs/git-browse ()
- "Open the website for the current version controlled file. Fallback to
-repository root."
- (interactive)
- (require 'git-link)
- (cl-destructuring-bind (beg end)
- (if buffer-file-name (git-link--get-region))
- (let ((git-link-open-in-browser t))
- (git-link (git-link--select-remote) beg end))))
-
-;;;###autoload
-(defun +vcs/git-browse-issues ()
- "Open the github issues page for current repo."
- (interactive)
- (if-let* ((root (+vcs-root)))
- (browse-url (concat root "/issues"))
- (user-error "No git root found!")))
-
-;;;###autoload
-(defun +vcs|init-header-line ()
- "Toggle the git-timemachine header-line on activate. Use this on
-`git-timemachine-mode-hook'."
- (if git-timemachine-mode
- (+vcs*update-header-line)
- (setq-local header-line-format nil)))
-
-;;;###autoload
-(defun +vcs|enable-smerge-mode-maybe ()
- "Auto-enable `smerge-mode' when merge conflict is detected."
- (save-excursion
- (goto-char (point-min))
- (when (re-search-forward "^<<<<<<< " nil :noerror)
- (smerge-mode 1)
- (when (and (featurep 'hydra)
- +vcs-auto-hydra-smerge)
- (+hydra-smerge/body)))))
-
-;;;###autoload
-(defun +vcs*update-header-line (&rest _)
- "Show revision details in the header-line, instead of the minibuffer.
-
-Sometimes I forget `git-timemachine' is enabled in a buffer. Putting info into,
-putting them in `header-line-format' has better visibility."
- (when (and git-timemachine-mode git-timemachine-revision)
- (let* ((revision git-timemachine-revision)
- (date-relative (nth 3 revision))
- (date-full (nth 4 revision))
- (author (if git-timemachine-show-author (concat (nth 6 revision) ": ") ""))
- (sha-or-subject (if (eq git-timemachine-minibuffer-detail 'commit) (car revision) (nth 5 revision))))
- (setq-local
- header-line-format
- (format "%s%s [%s (%s)]"
- (propertize author 'face 'git-timemachine-minibuffer-author-face)
- (propertize sha-or-subject 'face 'git-timemachine-minibuffer-detail-face)
- date-full date-relative)))))
diff --git a/modules/feature/version-control/config.el b/modules/feature/version-control/config.el
deleted file mode 100644
index f13540e2e..000000000
--- a/modules/feature/version-control/config.el
+++ /dev/null
@@ -1,68 +0,0 @@
-;;; feature/version-control/config.el -*- lexical-binding: t; -*-
-
-(or (featurep! -git) (load! +git))
-;; TODO (or (featurep! -hg) (load! +hg))
-
-;;
-(setq vc-make-backup-files nil)
-
-(defvar +vcs-auto-hydra-smerge t
- "When entering `smerge-mode' automatically open associated hydra.")
-
-
-(after! vc-annotate
- (set! :popup
- '("*vc-diff*" :size 15 :noselect t)
- '("*vc-change-log*" :size 15)
- '(vc-annotate-mode :same t))
-
- (set! :evil-state 'vc-annotate-mode 'normal)
- (set! :evil-state 'vc-git-log-view-mode 'normal))
-
-(def-package! smerge-mode
- :hook (find-file . +vcs|enable-smerge-mode-maybe)
- :config
- (when (version< emacs-version "26")
- (with-no-warnings
- (defalias #'smerge-keep-upper #'smerge-keep-mine)
- (defalias #'smerge-keep-lower #'smerge-keep-other)
- (defalias #'smerge-diff-base-upper #'smerge-diff-base-mine)
- (defalias #'smerge-diff-upper-lower #'smerge-diff-mine-other)
- (defalias #'smerge-diff-base-lower #'smerge-diff-base-other)))
-
- (def-hydra! +hydra-smerge (:hint nil
- :pre (smerge-mode 1)
- ;; Disable `smerge-mode' when quitting hydra if
- ;; no merge conflicts remain.
- :post (smerge-auto-leave))
- "
- ╭────────┐
- Movement Keep Diff Other │ smerge │
- ╭─────────────────────────────────────────────────┴────────╯
- ^_g_^ [_b_] base [_<_] upper/base [_C_] Combine
- ^_C-k_^ [_u_] upper [_=_] upper/lower [_r_] resolve
- ^_k_ ↑^ [_l_] lower [_>_] base/lower [_R_] remove
- ^_j_ ↓^ [_a_] all [_H_] hightlight
- ^_C-j_^ [_RET_] current [_E_] ediff ╭──────────
- ^_G_^ │ [_q_] quit"
- ("g" (progn (goto-char (point-min)) (smerge-next)))
- ("G" (progn (goto-char (point-max)) (smerge-prev)))
- ("C-j" smerge-next)
- ("C-k" smerge-prev)
- ("j" next-line)
- ("k" previous-line)
- ("b" smerge-keep-base)
- ("u" smerge-keep-upper)
- ("l" smerge-keep-lower)
- ("a" smerge-keep-all)
- ("RET" smerge-keep-current)
- ("\C-m" smerge-keep-current)
- ("<" smerge-diff-base-upper)
- ("=" smerge-diff-upper-lower)
- (">" smerge-diff-base-lower)
- ("H" smerge-refine)
- ("E" smerge-ediff)
- ("C" smerge-combine-with-next)
- ("r" smerge-resolve)
- ("R" smerge-kill-current)
- ("q" nil :color blue)))
diff --git a/modules/feature/version-control/packages.el b/modules/feature/version-control/packages.el
deleted file mode 100644
index 05623f80b..000000000
--- a/modules/feature/version-control/packages.el
+++ /dev/null
@@ -1,16 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; feature/version-control/packages.el
-
-;;; config.el
-;; n/a
-
-;;; +git
-(unless (featurep! -git)
- (package! git-gutter-fringe)
- (package! git-link)
- (package! git-timemachine)
- (package! gitconfig-mode)
- (package! gitignore-mode)
- (package! magit))
-
-;;; TODO +hg
diff --git a/modules/feature/workspaces/README.org b/modules/feature/workspaces/README.org
index 29f8dec1a..877280479 100644
--- a/modules/feature/workspaces/README.org
+++ b/modules/feature/workspaces/README.org
@@ -61,9 +61,9 @@ in [[../../private/default/+evil-commands.el][private/default/+evil-commands.el]
| ~+workspace/new~ | =SPC TAB n= | Create a new, blank workspace |
| ~+workspace/display~ | =SPC TAB TAB= | Display open workspaces in the mode-line |
| ~+workspace/load~ | =SPC TAB l= | Load a saved workspace into the current session |
-| ~+workspace/load-session~ | =SPC TAB L= / =:sl[oad]= | Replace current session with a saved one |
+| ~doom/quicksave-load~ | =SPC TAB L= / =:sl[oad]= | Replace current session with a saved one |
| ~+workspace/save~ | =SPC TAB s= | Save the current workspace to a file |
-| ~+workspace/save-session~ | =SPC TAB S= / =:ss[ave]= | Save current session |
+| ~doom/quicksave-save~ | =SPC TAB S= / =:ss[ave]= | Save current session |
| ~+workspace/switch-to~ | =SPC TAB .= | Switch to an open workspace |
| ~+workspace/switch-left~ | =SPC TAB [= / =[ w= / =gT= | Switch to previous workspace |
| ~+workspace/switch-right~ | =SPC TAB [= / =] w= / =gt= | Switch to next workspace |
diff --git a/modules/feature/workspaces/autoload/evil.el b/modules/feature/workspaces/autoload/evil.el
index 16f75c4e6..a18d06f74 100644
--- a/modules/feature/workspaces/autoload/evil.el
+++ b/modules/feature/workspaces/autoload/evil.el
@@ -1,20 +1,6 @@
;;; feature/workspaces/autoload/evil.el -*- lexical-binding: t; -*-
;;;###if (featurep! :feature evil)
-;;;###autoload (autoload '+workspace:save-session "feature/workspaces/autoload/evil" nil t)
-(evil-define-command +workspace:save-session (&optional bang name)
- "Ex wrapper around `+workspace/save-session'. If BANG, then autosave
-(pointless if autosaving/loading is off). If NAME is nil, default to 'last'."
- (interactive "")
- (+workspace/save-session (if bang persp-auto-save-fname name)))
-
-;;;###autoload (autoload '+workspace:load-session "feature/workspaces/autoload/evil" nil t)
-(evil-define-command +workspace:load-session (&optional bang name)
- "Ex wrapper around `+workspace/load-session'. If BANG, then load last autosave
-(pointless if autosaving/loading is off). If NAME is nil, defaults to 'last'."
- (interactive "")
- (+workspace/load-session (if bang persp-auto-save-fname name)))
-
;;;###autoload (autoload '+workspace:save "feature/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:save (&optional name)
"Ex wrapper around `+workspace/save-session'."
diff --git a/modules/feature/workspaces/autoload/workspaces.el b/modules/feature/workspaces/autoload/workspaces.el
index 7aff9382c..61b602f36 100644
--- a/modules/feature/workspaces/autoload/workspaces.el
+++ b/modules/feature/workspaces/autoload/workspaces.el
@@ -1,24 +1,21 @@
;;; feature/workspaces/autoload/workspaces.el -*- lexical-binding: t; -*-
-(defvar +workspace-data-file "_workspaces"
- "The file basename in which to store single workspace perspectives.")
-
(defvar +workspace--last nil)
(defvar +workspace--index 0)
-;;
-(defface +workspace-tab-selected-face '((t (:inherit 'highlight)))
+;;;###autoload
+(defface +workspace-tab-selected-face '((t (:inherit highlight)))
"The face for selected tabs displayed by `+workspace/display'"
- :group 'doom)
+ :group 'persp-mode)
-(defface +workspace-tab-face '((t (:inherit 'default)))
+;;;###autoload
+(defface +workspace-tab-face '((t (:inherit default)))
"The face for selected tabs displayed by `+workspace/display'"
- :group 'doom)
+ :group 'persp-mode)
;;
;; Library
-;;
(defun +workspace--protected-p (name)
(equal name persp-nil-name))
@@ -34,65 +31,70 @@
;; --- Predicates -------------------------
;;;###autoload
-(defalias #'+workspace-p #'persp-p "Return t if OBJ is a perspective hash table.")
+(defalias #'+workspace-p #'perspective-p
+ "Return t if OBJ is a perspective hash table.")
;;;###autoload
(defun +workspace-exists-p (name)
"Returns t if NAME is the name of an existing workspace."
- (cl-assert (stringp name) t)
(member name (+workspace-list-names)))
;;;###autoload
-(defun +workspace-contains-buffer-p (buffer &optional workspace)
- "Return non-nil if buffer is in workspace (defaults to current workspace)."
- (persp-contain-buffer-p buffer (or workspace (+workspace-current)) nil))
+(defalias #'+workspace-contains-buffer-p #'persp-contain-buffer-p
+ "Return non-nil if BUFFER is in WORKSPACE (defaults to current workspace).")
;; --- Getters ----------------------------
;;;###autoload
-(defun +workspace-get (name &optional noerror)
- "Returns a workspace (perspective hash table) named NAME."
- (when-let* ((persp (persp-get-by-name name)))
- (cond ((+workspace-p persp) persp)
- ((not noerror) (error "'%s' is an invalid workspace" name)))))
+(defalias #'+workspace-current #'get-current-persp
+ "Return the currently active workspace.")
;;;###autoload
-(defalias '+workspace-current #'get-current-persp)
+(defun +workspace-get (name &optional noerror)
+ "Return a workspace named NAME. Unless NOERROR is non-nil, this throws an
+error if NAME doesn't exist."
+ (cl-check-type name string)
+ (when-let* ((persp (persp-get-by-name name)))
+ (cond ((+workspace-p persp) persp)
+ ((not noerror)
+ (error "No workspace called '%s' was found" name)))))
;;;###autoload
(defun +workspace-current-name ()
"Get the name of the current workspace."
- (safe-persp-name (get-current-persp)))
+ (safe-persp-name (+workspace-current)))
;;;###autoload
(defun +workspace-list ()
- "Return a list of workspace structs."
- (mapcar #'persp-get-by-name (+workspace-list-names)))
+ "Return a list of workspace structs (satisifes `+workspace-p')."
+ (cdr (cl-loop for persp being the hash-values of *persp-hash*
+ collect persp)))
;;;###autoload
(defun +workspace-list-names ()
- "Return a list of workspace names (strings)."
- (delete persp-nil-name (persp-names-current-frame-fast-ordered)))
+ "Return the list of names of open workspaces."
+ (cdr persp-names-cache))
;;;###autoload
(defun +workspace-buffer-list (&optional persp)
- "Return a list of buffers in PERSP (defaults to the current perspective).
+ "Return a list of buffers in PERSP.
The buffer list is ordered by recency (same as `buffer-list').
-PERSP can be a string (name of a workspace) or a perspective hash (satisfies
-`+workspace-p').
-
-If PERSP is t, then return a list of orphaned buffers associated with no
-perspectives."
+PERSP can be a string (name of a workspace) or a workspace (satisfies
+`+workspace-p'). If nil or omitted, it defaults to the current workspace."
(let ((persp (or persp (+workspace-current))))
- (if (eq persp t)
- (cl-remove-if #'persp--buffer-in-persps (buffer-list))
- (cl-assert (+workspace-p persp) t)
- (cl-loop for buf in (buffer-list)
- if (+workspace-contains-buffer-p buf persp)
- collect buf))))
+ (unless (+workspace-p persp)
+ (user-error "Not in a valid workspace (%s)" persp))
+ (cl-loop for buf in (buffer-list)
+ if (+workspace-contains-buffer-p buf persp)
+ collect buf)))
+
+;;;###autoload
+(defun +workspace-orphaned-buffer-list ()
+ "Return a list of buffers that aren't associated with any perspective."
+ (cl-remove-if #'persp--buffer-in-persps (buffer-list)))
;; --- Actions ----------------------------
@@ -104,19 +106,12 @@ retrieve perspectives that were explicitly saved with `+workspace-save'.
Returns t if successful, nil otherwise."
(when (+workspace-exists-p name)
- (error "A workspace named '%s' already exists." name))
+ (user-error "A workspace named '%s' already exists." name))
(persp-load-from-file-by-names
- (expand-file-name +workspace-data-file persp-save-dir)
+ (expand-file-name +workspaces-data-file persp-save-dir)
*persp-hash* (list name))
(+workspace-exists-p name))
-;;;###autoload
-(defun +workspace-load-session (&optional name)
- "Replace current session with the entire session named NAME. If NAME is nil,
-use `persp-auto-save-fname'."
- (persp-load-state-from-file
- (expand-file-name (or name persp-auto-save-fname) persp-save-dir)))
-
;;;###autoload
(defun +workspace-save (name)
"Saves a single workspace (NAME) from the current session. Can be loaded again
@@ -126,24 +121,11 @@ perspective hash table.
Returns t on success, nil otherwise."
(unless (+workspace-exists-p name)
(error "'%s' is an invalid workspace" name))
- (let ((fname (expand-file-name +workspace-data-file persp-save-dir)))
+ (let ((fname (expand-file-name +workspaces-data-file persp-save-dir)))
(persp-save-to-file-by-names fname *persp-hash* (list name))
(and (member name (persp-list-persp-names-in-file fname))
t)))
-;;;###autoload
-(defun +workspace-save-session (&optional name)
- "Save a whole session as NAME. If NAME is nil, use `persp-auto-save-fname'.
-Return t on success, nil otherwise."
- (let ((fname (expand-file-name (or name persp-auto-save-fname)
- persp-save-dir))
- (persp-auto-save-opt
- (if (or (not name)
- (equal name persp-auto-save-fname))
- 0
- persp-auto-save-opt)))
- (and (persp-save-state-to-file fname) t)))
-
;;;###autoload
(defun +workspace-new (name)
"Create a new workspace named NAME. If one already exists, return nil.
@@ -152,7 +134,16 @@ Otherwise return t on success, nil otherwise."
(error "Can't create a new '%s' workspace" name))
(when (+workspace-exists-p name)
(error "A workspace named '%s' already exists" name))
- (and (persp-add-new name) t))
+ (let ((persp (persp-add-new name))
+ (+popup--inhibit-transient t))
+ (save-window-excursion
+ (let ((ignore-window-parameters t)
+ (+popup--inhibit-transient t))
+ (delete-other-windows))
+ (switch-to-buffer (doom-fallback-buffer))
+ (setf (persp-window-conf persp)
+ (funcall persp-window-state-get-function (selected-frame))))
+ persp))
;;;###autoload
(defun +workspace-rename (name new-name)
@@ -163,19 +154,24 @@ success, nil otherwise."
(persp-rename new-name (+workspace-get name)))
;;;###autoload
-(defun +workspace-delete (name &optional inhibit-kill-p)
- "Delete the workspace denoted by NAME, which can be the name of a perspective
+(defun +workspace-delete (workspace &optional inhibit-kill-p)
+ "Delete the workspace denoted by WORKSPACE, which can be the name of a perspective
or its hash table. If INHIBIT-KILL-P is non-nil, don't kill this workspace's
buffers."
- (when (+workspace--protected-p name)
- (error "Can't delete '%s' workspace" name))
- (+workspace-get name) ; error checking
- (persp-kill name inhibit-kill-p)
- (not (+workspace-exists-p name)))
+ (unless (stringp workspace)
+ (setq workspace (persp-name workspace)))
+ (when (+workspace--protected-p workspace)
+ (error "Can't delete '%s' workspace" workspace))
+ (+workspace-get workspace) ; error checking
+ (persp-kill workspace inhibit-kill-p)
+ (not (+workspace-exists-p workspace)))
;;;###autoload
(defun +workspace-switch (name &optional auto-create-p)
- "Switch to another workspace."
+ "Switch to another workspace named NAME (a string).
+
+If AUTO-CREATE-P is non-nil, create the workspace if it doesn't exist, otherwise
+throws an error."
(unless (+workspace-exists-p name)
(if auto-create-p
(+workspace-new name)
@@ -184,13 +180,16 @@ buffers."
(setq +workspace--last
(or (and (not (string= old-name persp-nil-name))
old-name)
- +workspaces-main)))
- (persp-frame-switch name))
+ +workspaces-main))
+ (persp-frame-switch name)
+ (equal (+workspace-current-name) name)))
;;
-;; Interactive commands
-;;
+;; Commands
+
+;;;###autoload
+(defalias '+workspace/restore-last-session #'doom/quickload-session)
;;;###autoload
(defun +workspace/load (name)
@@ -203,29 +202,12 @@ current workspace (by name) from session files."
(completing-read
"Workspace to load: "
(persp-list-persp-names-in-file
- (expand-file-name +workspace-data-file persp-save-dir))))))
+ (expand-file-name +workspaces-data-file persp-save-dir))))))
(if (not (+workspace-load name))
(+workspace-error (format "Couldn't load workspace %s" name))
(+workspace/switch-to name)
(+workspace/display)))
-;;;###autoload
-(defun +workspace/load-session (&optional name)
- "Load a session and switch to it. If called with C-u, try to load the last
-session."
- (interactive
- (list
- (unless current-prefix-arg
- (completing-read
- "Session to load: "
- (directory-files persp-save-dir nil "^[^_.]")
- nil t))))
- (condition-case ex
- (let ((name (or name persp-auto-save-fname)))
- (+workspace-load-session name)
- (+workspace-message (format "'%s' workspace loaded" name) 'success))
- '(error (+workspace-error (cadr ex) t))))
-
;;;###autoload
(defun +workspace/save (name)
"Save the current workspace. If called with C-u, autosave the current
@@ -239,34 +221,17 @@ workspace."
(+workspace-message (format "'%s' workspace saved" name) 'success)
(+workspace-error (format "Couldn't save workspace %s" name))))
-;;;###autoload
-(defun +workspace/save-session (&optional name)
- "Save the current session. If called with C-u, prompt you for the name to save
-the session as."
- (interactive
- (list
- (when current-prefix-arg
- (completing-read
- "Save session as: "
- (directory-files persp-save-dir nil "^[^_.]")))))
- (condition-case ex
- (let ((name (or name persp-auto-save-fname)))
- (if (+workspace-save-session name)
- (+workspace-message (format "Saved session as '%s'" name) 'success)
- (error "Couldn't save session as '%s'" name)))
- '(error (+workspace-error (cadr ex) t))))
-
;;;###autoload
(defun +workspace/rename (new-name)
"Rename the current workspace."
(interactive (list (read-from-minibuffer "New workspace name: ")))
- (condition-case ex
+ (condition-case-unless-debug ex
(let* ((current-name (+workspace-current-name))
(old-name (+workspace-rename current-name new-name)))
(unless old-name
(error "Failed to rename %s" current-name))
(+workspace-message (format "Renamed '%s'->'%s'" old-name new-name) 'success))
- ('error (+workspace-error (cadr ex) t))))
+ ('error (+workspace-error ex t))))
;;;###autoload
(defun +workspace/delete (name)
@@ -280,64 +245,59 @@ workspace to delete."
(+workspace-list-names)
nil nil current-name)
current-name))))
- (condition-case ex
- (+workspace-message
- (let ((workspaces (length (+workspace-list-names))))
- (cond ((> workspaces 1)
- (+workspace-delete name)
- (+workspace-switch
- (if (+workspace-exists-p +workspace--last)
- +workspace--last
- (car (+workspace-list-names))))
- (format "Deleted '%s' workspace" name))
- ((= workspaces 1)
- (format "Can't delete the last workspace!"))
- (t
- (+workspace-delete name)
- (+workspace-switch +workspaces-main t)
- (switch-to-buffer (doom-fallback-buffer))
- (format "No workspaces detected! Auto-creating '%s' workspace" +workspaces-main))))
- 'success)
- ('error (+workspace-error (cadr ex) t))))
+ (condition-case-unless-debug ex
+ (let ((workspaces (+workspace-list-names)))
+ (if (not (member name workspaces))
+ (+workspace-message (format "'%s' workspace doesn't exist" name) 'warn)
+ (cond ((delq (selected-frame) (persp-frames-with-persp (get-frame-persp)))
+ (user-error "Can't close workspace, it's visible in another frame"))
+ ((> (length workspaces) 1)
+ (+workspace-delete name)
+ (+workspace-switch
+ (if (+workspace-exists-p +workspace--last)
+ +workspace--last
+ (car (+workspace-list-names))))
+ (unless (doom-buffer-frame-predicate (window-buffer))
+ (switch-to-buffer (doom-fallback-buffer))))
+ (t
+ (+workspace-switch +workspaces-main t)
+ (unless (string= (car workspaces) +workspaces-main)
+ (+workspace-delete name))
+ (doom/kill-all-buffers)))
+ (+workspace-message (format "Deleted '%s' workspace" name) 'success)))
+ ('error (+workspace-error ex t))))
;;;###autoload
(defun +workspace/kill-session ()
- "Delete the current session, clears all workspaces, windows and buffers."
+ "Delete the current session, all workspaces, windows and their buffers."
(interactive)
(unless (cl-every #'+workspace-delete (+workspace-list-names))
(+workspace-error "Could not clear session"))
(+workspace-switch +workspaces-main t)
- (doom/kill-all-buffers)
- (let ((fallback-buf (doom-fallback-buffer)))
- (switch-to-buffer fallback-buf)
- (doom/cleanup-session)))
+ (doom/kill-all-buffers))
;;;###autoload
(defun +workspace/kill-session-and-quit ()
- "Forgets current session and quits."
+ "Kill emacs without saving anything."
(interactive)
- (+workspace/kill-session)
- (save-buffers-kill-terminal))
+ (let ((persp-auto-save-opt 0))
+ (kill-emacs)))
;;;###autoload
(defun +workspace/new (&optional name clone-p)
- "Create a new workspace named NAME. If OVERWRITE-P is non-nil, clear any
-pre-existing workspace."
+ "Create a new workspace named NAME. If CLONE-P is non-nil, clone the current
+workspace, otherwise the new workspace is blank."
(interactive "iP")
(unless name
(setq name (format "#%s" (+workspace--generate-id))))
- (condition-case ex
- (let ((exists-p (+workspace-exists-p name)))
- (if exists-p
- (error "%s already exists" name)
- (+workspace-switch name t)
- (if clone-p
- (dolist (window (window-list))
- (persp-add-buffer (window-buffer window) persp nil))
- (delete-other-windows-internal)
- (switch-to-buffer (doom-fallback-buffer)))
- (+workspace/display)))
- ('error (+workspace-error (cadr ex) t))))
+ (condition-case e
+ (cond ((+workspace-exists-p name)
+ (error "%s already exists" name))
+ (clone-p (persp-copy name t))
+ (t
+ (+workspace-switch name t)
+ (+workspace/display)))
+ ((debug error) (+workspace-error (cadr e) t))))
;;;###autoload
(defun +workspace/switch-to (index)
@@ -349,7 +309,7 @@ end of the workspace list."
(when (and (stringp index)
(string-match-p "^[0-9]+$" index))
(setq index (string-to-number index)))
- (condition-case ex
+ (condition-case-unless-debug ex
(let ((names (+workspace-list-names))
(old-name (+workspace-current-name)))
(cond ((numberp index)
@@ -382,13 +342,13 @@ end of the workspace list."
(let ((current-name (+workspace-current-name)))
(if (equal current-name persp-nil-name)
(+workspace-switch +workspaces-main t)
- (condition-case ex
+ (condition-case-unless-debug ex
(let* ((persps (+workspace-list-names))
(perspc (length persps))
(index (cl-position current-name persps)))
(when (= perspc 1)
(user-error "No other workspaces"))
- (+workspace/switch-to (% (+ index n) perspc))
+ (+workspace/switch-to (% (+ index n perspc) perspc))
(unless (called-interactively-p 'interactive)
(+workspace/display)))
('user-error (+workspace-error (cadr ex) t))
@@ -402,37 +362,29 @@ end of the workspace list."
;;;###autoload
(defun +workspace/close-window-or-workspace ()
- "Close the selected window. If it's the last window in the workspace, close
-the workspace and move to the next."
+ "Close the selected window. If it's the last window in the workspace, either
+close the workspace (as well as its associated frame, if one exists) and move to
+the next."
(interactive)
- (if (doom-popup-p)
- (doom/popup-close)
- (let ((current-persp-name (+workspace-current-name)))
- (cond ((or (+workspace--protected-p current-persp-name)
- (> (length (doom-visible-windows)) 1))
- (if (bound-and-true-p evil-mode)
- (evil-window-delete)
- (delete-window)))
- ((> (length (+workspace-list-names)) 1)
- (+workspace/delete current-persp-name))))))
+ (let ((delete-window-fn (if (featurep 'evil) #'evil-window-delete #'delete-window)))
+ (if (window-dedicated-p)
+ (funcall delete-window-fn)
+ (let ((current-persp-name (+workspace-current-name)))
+ (cond ((or (+workspace--protected-p current-persp-name)
+ (cdr (doom-visible-windows)))
+ (funcall delete-window-fn))
-;;;###autoload
-(defun +workspace/close-workspace-or-frame ()
- "Close the current workspace. If it's the last, delete the frame instead."
- (interactive)
- (let ((frames (length (frame-list)))
- (workspaces (length (+workspace-list-names))))
- (cond ((> workspaces 1)
- (call-interactively #'+workspace/delete))
- ((> frames 1)
- (call-interactively #'delete-frame))
- (t
- (error "Can't delete last frame.")))))
+ ((cdr (+workspace-list-names))
+ (let ((frame-persp (frame-parameter nil 'workspace)))
+ (if (string= frame-persp (+workspace-current-name))
+ (delete-frame)
+ (+workspace/delete current-persp-name))))
+
+ (t (+workspace-error "Can't delete last workspace" t)))))))
;;
;; Tabs display in minibuffer
-;;
(defun +workspace--tabline (&optional names)
(let ((names (or names (+workspace-list-names)))
@@ -448,7 +400,7 @@ the workspace and move to the next."
'+workspace-tab-face)))
" ")))
-(defun +workspace--message-body (message &optional type)
+(defun +workspace--message-body (message &optional type)
(concat (+workspace--tabline)
(propertize " | " 'face 'font-lock-comment-face)
(propertize (format "%s" message)
@@ -466,36 +418,114 @@ the workspace and move to the next."
;;;###autoload
(defun +workspace-error (message &optional noerror)
"Show an 'elegant' error in the echo area next to a listing of workspaces."
- (funcall (if noerror #'message #'error) "%s" (+workspace--message-body message 'error)))
+ (funcall (if noerror #'message #'error)
+ "%s" (+workspace--message-body message 'error)))
;;;###autoload
(defun +workspace/display ()
"Display a list of workspaces (like tabs) in the echo area."
(interactive)
- (message "%s" (+workspace--tabline)))
+ (let (message-log-max)
+ (minibuffer-message "%s" (+workspace--tabline))))
+
+
+;;
+;; Hooks
;;;###autoload
-(defun +workspace-on-new-frame (frame &optional _new-frame-p)
- "Spawn a perspective for each new frame."
- (select-frame frame)
- (+workspace/new)
- (set-frame-parameter frame 'assoc-persp (+workspace-current-name)))
-
-;;;###autoload
-(defun +workspaces|delete-associated-workspace-maybe (frame)
- "Delete workspace associated with current frame IF it has no real buffers."
+(defun +workspaces|delete-associated-workspace (&optional frame)
+ "Delete workspace associated with current frame.
+A workspace gets associated with a frame when a new frame is interactively
+created."
(when persp-mode
- (let ((frame-persp (frame-parameter frame 'assoc-persp)))
- (when (and (equal frame-persp (+workspace-current-name))
- (not (equal frame-persp +workspaces-main)))
+ (unless frame
+ (setq frame (selected-frame)))
+ (let ((frame-persp (frame-parameter frame 'workspace)))
+ (when (string= frame-persp (+workspace-current-name))
(+workspace/delete frame-persp)))))
+;;;###autoload
+(defun +workspaces|cleanup-unassociated-buffers ()
+ "Kill leftover buffers that are unassociated with any perspective."
+ (when persp-mode
+ (cl-loop for buf in (buffer-list)
+ unless (or (persp--buffer-in-persps buf)
+ (get-buffer-window buf))
+ if (kill-buffer buf)
+ sum 1)))
+
+;;;###autoload
+(defun +workspaces|associate-frame (frame &optional _new-frame-p)
+ "Create a blank, new perspective and associate it with FRAME."
+ (when persp-mode
+ (if (not (persp-frame-list-without-daemon))
+ (+workspace-switch +workspaces-main t)
+ (with-selected-frame frame
+ (+workspace-switch (format "#%s" (+workspace--generate-id)) t)
+ (unless (doom-real-buffer-p (current-buffer))
+ (switch-to-buffer (doom-fallback-buffer)))
+ (set-frame-parameter frame 'workspace (+workspace-current-name))
+ ;; ensure every buffer has a buffer-predicate
+ (persp-set-frame-buffer-predicate frame))
+ (run-at-time 0.1 nil #'+workspace/display))))
+
+(defvar +workspaces--project-dir nil)
+;;;###autoload
+(defun +workspaces|set-project-action ()
+ "A `projectile-switch-project-action' that sets the project directory for
+`+workspaces|switch-to-project'."
+ (setq +workspaces--project-dir default-directory))
+
+;;;###autoload
+(defun +workspaces|switch-to-project (&optional dir)
+ "Creates a workspace dedicated to a new project. If one already exists, switch
+to it. If in the main workspace and it's empty, recycle that workspace, without
+renaming it.
+
+Afterwords, runs `+workspaces-switch-project-function'. By default, this prompts
+the user to open a file in the new project.
+
+This be hooked to `projectile-after-switch-project-hook'."
+ (when dir
+ (setq +workspaces--project-dir dir))
+ (when (and persp-mode +workspaces--project-dir)
+ (unwind-protect
+ (if (and (not (null +workspaces-on-switch-project-behavior))
+ (or (eq +workspaces-on-switch-project-behavior t)
+ (+workspace-buffer-list)))
+ (let* (persp-p
+ (persp
+ (let ((project-name (doom-project-name +workspaces--project-dir)))
+ (or (setq persp-p (+workspace-get project-name t))
+ (+workspace-new project-name))))
+ (new-name (persp-name persp)))
+ (+workspace-switch new-name)
+ (unless persp-p
+ (switch-to-buffer (doom-fallback-buffer)))
+ (with-current-buffer (doom-fallback-buffer)
+ (setq default-directory +workspaces--project-dir))
+ (unless current-prefix-arg
+ (funcall +workspaces-switch-project-function +workspaces--project-dir))
+ (+workspace-message
+ (format "Switched to '%s' in new workspace" new-name)
+ 'success))
+ (with-current-buffer (doom-fallback-buffer)
+ (setq default-directory +workspaces--project-dir)
+ (message "Switched to '%s'" (doom-project-name +workspaces--project-dir)))
+ (unless current-prefix-arg
+ (funcall +workspaces-switch-project-function +workspaces--project-dir)))
+ (setq +workspaces--project-dir nil))))
+
+
+;;
+;; Advice
+
;;;###autoload
(defun +workspaces*autosave-real-buffers (orig-fn &rest args)
- "Don't autosave if no real buffers are open."
- (when (doom-real-buffer-list)
- (apply orig-fn args))
- t)
+ "Don't autosave if no real buffers are open."
+ (when (doom-real-buffer-list)
+ (apply orig-fn args))
+ t)
;;;###autoload
(defun +workspaces*switch-project-by-name (orig-fn &rest args)
diff --git a/modules/feature/workspaces/config.el b/modules/feature/workspaces/config.el
index f03fb236f..2a7bc495a 100644
--- a/modules/feature/workspaces/config.el
+++ b/modules/feature/workspaces/config.el
@@ -5,108 +5,192 @@
;; it because it was unstable and slow; `persp-mode' is neither (and still
;; maintained).
;;
-;; By default, sessions are autosaved, but not autoloaded. Use :ss or
-;; `+workspace/save-session' to save, and :sl or `+workspace/load-session' to
-;; load the last autosaved session. You can give sessions a custom name so they
-;; can be loaded later.
-;;
;; NOTE persp-mode requires `workgroups' for file persistence in Emacs 24.4.
(defvar +workspaces-main "main"
- "The name of the primary and initial workspace, which cannot be deleted or
-renamed.")
+ "The name of the primary and initial workspace, which cannot be deleted.")
+
+(defvar +workspaces-switch-project-function #'doom-project-find-file
+ "The function to run after `projectile-switch-project' or
+`counsel-projectile-switch-project'. This function must take one argument: the
+new project directory.")
+
+(defvar +workspaces-on-switch-project-behavior 'non-empty
+ "Controls the behavior of workspaces when switching to a new project.
+
+Can be one of the following:
+
+t Always create a new workspace for the project
+'non-empty Only create a new workspace if the current one already has buffers
+ associated with it.
+nil Never create a new workspace on project switch.")
+
+;; FIXME actually use this for wconf bookmark system
+(defvar +workspaces-data-file "_workspaces"
+ "The basename of the file to store single workspace perspectives. Will be
+stored in `persp-save-dir'.")
;;
-;; Plugins
-;;
+;; Packages
(def-package! persp-mode
+ :commands (persp-switch-to-buffer)
+ :init
+ (defun +workspaces|init ()
+ ;; Remove default buffer predicate so persp-mode can put in its own
+ (setq default-frame-alist
+ (delq (assq 'buffer-predicate default-frame-alist)
+ default-frame-alist))
+ (add-hook 'after-make-frame-functions #'+workspaces|init-frame)
+ (require 'persp-mode)
+ (unless (daemonp)
+ (+workspaces|init-frame (selected-frame))))
+
+ (defun +workspaces|init-frame (frame)
+ "Ensure a main workspace exists and is switched to, if FRAME isn't in any
+workspace. Also ensures that the *Warnings* buffer will be visible in main.
+
+Uses `+workspaces-main' to determine the name of the main workspace."
+ (unless persp-mode
+ (persp-mode +1)
+ (unless noninteractive
+ (let (persp-before-switch-functions)
+ (with-selected-frame frame
+ ;; The default perspective persp-mode creates (`persp-nil-name') is
+ ;; special and doesn't represent a real persp object, so buffers can't
+ ;; really be assigned to it, among other quirks. We create a *real*
+ ;; main workspace to fill this role.
+ (unless (persp-get-by-name +workspaces-main)
+ (persp-add-new +workspaces-main))
+ ;; Switch to it if we aren't auto-loading the last session
+ (when (and (string= (safe-persp-name (get-current-persp)) persp-nil-name)
+ (= persp-auto-resume-time -1))
+ (persp-frame-switch +workspaces-main frame)
+ ;; We want to know where we are in every new daemon frame
+ (when (daemonp)
+ (run-at-time 0.1 nil #'+workspace/display))
+ ;; Fix #319: the warnings buffer gets swallowed by creating
+ ;; `+workspaces-main', so we display it manually, if it exists.
+ (when-let* ((warnings (get-buffer "*Warnings*")))
+ (save-excursion
+ (display-buffer-in-side-window
+ warnings '((window-height . shrink-window-if-larger-than-buffer)))))))))))
+
+ (add-hook 'doom-init-modules-hook #'+workspaces|init t)
:config
(setq persp-autokill-buffer-on-remove 'kill-weak
- persp-nil-name "nil"
persp-nil-hidden t
persp-auto-save-fname "autosave"
persp-save-dir (concat doom-etc-dir "workspaces/")
- persp-set-last-persp-for-new-frames nil
+ persp-set-last-persp-for-new-frames t
persp-switch-to-added-buffer nil
persp-remove-buffers-from-nil-persp-behaviour nil
- ;; Don't restore winconf on new frames
- persp-init-frame-behaviour t
- persp-init-new-frame-behaviour-override 'auto-temp
- ;; Don't auto-load on startup
- persp-auto-resume-time -1
- ;; auto-save on kill
- persp-auto-save-opt (if noninteractive 0 1))
+ persp-auto-resume-time -1 ; Don't auto-load on startup
+ persp-auto-save-opt (if noninteractive 0 1)) ; auto-save on kill
- ;; Bootstrap
- (add-hook 'doom-post-init-hook #'+workspaces|init)
- (add-hook 'after-make-frame-functions #'+workspaces|init)
-
- (define-key persp-mode-map [remap delete-window] #'+workspace/close-window-or-workspace)
-
- ;; per-frame and per-project workspaces
- (setq persp-init-new-frame-behaviour-override nil
- persp-interactive-init-frame-behaviour-override #'+workspace-on-new-frame)
- (add-hook 'delete-frame-functions #'+workspaces|delete-associated-workspace-maybe)
-
- (defun +workspaces|per-project (&optional root)
- "Open a new workspace when switching to another project.
-
-Ensures the scratch (or dashboard) buffers are CDed into the project's root."
- (when persp-mode
- (let ((cwd default-directory))
- (+workspace-switch (projectile-project-name) t)
- (switch-to-buffer (doom-fallback-buffer))
- (setq default-directory cwd)
- (+workspace-message
- (format "Switched to '%s' in new workspace" (+workspace-current-name))
- 'success))))
- (setq projectile-switch-project-action #'+workspaces|per-project)
-
- ;; only auto-save when real buffers are present
(advice-add #'persp-asave-on-exit :around #'+workspaces*autosave-real-buffers)
- (defun +workspaces|on-persp-mode ()
- ;; Remap `buffer-list' to current workspace's buffers in `doom-buffer-list'
- (if persp-mode
- (advice-add #'doom-buffer-list :override #'+workspace-buffer-list)
- (advice-remove #'doom-buffer-list #'+workspace-buffer-list)))
- (add-hook 'persp-mode-hook #'+workspaces|on-persp-mode)
+ ;; Ensure buffers we've opened/switched to are auto-added to the current
+ ;; perspective
+ (setq persp-add-buffer-on-find-file t
+ persp-add-buffer-on-after-change-major-mode t)
+ (add-hook 'persp-add-buffer-on-after-change-major-mode-filter-functions #'doom-unreal-buffer-p)
- ;; Defer delayed warnings even further, so they appear after persp-mode is
- ;; started and the main workspace is ready to display them. Otherwise, warning
- ;; buffers will be hidden on startup.
- (remove-hook 'delayed-warnings-hook #'display-delayed-warnings)
- (defun +workspaces|init (&optional frame)
- (unless persp-mode
- (persp-mode +1)
- ;; Ensure `persp-kill-buffer-query-function' is last in kill-buffer-query-functions
- (remove-hook 'kill-buffer-query-functions 'persp-kill-buffer-query-function)
- (add-hook 'kill-buffer-query-functions 'persp-kill-buffer-query-function t))
- (let ((frame (or frame (selected-frame))))
- (unless noninteractive
- ;; The default perspective persp-mode makes (defined by
- ;; `persp-nil-name') is special and doesn't actually represent a real
- ;; persp object, so buffers can't really be assigned to it, among other
- ;; quirks. We create a *real* main workspace to fill this role.
- (unless (persp-with-name-exists-p +workspaces-main)
- (persp-add-new +workspaces-main))
- ;; Switch to it if we aren't auto-loading the last session
- (when (and (equal (safe-persp-name (get-current-persp)) persp-nil-name)
- (= persp-auto-resume-time -1))
- (persp-frame-switch +workspaces-main frame)))
- (add-hook 'delayed-warnings-hook #'display-delayed-warnings t)))
+ (defun +workspaces|init-persp-mode ()
+ (cond (persp-mode
+ ;; `persp-kill-buffer-query-function' must be last
+ (remove-hook 'kill-buffer-query-functions 'persp-kill-buffer-query-function)
+ (add-hook 'kill-buffer-query-functions 'persp-kill-buffer-query-function t)
+ ;; Restrict buffer list to workspace
+ (advice-add #'doom-buffer-list :override #'+workspace-buffer-list))
+ ((advice-remove #'doom-buffer-list #'+workspace-buffer-list))))
+ (add-hook 'persp-mode-hook #'+workspaces|init-persp-mode)
- (defun +workspaces*auto-add-buffer (buffer &rest _)
- "Auto-associate buffers with perspectives upon opening them.
+ (defun +workspaces|leave-nil-perspective (&rest _)
+ (when (string= (+workspace-current-name) persp-nil-name)
+ (+workspace-switch (or (if (+workspace-p +workspace--last) +workspace--last)
+ (car (+workspace-list-names))
+ +workspaces-main)
+ 'auto-create)))
+ (add-hook 'persp-after-load-state-functions #'+workspaces|leave-nil-perspective)
-Allows a perspective-specific buffer list via `+workspaces-buffer-list'."
- (when (and persp-mode
- (not persp-temporarily-display-buffer)
- (doom-real-buffer-p buffer))
- (persp-add-buffer buffer (get-current-persp) nil)
- (force-mode-line-update t)))
- (advice-add #'switch-to-buffer :after #'+workspaces*auto-add-buffer)
- (advice-add #'display-buffer :after #'+workspaces*auto-add-buffer))
+ ;; Delete the current workspace if closing the last open window
+ (define-key! persp-mode-map
+ [remap delete-window] #'+workspace/close-window-or-workspace
+ [remap evil-delete-window] #'+workspace/close-window-or-workspace)
+
+ ;; per-frame workspaces
+ (setq persp-init-frame-behaviour t
+ persp-init-new-frame-behaviour-override nil
+ persp-interactive-init-frame-behaviour-override #'+workspaces|associate-frame
+ persp-emacsclient-init-frame-behaviour-override #'+workspaces|associate-frame)
+ (add-hook 'delete-frame-functions #'+workspaces|delete-associated-workspace)
+
+ ;; per-project workspaces, but reuse current workspace if empty
+ (setq projectile-switch-project-action #'+workspaces|set-project-action
+ counsel-projectile-switch-project-action
+ '(1 ("o" +workspaces|switch-to-project "open project in new workspace")
+ ("O" counsel-projectile-switch-project-action "jump to a project buffer or file")
+ ("f" counsel-projectile-switch-project-action-find-file "jump to a project file")
+ ("d" counsel-projectile-switch-project-action-find-dir "jump to a project directory")
+ ("b" counsel-projectile-switch-project-action-switch-to-buffer "jump to a project buffer")
+ ("m" counsel-projectile-switch-project-action-find-file-manually "find file manually from project root")
+ ("w" counsel-projectile-switch-project-action-save-all-buffers "save all project buffers")
+ ("k" counsel-projectile-switch-project-action-kill-buffers "kill all project buffers")
+ ("r" counsel-projectile-switch-project-action-remove-known-project "remove project from known projects")
+ ("c" counsel-projectile-switch-project-action-compile "run project compilation command")
+ ("C" counsel-projectile-switch-project-action-configure "run project configure command")
+ ("e" counsel-projectile-switch-project-action-edit-dir-locals "edit project dir-locals")
+ ("v" counsel-projectile-switch-project-action-vc "open project in vc-dir / magit / monky")
+ ("s" (lambda (project) (let ((projectile-switch-project-action (lambda () (call-interactively #'+ivy/project-search))))
+ (counsel-projectile-switch-project-by-name project))) "search project")
+ ("xs" counsel-projectile-switch-project-action-run-shell "invoke shell from project root")
+ ("xe" counsel-projectile-switch-project-action-run-eshell "invoke eshell from project root")
+ ("xt" counsel-projectile-switch-project-action-run-term "invoke term from project root")
+ ("X" counsel-projectile-switch-project-action-org-capture "org-capture into project")))
+
+ (add-hook 'projectile-after-switch-project-hook #'+workspaces|switch-to-project)
+
+ ;; In some scenarios, persp-mode throws error when Emacs tries to die,
+ ;; preventing its death and trapping us in Emacs.
+ (defun +workspaces*ignore-errors-on-kill-emacs (orig-fn)
+ (ignore-errors (funcall orig-fn)))
+ (advice-add #'persp-kill-emacs-h :around #'+workspaces*ignore-errors-on-kill-emacs)
+
+ ;;
+ ;; eshell
+ (persp-def-buffer-save/load
+ :mode 'eshell-mode :tag-symbol 'def-eshell-buffer
+ :save-vars '(major-mode default-directory))
+ ;; compile
+ (persp-def-buffer-save/load
+ :mode 'compilation-mode :tag-symbol 'def-compilation-buffer
+ :save-vars
+ '(major-mode default-directory compilation-directory compilation-environment compilation-arguments))
+ ;; Restore indirect buffers
+ (defvar +workspaces--indirect-buffers-to-restore nil)
+ (persp-def-buffer-save/load
+ :tag-symbol 'def-indirect-buffer
+ :predicate #'buffer-base-buffer
+ :save-function (lambda (buf tag vars)
+ (list tag (buffer-name buf) vars
+ (buffer-name (buffer-base-buffer))))
+ :load-function (lambda (savelist &rest _rest)
+ (cl-destructuring-bind (buf-name _vars base-buf-name &rest _)
+ (cdr savelist)
+ (push (cons buf-name base-buf-name)
+ +workspaces--indirect-buffers-to-restore)
+ nil)))
+ (defun +workspaces|reload-indirect-buffers (&rest _)
+ (dolist (ibc +workspaces--indirect-buffers-to-restore)
+ (let* ((nbn (car ibc))
+ (bbn (cdr ibc))
+ (bb (get-buffer bbn)))
+ (when bb
+ (when (get-buffer nbn)
+ (setq nbn (generate-new-buffer-name nbn)))
+ (make-indirect-buffer bb nbn t))))
+ (setq +workspaces--indirect-buffers-to-restore nil))
+ (add-hook 'persp-after-load-state-functions #'+workspaces|reload-indirect-buffers))
diff --git a/modules/feature/workspaces/test/autoload-workspaces.el b/modules/feature/workspaces/test/autoload-workspaces.el
deleted file mode 100644
index ff69bfb67..000000000
--- a/modules/feature/workspaces/test/autoload-workspaces.el
+++ /dev/null
@@ -1,95 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; feature/workspaces/test/autoload-workspaces.el
-
-(require! :feature workspaces)
-
-(defmacro with-workspace!! (buffer-args &rest body)
- (declare (indent defun))
- (let ((buffers
- (cl-loop for bsym in buffer-args
- collect `(,bsym (get-buffer-create ,(symbol-name bsym))))))
- `(let (noninteractive)
- (+workspaces|init)
- (save-window-excursion
- (let* (,@buffers)
- (cl-loop with persp = (get-current-persp)
- for buf in (list ,@(mapcar #'car buffers))
- do (persp-add-buffer buf persp)
- do (with-current-buffer buf
- (setq buffer-file-name (make-temp-file "workspaces-test-"))))
- ,@body
- (dolist (buf (list ,@(mapcar #'car buffers)))
- (persp-remove-buffer buf)
- (kill-buffer buf))))
- (persp-mode -1)
- (setq *persp-hash* nil
- persp-buffer-props-hash nil))))
-
-;;
-(def-test! init
- (with-workspace!! ()
- (should (equal (+workspace-current-name) +workspaces-main))))
-
-(def-test! auto-add-buffer-to-persp
- (let ((a (generate-new-buffer "a")))
- (doom-set-buffer-real a t)
- (with-workspace!! ()
- (should-not (+workspace-contains-buffer-p a))
- (switch-to-buffer a)
- (should (+workspace-contains-buffer-p a)))))
-
-(def-test! current
- (with-workspace!! ()
- (should (equal (+workspace-current-name) +workspaces-main))
- (should (+workspace-exists-p +workspaces-main))
- (let ((workspace (+workspace-get +workspaces-main))
- (current-workspace (+workspace-current)))
- (should workspace)
- (should (+workspace-p workspace))
- (should (+workspace-p current-workspace))
- (should (equal workspace current-workspace)))))
-
-(def-test! workspace-list
- (with-workspace!! ()
- (should (equal (+workspace-list-names)
- (list (+workspace-current-name))))
- (should (equal (+workspace-list)
- (list (+workspace-current))))))
-
-(def-test! workspace-crud
- "Creating, reading, updating and deleting workspaces."
- (with-workspace!! ()
- (let ((new-workspace-name "*new-test*")
- (renamed-workspace-name "*old-test*"))
- (should (+workspace-new new-workspace-name))
- (should (seq-contains (+workspace-list-names) new-workspace-name))
- (should (equal new-workspace-name
- (+workspace-rename new-workspace-name renamed-workspace-name)))
- (should-not (seq-contains (+workspace-list-names) new-workspace-name))
- (should (seq-contains (+workspace-list-names) renamed-workspace-name))
- (should (= (length (+workspace-list-names)) 2))
- (+workspace-delete renamed-workspace-name)
- (should (= (length (+workspace-list-names)) 1)))))
-
-(def-test! workspace-switch
- (with-workspace!! ()
- (let ((new-workspace-name "*new-test*"))
- (should-error (+workspace-switch new-workspace-name))
- (should (+workspace-switch new-workspace-name t))
- (should (equal (+workspace-current-name) new-workspace-name)))))
-
-(def-test! buffer-list
- (with-workspace!! (a b)
- (let ((c (get-buffer-create "c"))
- (d (get-buffer-create "d")))
- (should (+workspace-contains-buffer-p a))
- (should (+workspace-contains-buffer-p b))
- (should-not (+workspace-contains-buffer-p c))
- ;; New (and real) buffers should be added to workspace buffer list.
- (doom-set-buffer-real c t)
- (switch-to-buffer "c")
- (should (+workspace-contains-buffer-p c))
- ;; unreal buffers shouldn't
- (switch-to-buffer "d")
- (should-not (+workspace-contains-buffer-p d)))))
-
diff --git a/modules/feature/workspaces/test/test-workspaces.el b/modules/feature/workspaces/test/test-workspaces.el
new file mode 100644
index 000000000..4e50901da
--- /dev/null
+++ b/modules/feature/workspaces/test/test-workspaces.el
@@ -0,0 +1,123 @@
+;; -*- no-byte-compile: t; -*-
+;;; feature/workspaces/test/test-workspaces.el
+
+(describe "feature/workspaces"
+ :var (persp-auto-resume-time
+ persp-auto-save-opt
+ persp-switch-to-added-buffer
+ persp-autokill-persp-when-removed-last-buffer
+ persp-autokill-buffer-on-remove
+ in1 in2 out1 out2
+ persp1 persp1-name persp2 persp2-name
+ wconf)
+
+ (before-all
+ (delete-other-windows)
+ (require! :feature workspaces)
+ (require 'persp-mode))
+
+ (before-each
+ (switch-to-buffer "*scratch*")
+ (setq wconf (current-window-configuration)
+ persp-auto-resume-time -1
+ persp-auto-save-opt 0
+ persp-switch-to-added-buffer nil
+ persp-autokill-persp-when-removed-last-buffer nil
+ persp-autokill-buffer-on-remove nil
+ in1 (get-buffer-create "in1")
+ in2 (get-buffer-create "in2")
+ out1 (get-buffer-create "out1")
+ out2 (get-buffer-create "out2"))
+ (doom-set-buffer-real in1 t)
+ (doom-set-buffer-real out1 t)
+ (let (noninteractive)
+ (persp-mode +1)
+ (let (persp-before-switch-functions persp-activated-functions)
+ (setq persp1-name +workspaces-main
+ persp1 (persp-add-new persp1-name)
+ persp2-name "test"
+ persp2 (persp-add-new persp2-name))
+ (persp-switch persp1-name)
+ (persp-add-buffer (list in1 in2) persp1))))
+
+ (after-each
+ (let (kill-buffer-query-functions kill-buffer-hook)
+ (let (noninteractive ignore-window-parameters)
+ (dolist (persp (persp-names))
+ (ignore-errors (persp-kill persp)))
+ (persp-mode -1))
+ (set-window-configuration wconf)
+ (mapc #'kill-buffer (list in1 in2 out1 out2))))
+
+ ;;
+ (describe "switch"
+ (it "throws an error when switching to a non-existent workspace"
+ (expect (+workspace-switch "non-existent") :to-throw))
+ (it "switches to a valid workspace"
+ (+workspace-switch persp2-name)
+ (expect (+workspace-current-name) :to-equal persp2-name)))
+
+ (describe "current"
+ (it "returns the current workspace persp"
+ (expect (+workspace-p (+workspace-current)))
+ (expect (+workspace-current) :to-equal (get-current-persp)))
+ (it "returns the current workspace's name"
+ (expect (+workspace-current-name) :to-equal persp1-name)
+ (persp-switch (persp-name persp2))
+ (expect (+workspace-current-name) :to-equal persp2-name)))
+
+ (describe "exists-p"
+ (it "returns t for valid workspaces"
+ (expect (+workspace-exists-p persp1-name)))
+ (it "returns t for non-current (but valid) workspaces"
+ (expect (+workspace-exists-p persp2-name)))
+ (it "returns nil for non-existent workspaces"
+ (expect (+workspace-exists-p "non-existent") :to-be nil)))
+
+ (describe "buffer membership"
+ (it "returns t for buffers in current workspace"
+ (expect (+workspace-contains-buffer-p in1)))
+ (it "returns nil for buffers outside of current workspace"
+ (expect (+workspace-contains-buffer-p out1) :to-be nil))
+ (xit "returns a list of orphaned buffers"
+ (expect (+workspace-orphaned-buffer-list) :to-contain out2)))
+
+ (describe "list"
+ (it "returns a list of names"
+ (expect (+workspace-list-names)
+ :to-have-same-items-as (list persp1-name persp2-name)))
+ (it "returns a list of perspective structs"
+ (expect (+workspace-list)
+ :to-have-same-items-as (list persp1 persp2))))
+
+ (describe "CRUD"
+ (it "creates new workspaces"
+ (+workspace-new "X")
+ (expect (+workspace-list-names) :to-contain "X"))
+ (it "renames an existing workspace"
+ (+workspace-rename persp2-name "X")
+ (expect (persp-name persp2) :to-equal "X")
+ (expect (+workspace-list-names)
+ :to-have-same-items-as (list persp1-name "X")))
+ (it "deletes a live workspace"
+ (+workspace-delete persp2-name)
+ (expect (+workspace-list-names) :not :to-contain persp2-name)))
+
+ (describe "command"
+ (describe "close-window-or-workspace"
+ (before-each
+ (+workspace-switch persp2-name)
+ (split-window)
+ (expect (length (doom-visible-windows)) :to-be 2))
+ (it "kills window if more than one window"
+ (quiet! (+workspace/close-window-or-workspace))
+ (expect (length (doom-visible-windows)) :to-be 1))
+ (it "kills workspace on last window"
+ (quiet! (+workspace/close-window-or-workspace)
+ (+workspace/close-window-or-workspace))
+ (expect (+workspace-current-name) :to-equal persp1-name)))
+
+ (describe "rename"
+ (it "renames the current workspace"
+ (quiet! (+workspace/rename "X"))
+ (expect (+workspace-current-name) :to-equal "X")))))
diff --git a/modules/lang/agda/README.org b/modules/lang/agda/README.org
new file mode 100644
index 000000000..1812f748d
--- /dev/null
+++ b/modules/lang/agda/README.org
@@ -0,0 +1,9 @@
+#+TITLE: :lang agda
+
+This module adds support for the [[http://wiki.portal.chalmers.se/agda/pmwiki.php][agda]] programming language.
+
+Emacs support is included in the agda release (you can find installation
+instructions [[https://agda.readthedocs.io/en/latest/getting-started/installation.html][here]]). This module attempts to find the location of ~agda2.el~ via
+the ~agda-mode locate~ command that comes with the agda release. Users can set
+this manually by setting the ~+agda2-dir~ variable.
+
diff --git a/modules/lang/agda/config.el b/modules/lang/agda/config.el
new file mode 100644
index 000000000..0ad69349d
--- /dev/null
+++ b/modules/lang/agda/config.el
@@ -0,0 +1,39 @@
+;;; lang/agda/config.el -*- lexical-binding: t; -*-
+
+(defvar +agda-dir
+ (when (executable-find "agda-mode")
+ (file-name-directory (shell-command-to-string "agda-mode locate"))))
+
+(def-package! agda2
+ :when +agda-dir
+ :load-path +agda-dir)
+
+(def-package! agda2-mode
+ :defer t
+ :config
+ (map! :map agda2-mode-map
+ :localleader
+ "?" #'agda2-show-goals
+ "." #'agda2-goal-and-context-and-inferred
+ "," #'agda2-goal-and-context
+ "=" #'agda2-show-constraints
+ "SPC" #'agda2-give
+ "a" #'agda2-auto
+ "c" #'agda2-make-case
+ "d" #'agda2-infer-type-maybe-toplevel
+ "e" #'agda2-show-context
+ "gG" #'agda2-go-back
+ "h" #'agda2-helper-function-type
+ "l" #'agda2-load
+ "n" #'agda2-compute-normalised-maybe-toplevel
+ "p" #'agda2-module-contents-maybe-toplevel
+ "r" #'agda2-refine
+ "s" #'agda2-solveAll
+ "t" #'agda2-goal-type
+ "w" #'agda2-why-in-scope-maybe-toplevel
+ (:prefix "x"
+ "c" #'agda2-compile
+ "d" #'agda2-remove-annotations
+ "h" #'agda2-display-implicit-arguments
+ "q" #'agda2-quit
+ "r" #'agda2-restart)))
diff --git a/modules/lang/agda/doctor.el b/modules/lang/agda/doctor.el
new file mode 100644
index 000000000..e38ccbc03
--- /dev/null
+++ b/modules/lang/agda/doctor.el
@@ -0,0 +1,5 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/agda/doctor.el
+
+(unless (executable-find "agda-mode")
+ (warn! "Couldn't find agda-mode. Agda support won't work"))
diff --git a/modules/lang/assembly/autoload.el b/modules/lang/assembly/autoload.el
new file mode 100644
index 000000000..29e8ded24
--- /dev/null
+++ b/modules/lang/assembly/autoload.el
@@ -0,0 +1,4 @@
+;;; lang/assembly/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(add-to-list 'auto-mode-alist '("\\.hax\\'" . haxor-mode))
diff --git a/modules/lang/assembly/config.el b/modules/lang/assembly/config.el
deleted file mode 100644
index 98b879b24..000000000
--- a/modules/lang/assembly/config.el
+++ /dev/null
@@ -1,8 +0,0 @@
-;;; lang/assembly/config.el -*- lexical-binding: t; -*-
-
-(def-package! mips-mode :mode "\\.mips$")
-
-(def-package! haxor-mode :mode "\\.hax$")
-
-(def-package! nasm-mode :commands nasm-mode)
-
diff --git a/modules/lang/cc/README.org b/modules/lang/cc/README.org
index ecb9d6f88..d08a988a7 100644
--- a/modules/lang/cc/README.org
+++ b/modules/lang/cc/README.org
@@ -1,5 +1,22 @@
-#+TITLE: :lang cc
+#+TITLE: lang/cc
+#+DATE: January 16, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
+* Table of Contents :TOC_3:noexport:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Prerequisites][Prerequisites]]
+ - [[irony-server][irony-server]]
+ - [[MacOS][MacOS]]
+ - [[Arch Linux][Arch Linux]]
+ - [[rtags][rtags]]
+- [[Configure][Configure]]
+ - [[Project compile settings][Project compile settings]]
+ - [[Known issues with bear on macOS][Known issues with bear on macOS]]
+
+* Description
This module adds support for the C-family of languages: C, C++, and Objective-C.
+ Code completion (~company-irony~)
@@ -10,34 +27,44 @@ This module adds support for the C-family of languages: C, C++, and Objective-C.
+ Snippets ([[https://github.com/hlissner/emacs-snippets/tree/master/cc-mode][cc-mode]], [[https://github.com/hlissner/emacs-snippets/tree/master/c-mode][c-mode]], [[https://github.com/hlissner/emacs-snippets/tree/master/c++-mode][c++-mode]])
+ Several improvements to C++11 indentation and syntax highlighting.
-#+begin_quote
-C contends with Haskell and Ruby for my favorite language. That said, it's more
-accurate to say I write C, but a C++ feature or three.
+** Module Flags
++ ~+irony~ Enable Irony as a backend for code completion, syntax checking, and
+ eldoc support. This must be disabled to use LSP or another backend.
++ ~+rtags~ Enable rtags integration. A daemon will be spawned the first time you
+ open a C/C++/ObjC buffer, if one hasn't already.
-The module provides nominal support for Objective-C, which I really only use to
-inspect generated glue code for iOS mobile apps. Otherwise, I prefer Swift.
-#+end_quote
+** Plugins
++ [[https://github.com/Kitware/CMake][cmake-mode]]
++ [[https://github.com/chachi/cuda-mode][cuda-mode]]
++ [[https://github.com/liblit/demangle-mode][demangle-mode]]
++ [[https://github.com/jart/disaster][disaster]]
++ [[https://github.com/ludwigpacifici/modern-cpp-font-lock][modern-cpp-font-lock]]
++ [[https://github.com/salmanebah/opencl-mode][opencl-mode]]
++ [[https://github.com/jimhourihan/glsl-mode][glsl-mode]]*
++ [[https://github.com/guidoschmidt/company-glsl][gompany-glsl]]*
++ [[https://github.com/Sarcasm/irony-mode][irony]]*
++ [[https://github.com/ikirill/irony-eldoc][irony-eldoc]]*
++ [[https://github.com/Sarcasm/flycheck-irony][flycheck-irony]]*
++ [[https://github.com/Sarcasm/company-irony][company-irony]]*
++ [[https://github.com/hotpxl/company-irony-c-headers][company-irony-c-headers]]*
++ [[https://github.com/Andersbakken/rtags][rtags]]*
++ [[https://github.com/Andersbakken/rtags][ivy-rtags]] or [[https://github.com/Andersbakken/rtags][helm-rtags]]*
-* Table of Contents :TOC:
-- [[#install][Install]]
- - [[#irony-server][irony-server]]
- - [[#rtags][rtags]]
-- [[#configure][Configure]]
- - [[#compile-settings][Compile settings]]
+* Prerequisites
+This module requires
-* Install
-This module requires:
-
-+ irony-server
-+ rtags
++ irony-server (if ~+irony~ is enabled)
++ rtags (if ~+rtags~ is enabled)
** irony-server
Irony powers the code completion, eldoc and syntax checking systems.
+After installing its dependencies, run ~M-x irony-install-server~ in Emacs.
+
*** MacOS
Due to linking issues, MacOS users must compile irony-server manually:
-#+BEGIN_SRC sh :tangle (if (doom-system-os 'macos) "yes")
+#+BEGIN_SRC sh
brew install cmake
brew install llvm # 1gb+ installation! May take a while!
@@ -60,18 +87,16 @@ rm -rf irony-mode
#+END_SRC
*** Arch Linux
-#+BEGIN_SRC sh :tangle (if (doom-system-os 'arch) "yes")
-sudo pacman --needed --noconfirm -S clang cmake
+#+BEGIN_SRC sh
+pacman -S clang cmake
#+END_SRC
-Then run ~M-x irony-install-server~ in Emacs.
-
** rtags
-Code navigation requires an [[https://github.com/Andersbakken/rtags][rtags]] server (~rdm~) installed and running. This
-should be available through your OS's package manager.
+Code navigation requires an [[https://github.com/Andersbakken/rtags][rtags]] server (~rdm~) installed. This should be
+available through your OS's package manager.
This module will auto-start ~rdm~ when you open C/C++ buffers (so long as one
-isn't already). If you prefer to run it yourself, outside of Emacs:
+isn't already running). If you prefer to run it yourself:
#+BEGIN_SRC sh
rdm &
@@ -79,7 +104,7 @@ rc -J $PROJECT_ROOT # loads PROJECT_ROOT's compile_commands.json
#+END_SRC
* Configure
-** Compile settings
+** Project compile settings
By default, a set of default compile settings are defined in
~+cc-default-compiler-options~ for C, C++ and Objective C. Irony, rtags and
flycheck will fall back to these.
@@ -87,7 +112,7 @@ flycheck will fall back to these.
To make these tools aware of project specific build settings, you need a JSON
[[https://sarcasm.github.io/notes/dev/compilation-database.html#ninja][compilation database]] present (i.e. a ~compile_commands.json~ file).
-There are [[https://sarcasm.github.io/notes/dev/compilation-database.html][many ways to generate one]]. I use [[http://www.cmake.org/][CMake]] and [[https://github.com/rizsotto/Bear][bear]]:
+There are [[https://sarcasm.github.io/notes/dev/compilation-database.html][many ways to generate one]]. I use [[http://www.cmake.org/][CMake]] or [[https://github.com/rizsotto/Bear][bear]]:
#+BEGIN_SRC sh
# For CMake projects
@@ -98,7 +123,44 @@ make clean
bear make
#+END_SRC
-#+begin_quote
Use ~M-x +cc/reload-compile-db~ to reload your compile db in an already-open
C/C++/ObjC buffer.
+
+*** Known issues with bear on macOS
+MacOS' [[https://support.apple.com/en-us/HT204899][System Integrity Protection (SIP)]] might interfere with bear if ~make~ is
+under ~/usr/bin/~ which results in an empty compilation database.
+
+From the bear [[https://github.com/rizsotto/Bear#empty-compilation-database-on-os-x-captain-or-fedora][readme]]:
+
+#+begin_quote
+Security extension/modes on different operating systems might disable library
+preloads. This case Bear behaves normally, but the result compilation database
+will be empty. (Please make sure it's not the case when reporting bugs.) Notable
+examples for enabled security modes are: OS X 10.11 (check with csrutil status |
+grep 'System Integrity Protection'), and Fedora, CentOS, RHEL (check with
+sestatus | grep 'SELinux status').
+
+Workaround could be to disable the security feature while running Bear. (This
+might involve reboot of your computer, so might be heavy workaround.) Another
+option if the build tool is not installed under certain directories. Or use
+tools which are using compiler wrappers. (It injects a fake compiler which does
+record the compiler invocation and calls the real compiler too.) An example for
+such tool might be scan-build. The build system shall respect CC and CXX
+environment variables.
#+end_quote
+
+A workaround might be to install ~make~ via Homebrew which puts ~gmake~
+under ~/usr/local/~.
+
+#+BEGIN_SRC sh
+brew install make
+#+END_SRC
+
+#+BEGIN_SRC sh
+make clean
+bear gmake
+#+END_SRC
+
+Additional info:
++ [[https://github.com/rizsotto/Bear/issues/158][Empty compilation database with compiler in /usr/local]]
++ [[https://github.com/rizsotto/Bear/issues/152][Workaround for 'Empty compilation database on OS X Captain]]
diff --git a/modules/lang/cc/autoload.el b/modules/lang/cc/autoload.el
index d40bcd104..e50ea0665 100644
--- a/modules/lang/cc/autoload.el
+++ b/modules/lang/cc/autoload.el
@@ -1,52 +1,20 @@
;;; lang/cc/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
-(defun +cc/reload-compile-db (&optional force-p)
- "Reload the current project's JSON compilation database."
- (interactive "P")
- (unless (memq major-mode '(c-mode c++-mode objc-mode))
- (user-error "Not a C/C++/ObjC buffer"))
- (unless (doom-project-has! "compile_commands.json")
- (user-error "No compile_commands.json file"))
- ;; first rtag
- (when (and (featurep 'rtags)
- rtags-enabled
- (executable-find "rc"))
- (with-temp-buffer
- (message "Reloaded compile commands for rtags daemon")
- (rtags-call-rc :silent t "-J" (doom-project-root))))
- ;; then irony
- (when (and (featurep 'irony) irony-mode)
- (+cc|irony-init-compile-options)))
+(add-to-list 'auto-mode-alist '("\\.cl\\'" . opencl-mode))
-;;;###autoload
-(defun +cc*align-lambda-arglist (orig-fun &rest args)
- "Improve indentation of continued C++11 lambda function opened as argument."
- (if (and (eq major-mode 'c++-mode)
- (ignore-errors
- (save-excursion
- (goto-char (c-langelem-pos langelem))
- ;; Detect "[...](" or "[...]{". preceded by "," or "(",
- ;; and with unclosed brace.
- (looking-at-p ".*[(,][ \t]*\\[[^]]*\\][ \t]*[({][^}]*$"))))
- 0 ; no additional indent
- (apply orig-fun args)))
-;;;###autoload
-(defun +cc/autoclose->-maybe ()
- "For some reason smartparens won't autoskip >'s, this hack does."
- (interactive)
- (if (save-excursion
- (backward-char)
- (looking-at-p "[^ \t]>"))
- (forward-char)
- (call-interactively #'self-insert-command)))
+;;
+;; Library
;;;###autoload
(defun +cc-sp-point-is-template-p (id action context)
"Return t if point is in the right place for C++ angle-brackets."
(and (sp-in-code-p id action context)
- (sp-point-after-word-p id action context)))
+ (cond ((eq action 'insert)
+ (sp-point-after-word-p id action context))
+ ((eq action 'autoskip)
+ (/= (char-before) 32)))))
;;;###autoload
(defun +cc-sp-point-after-include-p (id action context)
@@ -57,33 +25,131 @@
(looking-at-p "[ ]*#include[^<]+"))))
;;;###autoload
-(defun +cc-c-lineup-inclass (_langelem)
- "Indent privacy keywords at same level as class properties."
- (if (memq major-mode '(c-mode c++-mode))
- (let ((inclass (assq 'inclass c-syntactic-context)))
- (save-excursion
- (goto-char (c-langelem-pos inclass))
- (if (or (looking-at "struct")
- (looking-at "typedef struct"))
- '+
- '++)))
- '+))
+(defun +cc-c++-lineup-inclass (langelem)
+ "Indent inclass lines one level further than access modifier keywords."
+ (and (eq major-mode 'c++-mode)
+ (or (assoc 'access-label c-syntactic-context)
+ (save-excursion
+ (save-match-data
+ (re-search-backward
+ "\\(?:p\\(?:ublic\\|r\\(?:otected\\|ivate\\)\\)\\)"
+ (c-langelem-pos langelem) t))))
+ '++))
+
+;;;###autoload
+(defun +cc-lineup-arglist-close (langlem)
+ "Line up the closing brace in an arglist with the opening brace IF cursor is
+preceded by the opening brace or a comma (disregarding whitespace in between)."
+ (when (save-excursion
+ (save-match-data
+ (skip-chars-backward " \t\n" (c-langelem-pos langelem))
+ (memq (char-before) (list ?, ?\( ?\;))))
+ (c-lineup-arglist langlem)))
+
+(defun +cc--re-search-for (regexp)
+ (save-excursion
+ (save-restriction
+ (save-match-data
+ (widen)
+ (goto-char (point-min))
+ (re-search-forward regexp magic-mode-regexp-match-limit t)))))
+
+;;;###autoload
+(defun +cc-c-c++-objc-mode ()
+ "Uses heuristics to detect `c-mode', `objc-mode' or `c++-mode'.
+
+1. Checks if there are nearby cpp/cc/m/mm files with the same name.
+2. Checks for ObjC and C++-specific keywords and libraries.
+3. Falls back to `+cc-default-header-file-mode', if set.
+4. Otherwise, activates `c-mode'.
+
+This is meant to replace `c-or-c++-mode' (introduced in Emacs 26.1), which
+doesn't support specification of the fallback mode and whose heuristics are
+simpler."
+ (let ((base (file-name-sans-extension (buffer-file-name (buffer-base-buffer)))))
+ (cond ((file-exists-p! (or (concat base ".cpp")
+ (concat base ".cc")))
+ (c++-mode))
+ ((or (file-exists-p! (or (concat base ".m")
+ (concat base ".mm")))
+ (+cc--re-search-for
+ (concat "^[ \t\r]*\\(?:"
+ "@\\(?:class\\|interface\\|property\\|end\\)\\_>"
+ "\\|#import +"
+ "\\|[-+] ([a-zA-Z0-9_]+)"
+ "\\)")))
+ (objc-mode))
+ ((+cc--re-search-for
+ (let ((id "[a-zA-Z0-9_]+") (ws "[ \t\r]+") (ws-maybe "[ \t\r]*"))
+ (concat "^" ws-maybe "\\(?:"
+ "using" ws "\\(?:namespace" ws "std;\\|std::\\)"
+ "\\|" "namespace" "\\(?:" ws id "\\)?" ws-maybe "{"
+ "\\|" "class" ws id ws-maybe "[:{\n]"
+ "\\|" "template" ws-maybe "<.*>"
+ "\\|" "#include" ws-maybe "<\\(?:string\\|iostream\\|map\\)>"
+ "\\)")))
+ (c++-mode))
+ ((functionp +cc-default-header-file-mode)
+ (funcall +cc-default-header-file-mode))
+ ((c-mode)))))
+
+(defun +cc-resolve-include-paths ()
+ (cl-loop with path = (or buffer-file-name default-directory)
+ for dir in +cc-default-include-paths
+ if (file-name-absolute-p dir)
+ collect dir
+ else if (projectile-locate-dominating-file path dir)
+ collect (expand-file-name dir it)))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun +cc/reload-compile-db ()
+ "Reload the current project's JSON compilation database."
+ (interactive)
+ (unless (memq major-mode '(c-mode c++-mode objc-mode))
+ (user-error "Not a C/C++/ObjC buffer"))
+ ;; first rtag
+ (when (and (featurep 'rtags)
+ rtags-enabled
+ (executable-find rtags-rc-binary-name))
+ (with-temp-buffer
+ (message "Reloaded compile commands for rtags daemon")
+ (rtags-call-rc :silent t "-J" (or (doom-project-root) default-directory))))
+ ;; then irony
+ (when (and (featurep 'irony) irony-mode)
+ (+cc|irony-init-compile-options)))
+
+;;;###autoload
+(defun +cc/imenu ()
+ "Invoke `rtags-imenu' if a running rdm process is available, otherwise invoke
+`imenu'."
+ (interactive)
+ (call-interactively
+ (if (and (processp rtags-rdm-process)
+ (not (eq (process-status rtags-rdm-process) 'exit))
+ (not (eq (process-status rtags-rdm-process) 'signal)))
+ #'rtags-imenu
+ #'imenu)))
;;
;; Hooks
-;;
;;;###autoload
(defun +cc|fontify-constants ()
"Better fontification for preprocessor constants"
- (font-lock-add-keywords
- nil '(("\\<[A-Z]*_[A-Z_]+\\>" . font-lock-constant-face)
- ("\\<[A-Z]\\{3,\\}\\>" . font-lock-constant-face))
- t))
+ (when (memq major-mode '(c-mode c++-mode))
+ (font-lock-add-keywords
+ nil '(("\\<[A-Z]*_[0-9A-Z_]+\\>" . font-lock-constant-face)
+ ("\\<[A-Z]\\{3,\\}\\>" . font-lock-constant-face))
+ t)))
+(defvar +cc--project-includes-alist nil)
;;;###autoload
-(defun +cc|irony-init-compile-options ()
+(defun +cc|init-irony-compile-options ()
"Initialize compiler options for irony-mode. It searches for the nearest
compilation database and initailizes it, otherwise falling back on
`+cc-default-compiler-options' and `+cc-default-include-paths'.
@@ -93,9 +159,47 @@ compilation dbs."
(when (memq major-mode '(c-mode c++-mode objc-mode))
(require 'irony-cdb)
(unless (irony-cdb-autosetup-compile-options)
- (irony-cdb--update-compile-options
- (append (delq nil (cdr-safe (assq major-mode +cc-default-compiler-options)))
- (cl-loop for path in +cc-default-include-paths
- nconc (list "-I" path)))
- (doom-project-root)))))
+ (let ((project-root (doom-project-root))
+ (include-paths (+cc-resolve-include-paths)))
+ (setf (alist-get project-root +cc--project-includes-alist)
+ include-paths)
+ (irony-cdb--update-compile-options
+ (append (delq nil (cdr-safe (assq major-mode +cc-default-compiler-options)))
+ (cl-loop for path in include-paths
+ collect (format "-I%s" path)))
+ project-root)))))
+;; ;;;###autoload
+;; (defun +cc|init-ccls-compile-options ()
+;; "TODO"
+;; (when (memq major-mode '(c-mode c++-mode objc-mode))
+;; (when-let* ((include-paths (+cc-resolve-include-paths)))
+;; (let ((args (delq nil (cdr-safe (assq major-mode +cc-default-compiler-options)))))
+;; (setf (alist-get (or (lsp-workspace-root)
+;; (lsp--suggest-project-root)
+;; (doom-project-root))
+;; +cc--project-includes-alist)
+;; include-paths)
+;; (setq ccls-initialization-options
+;; `(:clang (:extraArgs
+;; [,@(cl-loop for path in include-paths
+;; collect (format "-I%s" path))])))))))
+
+;;;###autoload
+(defun +cc|init-ffap-integration ()
+ "Takes the local project include paths and registers them with ffap.
+This way, `find-file-at-point' (and `+lookup/file') will know where to find most
+header files."
+ (when-let* ((project-root (or (bound-and-true-p irony--working-directory)
+ (and (featurep 'lsp)
+ (or (lsp-workspace-root)
+ (doom-project-root))))))
+ (require 'ffap)
+ (make-local-variable 'ffap-c-path)
+ (make-local-variable 'ffap-c++-path)
+ (cl-loop for dir in (or (cdr (assoc project-root +cc--project-includes-alist))
+ (+cc-resolve-include-paths))
+ do (add-to-list (pcase major-mode
+ (`c-mode 'ffap-c-path)
+ (`c++-mode 'ffap-c++-path))
+ (expand-file-name dir project-root)))))
diff --git a/modules/lang/cc/config.el b/modules/lang/cc/config.el
index f5b61e172..e6f83ae1c 100644
--- a/modules/lang/cc/config.el
+++ b/modules/lang/cc/config.el
@@ -1,104 +1,131 @@
;;; lang/cc/config.el --- c, c++, and obj-c -*- lexical-binding: t; -*-
-(defvar +cc-default-include-paths (list "include/")
- "A list of default paths, relative to a project root, to search for headers in
-C/C++. Paths can be absolute. This is ignored if your project has a JSON
-compilation database.")
+(defvar +cc-default-include-paths
+ (list "include"
+ "includes")
+ "A list of default relative paths which will be searched for up from the
+current file, to be passed to irony as extra header search paths. Paths can be
+absolute. This is ignored if your project has a compilation database.
+
+This is ignored by ccls.")
+
+(defvar +cc-default-header-file-mode 'c-mode
+ "Fallback major mode for .h files if all other heuristics fail (in
+`+cc-c-c++-objc-mode').")
(defvar +cc-default-compiler-options
`((c-mode . nil)
(c++-mode
- . ,(list "-std=c++11" ; use C++11 by default
+ . ,(list "-std=c++1z" ; use C++17 draft by default
(when IS-MAC
;; NOTE beware: you'll get abi-inconsistencies when passing
;; std-objects to libraries linked with libstdc++ (e.g. if you
;; use boost which wasn't compiled with libc++)
- (list "-stdlib=libc++"))))
+ "-stdlib=libc++")))
(objc-mode . nil))
"A list of default compiler options for the C family. These are ignored if a
-compilation database is present in the project.")
+compilation database is present in the project.
+
+This is ignored by ccls.")
;;
-;; Plugins
-;;
+;; Packages
(def-package! cc-mode
:commands (c-mode c++-mode objc-mode java-mode)
- :mode ("\\.mm" . objc-mode)
- :preface
- (defun +cc-c++-header-file-p ()
- (and buffer-file-name
- (equal (file-name-extension buffer-file-name) "h")
- (or (file-exists-p (expand-file-name
- (concat (file-name-sans-extension buffer-file-name)
- ".cpp")))
- (when-let* ((file (car-safe (projectile-get-other-files
- buffer-file-name
- (projectile-current-project-files)))))
- (equal (file-name-extension file) "cpp")))))
-
- (defun +cc-objc-header-file-p ()
- (and buffer-file-name
- (equal (file-name-extension buffer-file-name) "h")
- (re-search-forward "@\\" magic-mode-regexp-match-limit t)))
-
- (push (cons #'+cc-c++-header-file-p 'c++-mode) magic-mode-alist)
- (push (cons #'+cc-objc-header-file-p 'objc-mode) magic-mode-alist)
-
+ :mode ("\\.mm\\'" . objc-mode)
:init
- (setq-default c-basic-offset tab-width)
+ (setq-default c-basic-offset tab-width
+ c-backspace-function #'delete-backward-char
+ c-default-style "doom")
+
+ ;; The plusses in c++-mode can be annoying to search for ivy/helm (which reads
+ ;; queries as regexps), so we add these for convenience.
+ (defalias 'cpp-mode 'c++-mode)
+ (defvaralias 'cpp-mode-map 'c++-mode-map)
+
+ ;; Activate `c-mode', `c++-mode' or `objc-mode' depending on heuristics
+ (add-to-list 'auto-mode-alist '("\\.h\\'" . +cc-c-c++-objc-mode))
+
+ ;; Ensure find-file-at-point works in C modes, must be added before irony
+ ;; and/or lsp hooks are run.
+ (add-hook! (c-mode-local-vars c++-mode-local-vars objc-mode-local-vars)
+ #'+cc|init-ffap-integration)
:config
- (set! :electric '(c-mode c++-mode objc-mode java-mode)
- :chars '(?\n ?\}))
- (set! :company-backend
- '(c-mode c++-mode objc-mode)
- '(company-irony-c-headers company-irony))
+ (set-electric! '(c-mode c++-mode objc-mode java-mode) :chars '(?\n ?\} ?\{))
+ (set-docsets! 'c-mode "C")
+ (set-docsets! 'c++-mode "C++" "Boost")
- ;;; Style/formatting
- ;; C/C++ style settings
- (c-toggle-electric-state -1)
- (c-toggle-auto-newline -1)
- (c-set-offset 'substatement-open '0) ; don't indent brackets
- (c-set-offset 'inline-open '+)
- (c-set-offset 'block-open '+)
- (c-set-offset 'brace-list-open '+)
- (c-set-offset 'case-label '+)
- (c-set-offset 'access-label '-)
- (c-set-offset 'arglist-intro '+)
- (c-set-offset 'arglist-close '0)
- ;; Indent privacy keywords at same level as class properties
- ;; (c-set-offset 'inclass #'+cc-c-lineup-inclass)
+ (set-rotate-patterns! 'c++-mode
+ :symbols '(("public" "protected" "private")
+ ("class" "struct")))
+
+ (set-pretty-symbols! '(c-mode c++-mode)
+ ;; Functional
+ ;; :def "void "
+ ;; Types
+ :null "nullptr"
+ :true "true" :false "false"
+ :int "int" :float "float"
+ :str "std::string"
+ :bool "bool"
+ ;; Flow
+ :not "!"
+ :and "&&" :or "||"
+ :for "for"
+ :return "return"
+ :yield "#require")
;;; Better fontification (also see `modern-cpp-font-lock')
(add-hook 'c-mode-common-hook #'rainbow-delimiters-mode)
- (add-hook! (c-mode c++-mode) #'highlight-numbers-mode)
(add-hook! (c-mode c++-mode) #'+cc|fontify-constants)
- ;; Improve indentation of inline lambdas in C++11
- (advice-add #'c-lineup-arglist :around #'+cc*align-lambda-arglist)
+ ;; Custom style, based off of linux
+ (c-add-style
+ "doom" '((c-basic-offset . tab-width)
+ (c-comment-only-line-offset . 0)
+ (c-hanging-braces-alist (brace-list-open)
+ (brace-entry-open)
+ (substatement-open after)
+ (block-close . c-snug-do-while)
+ (arglist-cont-nonempty))
+ (c-cleanup-list brace-else-brace)
+ (c-offsets-alist
+ (knr-argdecl-intro . 0)
+ (substatement-open . 0)
+ (substatement-label . 0)
+ (statement-cont . +)
+ (case-label . +)
+ ;; align args with open brace OR don't indent at all (if open
+ ;; brace is at eolp and close brace is after arg with no trailing
+ ;; comma)
+ (brace-list-intro . 0)
+ (brace-list-close . -)
+ (arglist-intro . +)
+ (arglist-close +cc-lineup-arglist-close 0)
+ ;; don't over-indent lambda blocks
+ (inline-open . 0)
+ (inlambda . 0)
+ ;; indent access keywords +1 level, and properties beneath them
+ ;; another level
+ (access-label . -)
+ (inclass +cc-c++-lineup-inclass +)
+ (label . 0))))
;;; Keybindings
- ;; Completely disable electric keys because it interferes with smartparens and
- ;; custom bindings. We'll do this ourselves.
- (setq c-tab-always-indent nil
- c-electric-flag nil)
- (dolist (key '("#" "{" "}" "/" "*" ";" "," ":" "(" ")"))
- (define-key c-mode-base-map key nil))
;; Smartparens and cc-mode both try to autoclose angle-brackets intelligently.
;; The result isn't very intelligent (causes redundant characters), so just do
;; it ourselves.
- (map! :map c++-mode-map
- "<" nil
- :i ">" #'+cc/autoclose->-maybe)
-
+ (define-key! c++-mode-map "<" nil ">" nil)
;; ...and leave it to smartparens
+ (sp-with-modes '(c++-mode objc-mode)
+ (sp-local-pair "<" ">"
+ :when '(+cc-sp-point-is-template-p +cc-sp-point-after-include-p)
+ :post-handlers '(("| " "SPC"))))
+
(sp-with-modes '(c-mode c++-mode objc-mode java-mode)
- (sp-local-pair "<" ">" :when '(+cc-sp-point-is-template-p +cc-sp-point-after-include-p))
- (sp-local-pair "/*" "*/" :post-handlers '(("||\n[i]" "RET") ("| " "SPC")))
- ;; Doxygen blocks
- (sp-local-pair "/**" "*/" :post-handlers '(("||\n[i]" "RET") ("||\n[i]" "SPC")))
(sp-local-pair "/*!" "*/" :post-handlers '(("||\n[i]" "RET") ("[d-1]< | " "SPC")))))
@@ -107,135 +134,117 @@ compilation database is present in the project.")
(def-package! irony
- :after cc-mode
- :commands irony-install-server
- :preface (setq irony-server-install-prefix (concat doom-etc-dir "irony-server/"))
+ :unless (featurep! +lsp)
+ :commands (irony-install-server irony-mode)
+ :preface
+ (setq irony-server-install-prefix (concat doom-etc-dir "irony-server/"))
:init
(defun +cc|init-irony-mode ()
- (when (memq major-mode '(c-mode c++-mode objc-mode))
- (irony-mode +1)))
- (add-hook! (c-mode c++-mode objc-mode) #'+cc|init-irony-mode)
+ (if (file-directory-p irony-server-install-prefix)
+ (irony-mode +1)
+ (message "Irony server isn't installed")))
+ (add-hook! (c-mode-local-vars c++-mode-local-vars objc-mode-local-vars)
+ #'+cc|init-irony-mode)
:config
- (unless (file-directory-p irony-server-install-prefix)
- (warn "irony-mode: server isn't installed; run M-x irony-install-server"))
+ (setq irony-cdb-search-directory-list '("." "build" "build-conda"))
+
;; Initialize compilation database, if present. Otherwise, fall back on
;; `+cc-default-compiler-options'.
- (add-hook 'irony-mode-hook #'+cc|irony-init-compile-options))
+ (add-hook 'irony-mode-hook #'+cc|init-irony-compile-options)
-(def-package! irony-eldoc
- :after irony
- :hook (irony-mode . irony-eldoc))
+ (def-package! irony-eldoc
+ :hook (irony-mode . irony-eldoc))
-(def-package! flycheck-irony
- :when (featurep! :feature syntax-checker)
- :after irony
- :config
- (add-hook 'irony-mode-hook #'flycheck-mode)
- (flycheck-irony-setup))
+ (def-package! flycheck-irony
+ :when (featurep! :tools flycheck)
+ :config (flycheck-irony-setup))
-
-;;
-;; Tools
-;;
-
-(def-package! disaster :commands disaster)
+ (def-package! company-irony
+ :when (featurep! :completion company)
+ :init
+ (set-company-backend! 'irony-mode
+ '(:separate company-irony-c-headers company-irony))
+ :config
+ (require 'company-irony-c-headers)))
;;
;; Major modes
-;;
-(def-package! cmake-mode
- :mode "/CMakeLists\\.txt$"
- :mode "\\.cmake\\$"
- :config
- (set! :company-backend 'cmake-mode '(company-cmake company-yasnippet)))
+(def-package! company-cmake ; for `cmake-mode'
+ :when (featurep! :completion company)
+ :after cmake-mode
+ :config (set-company-backend! 'cmake-mode 'company-cmake))
-(def-package! cuda-mode :mode "\\.cuh?$")
-
-(def-package! opencl-mode :mode "\\.cl$")
(def-package! demangle-mode
:hook llvm-mode)
-(def-package! glsl-mode
- :mode "\\.glsl$"
- :mode "\\.vert$"
- :mode "\\.frag$"
- :mode "\\.geom$")
-
-;;
-;; Company plugins
-;;
-
-(def-package! company-cmake
- :when (featurep! :completion company)
- :after cmake-mode)
-
-(def-package! company-irony
- :when (featurep! :completion company)
- :after irony)
-
-(def-package! company-irony-c-headers
- :when (featurep! :completion company)
- :after company-irony)
-
-(def-package! company-glsl
+(def-package! company-glsl ; for `glsl-mode'
:when (featurep! :completion company)
:after glsl-mode
- :config
- (if (executable-find "glslangValidator")
- (warn "glsl-mode: couldn't find glslangValidator, disabling company-glsl")
- (set! :company-backend 'glsl-mode '(company-glsl))))
+ :config (set-company-backend! 'glsl-mode 'company-glsl))
;;
;; Rtags Support
-;;
(def-package! rtags
- :after cc-mode
+ :unless (featurep! +lsp)
+ :commands rtags-executable-find
+ :preface
+ (setq rtags-install-path (concat doom-etc-dir "rtags/"))
+ :init
+ (defun +cc|init-rtags ()
+ "Start an rtags server in c-mode and c++-mode buffers."
+ (when (and (require 'rtags nil t)
+ (rtags-executable-find rtags-rdm-binary-name))
+ (rtags-start-process-unless-running)))
+ (add-hook! (c-mode-local-vars c++-mode-local-vars objc-mode-local-vars)
+ #'+cc|init-rtags)
:config
(setq rtags-autostart-diagnostics t
rtags-use-bookmarks nil
rtags-completions-enabled nil
+ rtags-display-result-backend
+ (cond ((featurep! :completion ivy) 'ivy)
+ ((featurep! :completion helm) 'helm)
+ ('default))
;; If not using ivy or helm to view results, use a pop-up window rather
;; than displaying it in the current window...
rtags-results-buffer-other-window t
;; ...and don't auto-jump to first match before making a selection.
rtags-jump-to-first-match nil)
- (let ((bins (cl-remove-if-not #'executable-find '("rdm" "rc"))))
- (if (/= (length bins) 2)
- (warn "cc-mode: couldn't find %s, disabling rtags support" bins)
- (add-hook! (c-mode c++-mode) #'rtags-start-process-unless-running)
- (set! :jump '(c-mode c++-mode)
- :definition #'rtags-find-symbol-at-point
- :references #'rtags-find-references-at-point)))
+ (set-lookup-handlers! '(c-mode c++-mode)
+ :definition #'rtags-find-symbol-at-point
+ :references #'rtags-find-references-at-point)
- (add-hook 'doom-cleanup-hook #'rtags-cancel-process)
- (add-hook! kill-emacs (ignore-errors (rtags-cancel-process)))
+ (add-hook! 'kill-emacs-hook (ignore-errors (rtags-cancel-process)))
;; Use rtags-imenu instead of imenu/counsel-imenu
- (map! :map (c-mode-map c++-mode-map) [remap imenu] #'rtags-imenu)
+ (define-key! (c-mode-map c++-mode-map) [remap imenu] #'+cc/imenu)
- (add-hook 'rtags-jump-hook #'evil-set-jump)
+ (when (featurep 'evil)
+ (add-hook 'rtags-jump-hook #'evil-set-jump))
(add-hook 'rtags-after-find-file-hook #'recenter))
-(def-package! ivy-rtags
- :when (featurep! :completion ivy)
- :after rtags
- :init
- ;; NOTE Ivy integration breaks when rtags is byte-compiled with Emacs 26 or
- ;; later, so we un-byte-compile it before we load it.
- (eval-when-compile
- (when (>= emacs-major-version 26)
- (when-let* ((elc-file (locate-library "rtags.elc" t doom--package-load-path)))
- (delete-file elc-file))))
- :config (setq rtags-display-result-backend 'ivy))
-(def-package! helm-rtags
- :when (featurep! :completion helm)
- :after rtags
- :config (setq rtags-display-result-backend 'helm))
+;;
+;; LSP
+
+(def-package! ccls
+ :when (featurep! +lsp)
+ :hook ((c-mode-local-vars c++-mode-local-vars objc-mode-local-vars) . +cc|init-ccls)
+ :init
+ (after! projectile
+ (add-to-list 'projectile-globally-ignored-directories ".ccls-cache")
+ (add-to-list 'projectile-project-root-files-bottom-up ".ccls-root")
+ (add-to-list 'projectile-project-root-files-top-down-recurring "compile_commands.json"))
+ :config
+ (defun +cc|init-ccls ()
+ (setq-local company-transformers nil)
+ (setq-local company-lsp-async t)
+ (setq-local company-lsp-cache-candidates nil)
+ (lsp)))
diff --git a/modules/lang/cc/doctor.el b/modules/lang/cc/doctor.el
new file mode 100644
index 000000000..bb1d22c3e
--- /dev/null
+++ b/modules/lang/cc/doctor.el
@@ -0,0 +1,18 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/cc/doctor.el
+
+(when (require 'rtags nil t)
+ ;; rtags
+ (let ((bins (cl-remove-if #'executable-find `(,rtags-rdm-binary-name ,rtags-rc-binary-name))))
+ (when (/= (length bins) 0)
+ (warn! "Couldn't find the rtag client and/or server programs %s. Disabling rtags support" bins))))
+
+;; irony server
+(when (require 'irony nil t)
+ (unless (file-directory-p irony-server-install-prefix)
+ (warn! "Irony server isn't installed. Run M-x irony-install-server")))
+
+(when (featurep! :completion company)
+ ;; glslangValidator
+ (unless (executable-find "glslangValidator")
+ (warn! "Couldn't find glslangValidator. GLSL code completion is disabled")))
diff --git a/modules/lang/cc/packages.el b/modules/lang/cc/packages.el
index 8151bc4fd..5a73da2f9 100644
--- a/modules/lang/cc/packages.el
+++ b/modules/lang/cc/packages.el
@@ -5,22 +5,24 @@
(package! cuda-mode)
(package! demangle-mode)
(package! disaster)
-(package! glsl-mode)
-(package! irony)
-(package! irony-eldoc)
(package! modern-cpp-font-lock)
(package! opencl-mode)
-(when (featurep! :feature syntax-checker)
- (package! flycheck-irony))
+(when (package! glsl-mode)
+ (when (featurep! :completion company)
+ (package! company-glsl :recipe (:fetcher github :repo "Kaali/company-glsl"))))
-(when (featurep! :completion company)
- (package! company-glsl :recipe (:fetcher github :repo "Kaali/company-glsl"))
- (package! company-irony)
- (package! company-irony-c-headers))
-
-(package! rtags)
-(when (featurep! :completion ivy)
- (package! ivy-rtags))
-(when (featurep! :completion helm)
- (package! helm-rtags))
+(if (featurep! +lsp)
+ (package! ccls)
+ (when (package! irony)
+ (package! irony-eldoc)
+ (when (featurep! :tools flycheck)
+ (package! flycheck-irony))
+ (when (featurep! :completion company)
+ (package! company-irony)
+ (package! company-irony-c-headers)))
+ (when (package! rtags)
+ (when (featurep! :completion ivy)
+ (package! ivy-rtags))
+ (when (featurep! :completion helm)
+ (package! helm-rtags))))
diff --git a/modules/lang/clojure/autoload.el b/modules/lang/clojure/autoload.el
new file mode 100644
index 000000000..d555e26ff
--- /dev/null
+++ b/modules/lang/clojure/autoload.el
@@ -0,0 +1,14 @@
+;;; lang/clojure/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +clojure/repl (&optional arg)
+ "Open a Cider REPL and return the buffer."
+ (interactive "P")
+ (cider-jack-in arg)
+ (current-buffer))
+
+;;;###autoload
+(defun +clojure/cider-switch-to-repl-buffer-and-switch-ns ()
+ "TODO"
+ (interactive)
+ (cider-switch-to-repl-buffer t))
diff --git a/modules/lang/clojure/config.el b/modules/lang/clojure/config.el
index 5dd2ec8c5..afca38fba 100644
--- a/modules/lang/clojure/config.el
+++ b/modules/lang/clojure/config.el
@@ -1,44 +1,118 @@
;;; lang/clojure/config.el -*- lexical-binding: t; -*-
-(def-package! clojure-mode
- :mode "\\.clj$"
- :mode ("\\.cljs$" . clojurescript-mode)
- :config
- (map! :map clojure-mode-map
- (:localleader
- :n "'" #'cider-jack-in
- :n "\"" #'cider-jack-in-clojurescript
- :n "B" #'cider-switch-to-repl-buffer
- :n "b" #'cider-eval-buffer
- :n "n" #'cider-repl-set-ns
- :n "j" #'cider-find-var
- :n "d" #'cider-doc
- :n "c" #'cider-repl-clear-buffer
- :n "p" #'cider-eval-sexp-at-point
- :n "r" #'cider-eval-region)))
-
-
-(def-package! clj-refactor
- :after clojure-mode
- :config
- ;; setup some extra namespace auto completion for great awesome
- (dolist (mapping '(("re-frame" . "re-frame.core")
- ("reagent" . "reagent.core")
- ("str" . "clojure.str")))
- (add-to-list 'cljr-magic-require-namespaces mapping t)))
+;; `clojure-mode'
+(add-hook 'clojure-mode-hook #'rainbow-delimiters-mode)
(def-package! cider
- ;; NOTE: if you don't have an org directory set (the dir doesn't exist), cider jack in won't work.
- :commands (cider-jack-in cider-mode cider-jack-in-clojurescript)
+ ;; NOTE: if you don't have an org directory set (the dir doesn't exist),
+ ;; cider jack in won't work.
+ :commands (cider-jack-in cider-jack-in-clojurescript)
+ :hook (clojure-mode-local-vars . cider-mode)
+ :init
+ (set-repl-handler! 'clojure-mode #'+clojure/repl)
+ (set-eval-handler! 'clojure-mode #'cider-eval-region)
+ (set-lookup-handlers! 'clojure-mode
+ :definition #'cider-find-dwim
+ :documentation #'cider-doc)
+ (add-hook 'cider-mode-hook #'eldoc-mode)
:config
- (setq nrepl-hide-special-buffers t)
+ (set-popup-rules!
+ '(("^\\*cider-error*" :ignore t)
+ ("^\\*cider-repl" :quit nil)
+ ("^\\*cider-repl-history" :vslot 2 :ttl nil)))
- ;; settings for cider repl as a popup (prevent it from being closed on escape, especially.)
- (set! :popup "^\\*cider" :regexp t :noselect t :noesc t)
+ (setq nrepl-hide-special-buffers t
+ nrepl-log-messages nil
+ cider-font-lock-dynamically '(macro core function var)
+ cider-overlays-use-font-lock t
+ cider-prompt-for-symbol nil
+ cider-repl-display-help-banner nil
+ cider-repl-history-display-duplicates nil
+ cider-repl-history-display-style 'one-line
+ cider-repl-history-file (concat doom-cache-dir "cider-repl-history")
+ cider-repl-history-highlight-current-entry t
+ cider-repl-history-quit-action 'delete-and-restore
+ cider-repl-history-highlight-inserted-item t
+ cider-repl-history-size 1000
+ cider-repl-pop-to-buffer-on-connect 'display-only
+ cider-repl-result-prefix ";; => "
+ cider-repl-print-length 100
+ cider-repl-use-clojure-font-lock t
+ cider-repl-use-pretty-printing t
+ cider-repl-wrap-history nil
+ cider-stacktrace-default-filters '(tooling dup))
- ;; Setup cider for clojurescript / figwheel dev.
- (setq cider-cljs-lein-repl
- "(do (require 'figwheel-sidecar.repl-api)
- (figwheel-sidecar.repl-api/start-figwheel!)
- (figwheel-sidecar.repl-api/cljs-repl))"))
+ (map! (:localleader
+ (:map clojure-mode-map
+ "'" #'cider-jack-in
+ "\"" #'cider-jack-in-clojurescript
+
+ (:prefix ("e" . "eval")
+ "d" #'cider-eval-defun-at-point
+ "D" #'cider-insert-defun-in-repl
+ "e" #'cider-eval-last-sexp
+ "E" #'cider-insert-last-sexp-in-repl
+ "r" #'cider-eval-region
+ "R" #'cider-insert-region-in-repl
+ "u" #'cider-undef)
+ (:prefix ("g" . "go/jump")
+ "b" #'cider-pop-back
+ "g" #'cider-find-var
+ "n" #'cider-find-ns)
+ (:prefix ("h" . "help")
+ "n" #'cider-find-ns
+ "a" #'cider-apropos
+ "d" #'cider-doc
+ "g" #'cider-grimoire-web
+ "j" #'cider-javadoc)
+ (:prefix ("i" . "inspect")
+ "i" #'cider-inspect
+ "r" #'cider-inspect-last-result)
+ (:prefix ("m" . "macro")
+ "e" #'cider-macroexpand-1
+ "E" #'cider-macroexpand-al)
+ (:prefix ("n" . "namespace")
+ "n" #'cider-browse-ns
+ "N" #'cider-browse-ns-all)
+ (:prefix ("r" . "repl")
+ "n" #'cider-repl-set-ns
+ "q" #'cider-quit
+ "r" #'cider-refresh
+ "R" #'cider-restart
+ "b" #'cider-switch-to-repl-buffer
+ "B" #'+clojure/cider-switch-to-repl-buffer-and-switch-ns
+ "c" #'cider-find-and-clear-repl-output)))
+
+ (:when (featurep! :feature evil +everywhere)
+ :map cider-repl-mode-map
+ :i [S-return] #'cider-repl-newline-and-indent
+ (:localleader
+ ("n" #'cider-repl-set-ns
+ "q" #'cider-quit
+ "r" #'cider-ns-refresh
+ "R" #'cider-restart
+ "c" #'cider-repl-clear-buffer))
+ :map cider-repl-history-mode-map
+ :i [return] #'cider-repl-history-insert-and-quit
+ :i "q" #'cider-repl-history-quit
+ :i "l" #'cider-repl-history-occur
+ :i "s" #'cider-repl-history-search-forward
+ :i "r" #'cider-repl-history-search-backward
+ :i "U" #'cider-repl-history-undo-other-window)))
+
+
+(def-package! clj-refactor
+ :hook (clojure-mode . clj-refactor-mode)
+ :init
+ (set-lookup-handlers! 'clojure-mode
+ :references #'cljr-find-usages)
+ :config
+ (map! :map clojure-mode-map
+ :localleader
+ :desc "refactor" "R" #'hydra-cljr-help-menu/body))
+
+
+(def-package! flycheck-joker
+ :when (featurep! :tools flycheck)
+ :after flycheck)
diff --git a/modules/lang/clojure/packages.el b/modules/lang/clojure/packages.el
index a61dc39ed..e289c1e81 100644
--- a/modules/lang/clojure/packages.el
+++ b/modules/lang/clojure/packages.el
@@ -4,3 +4,5 @@
(package! cider)
(package! clj-refactor)
+(when (featurep! :tools flycheck)
+ (package! flycheck-joker))
diff --git a/modules/lang/common-lisp/autoload/common-lisp.el b/modules/lang/common-lisp/autoload/common-lisp.el
new file mode 100644
index 000000000..7fd720f11
--- /dev/null
+++ b/modules/lang/common-lisp/autoload/common-lisp.el
@@ -0,0 +1,11 @@
+;;; lang/common-lisp/autoload/common-lisp.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +common-lisp*sly-last-sexp (command &rest args)
+ "In normal-state or motion-state, last sexp ends at point."
+ (if (and (not evil-move-beyond-eol)
+ (or (evil-normal-state-p) (evil-motion-state-p)))
+ (save-excursion
+ (unless (or (eobp) (eolp)) (forward-char))
+ (apply command args))
+ (apply command args)))
diff --git a/modules/lang/common-lisp/config.el b/modules/lang/common-lisp/config.el
new file mode 100644
index 000000000..816561db6
--- /dev/null
+++ b/modules/lang/common-lisp/config.el
@@ -0,0 +1,245 @@
+;;; lang/common-lisp/config.el -*- lexical-binding: t; -*-
+
+;; `lisp-mode' is loaded at startup. In order to lazy load its config we need to
+;; pretend it isn't loaded
+(defer-feature! lisp-mode)
+
+
+;;
+;; packages
+
+(defvar inferior-lisp-program "sbcl")
+
+(after! lisp-mode
+ (set-repl-handler! 'lisp-mode #'sly-mrepl)
+ (set-eval-handler! 'lisp-mode #'sly-eval-region)
+ (set-lookup-handlers! 'lisp-mode
+ :definition #'sly-edit-definition
+ :documentation #'sly-describe-symbol)
+
+ (add-hook 'lisp-mode-hook #'rainbow-delimiters-mode))
+
+
+(after! sly
+ (setq sly-mrepl-history-file-name (concat doom-cache-dir "sly-mrepl-history")
+ sly-kill-without-query-p t
+ sly-net-coding-system 'utf-8-unix
+ ;; Change this to `sly-flex-completions' for fuzzy completion
+ sly-complete-symbol-function 'sly-simple-completions)
+
+ (set-popup-rules!
+ '(("^\\*sly-mrepl" :vslot 2 :quit nil :ttl nil)
+ ("^\\*sly-compilation" :vslot 3 :ttl nil)
+ ("^\\*sly-traces" :vslot 4 :ttl nil)
+ ;; Do not display debugger or inspector buffers in a popup window. These
+ ;; buffers are meant to be displayed with sufficient vertical space.
+ ("^\\*sly-\\(?:db\\|inspector\\)" :ignore t)))
+
+ (sp-with-modes '(sly-mrepl-mode)
+ (sp-local-pair "'" "'" :actions nil)
+ (sp-local-pair "`" "`" :actions nil))
+
+ (defun +common-lisp|cleanup-sly-maybe ()
+ "Kill processes and leftover buffers when killing the last sly buffer."
+ (unless (cl-loop for buf in (delq (current-buffer) (buffer-list))
+ if (and (buffer-local-value 'sly-mode buf)
+ (get-buffer-window buf))
+ return t)
+ (dolist (conn (sly--purge-connections))
+ (sly-quit-lisp-internal conn 'sly-quit-sentinel t))
+ (let (kill-buffer-hook kill-buffer-query-functions)
+ (mapc #'kill-buffer
+ (cl-loop for buf in (delq (current-buffer) (buffer-list))
+ if (buffer-local-value 'sly-mode buf)
+ collect buf)))))
+
+ (defun +common-lisp|init-sly ()
+ "Attempt to auto-start sly when opening a lisp buffer."
+ (cond ((or (doom-temp-buffer-p (current-buffer))
+ (sly-connected-p)))
+ ((executable-find inferior-lisp-program)
+ (let ((sly-auto-start 'always))
+ (sly-auto-start)
+ (add-hook 'kill-buffer-hook #'+common-lisp|cleanup-sly-maybe nil t)))
+ ((message "WARNING: Couldn't find `inferior-lisp-program' (%s)"
+ inferior-lisp-program))))
+ (add-hook 'sly-mode-hook #'+common-lisp|init-sly)
+
+ (defun +common-lisp*refresh-sly-version (version conn)
+ "Update `sly-protocol-version', which will likely be incorrect or nil due to
+an issue where `load-file-name' is incorrect. Because Doom's packages are
+installed through an external script (bin/doom), `load-file-name' is set to
+bin/doom while packages at compile-time (not a runtime though)."
+ (unless sly-protocol-version
+ (setq sly-protocol-version (sly-version nil (locate-library "sly.el"))))
+ (advice-remove #'sly-check-version #'+common-lisp*refresh-sly-version))
+ (advice-add #'sly-check-version :before #'+common-lisp*refresh-sly-version)
+
+ (map! :localleader
+ :map lisp-mode-map
+ :desc "Sly" "'" #'sly
+ :desc "Sly (ask)" ";" (λ! () (let ((current-prefix-arg '-)) (sly nil nil t)))
+ (:prefix ("g" . "Go")
+ :desc "Go back" "b" #'sly-pop-find-definition-stack
+ :desc "Go to" "d" #'sly-edit-definition
+ :desc "Go to (other window)" "D" #'sly-edit-definition-other-window
+ :desc "Next note" "n" #'sly-next-note
+ :desc "Previous note" "N" #'sly-previous-note
+ :desc "Next sticker" "s" #'sly-stickers-next-sticker
+ :desc "Previous sticker" "S" #'sly-stickers-prev-sticker)
+ (:prefix ("h" . "Help")
+ :desc "Who calls" "<" #'sly-who-calls
+ :desc "Calls who" ">" #'sly-calls-who
+ :desc "Lookup format directive" "~" #'hyperspec-lookup-format
+ :desc "Lookup reader macro" "#" #'hyperspec-lookup-reader-macro
+ :desc "Apropos" "a" #'sly-apropos
+ :desc "Who binds" "b" #'sly-who-binds
+ :desc "Disassemble symbol" "d" #'sly-disassemble-symbol
+ :desc "Describe symbol" "h" #'sly-describe-symbol
+ :desc "HyperSpec lookup" "H" #'sly-hyperspec-lookup
+ :desc "Who macro-expands" "m" #'sly-who-macroexpands
+ :desc "Apropos package" "p" #'sly-apropos-package
+ :desc "Who references" "r" #'sly-who-references
+ :desc "Who specializes" "s" #'sly-who-specializes
+ :desc "Who sets" "S" #'sly-who-sets)
+ (:prefix ("c" . "Compile")
+ :desc "Compile file" "c" #'sly-compile-file
+ :desc "Compile/load file" "C" #'sly-compile-and-load-file
+ :desc "Compile toplevel form" "f" #'sly-compile-defun
+ :desc "Load file" "l" #'sly-load-file
+ :desc "Remove notes" "n" #'sly-remove-notes
+ :desc "Compile region" "r" #'sly-compile-region)
+ (:prefix ("e" . "Evaluate")
+ :desc "Evaulate buffer" "b" #'sly-eval-buffer
+ :desc "Evaluate last" "e" #'sly-eval-last-expression
+ :desc "Evaluate/print last" "E" #'sly-eval-print-last-expression
+ :desc "Evaluate defun" "f" #'sly-eval-defun
+ :desc "Undefine function" "F" #'sly-undefine-function
+ :desc "Evaluate region" "r" #'sly-eval-region)
+ (:prefix ("m" . "Macro")
+ :desc "Macrostep" "e" #'macrostep-expand)
+ (:prefix ("r" . "REPL")
+ :desc "Clear REPL" "c" #'sly-mrepl-clear-repl
+ :desc "Quit connection" "q" #'sly-quit-lisp
+ :desc "Restart connection" "r" #'sly-restart-inferior-lisp
+ :desc "Sync REPL" "s" #'sly-mrepl-sync)
+ (:prefix ("s" . "Stickers")
+ :desc "Toggle breaking stickers" "b" #'sly-stickers-toggle-break-on-stickers
+ :desc "Clear defun stickers" "c" #'sly-stickers-clear-defun-stickers
+ :desc "Clear buffer stickers" "C" #'sly-stickers-clear-buffer-stickers
+ :desc "Fetch stickers" "f" #'sly-stickers-fetch
+ :desc "Replay stickers" "r" #'sly-stickers-replay
+ :desc "Add/remove sticker" "s" #'sly-stickers-dwim)
+ (:prefix ("t" . "Trace")
+ :desc "Toggle" "t" #'sly-toggle-trace-fdefinition
+ :desc "Toggle (fancy)" "T" #'sly-toggle-fancy-trace
+ :desc "Untrace all" "u" #'sly-untrace-all))
+
+ (when (featurep! :feature evil +everywhere)
+ (add-hook 'sly-mode-hook #'evil-normalize-keymaps)
+ (add-hook 'sly-popup-buffer-mode-hook #'evil-normalize-keymaps)
+ (unless evil-move-beyond-eol
+ (advice-add #'sly-eval-last-expression :around #'+common-lisp*sly-last-sexp)
+ (advice-add #'sly-pprint-eval-last-expression :around #'+common-lisp*sly-last-sexp)
+ (advice-add #'sly-eval-print-last-expression :around #'+common-lisp*sly-last-sexp)
+ (advice-add #'sly-eval-last-expression-in-repl :around #'+common-lisp*sly-last-sexp))
+ (set-evil-initial-state!
+ '(sly-db-mode sly-inspector-mode sly-popup-buffer-mode sly-xref-mode)
+ 'normal)
+ (evil-define-key 'insert sly-mrepl-mode-map
+ [S-return] #'newline-and-indent
+ [backspace] #'sp-backward-delete-char
+ [up] (λ! () (evil-goto-line) (comint-previous-input 1))
+ [down] (λ! () (evil-goto-line) (comint-next-input 1)))
+ (evil-define-key 'normal sly-parent-map
+ (kbd "C-t") #'sly-pop-find-definition-stack)
+ (evil-define-key 'normal sly-popup-buffer-mode-map
+ (kbd "C-t") 'sly-pop-find-definition-stack
+ "q" 'quit-window)
+ (evil-define-key 'normal sly-db-mode-map
+ [follow-link] 'mouse-face
+ [mouse-2] 'sly-db-default-action/mouse
+ [remap quit-window] 'sly-db-quit
+ (kbd "C-i") 'sly-db-cycle
+ (kbd "C-j") 'sly-db-down
+ (kbd "C-k") 'sly-db-up
+ (kbd "C-m") 'sly-db-default-action
+ (kbd "C-S-j") 'sly-db-details-down
+ (kbd "C-S-k") 'sly-db-details-up
+ "]" 'sly-db-details-down
+ "[" 'sly-db-details-up
+ "0" 'sly-db-invoke-restart-0
+ "1" 'sly-db-invoke-restart-1
+ "2" 'sly-db-invoke-restart-2
+ "3" 'sly-db-invoke-restart-3
+ "4" 'sly-db-invoke-restart-4
+ "5" 'sly-db-invoke-restart-5
+ "6" 'sly-db-invoke-restart-6
+ "7" 'sly-db-invoke-restart-7
+ "8" 'sly-db-invoke-restart-8
+ "9" 'sly-db-invoke-restart-9
+ "a" 'sly-db-abort
+ "A" 'sly-db-break-with-system-debugger
+ "b" 'sly-db-break-on-return
+ "B" 'sly-db-break-with-default-debugger
+ "c" 'sly-db-continue
+ "C" 'sly-db-inspect-condition
+ "d" 'sly-db-pprint-eval-in-frame
+ "D" 'sly-db-disassemble
+ "e" 'sly-db-eval-in-frame
+ "g:" 'sly-interactive-eval
+ "g?" 'describe-mode
+ "gg" 'sly-db-beginning-of-backtrace
+ "gj" 'sly-db-down
+ "gk" 'sly-db-up
+ "gr" 'sly-db-restart-frame
+ "G" 'sly-db-end-of-backtrace
+ "i" 'sly-db-inspect-in-frame
+ "I" 'sly-db-invoke-restart-by-name
+ "n" 'sly-db-next
+ "o" 'sly-db-out
+ "P" 'sly-db-print-condition
+ "q" 'sly-db-quit
+ "R" 'sly-db-return-from-frame
+ "s" 'sly-db-step
+ "S" 'sly-db-show-frame-source
+ "t" 'sly-db-toggle-details)
+ (evil-define-key 'normal sly-inspector-mode-map
+ [backtab] 'backward-button
+ [return] 'push-button
+ [(shift tab)] 'backward-button
+ (kbd "") 'sly-mrepl-copy-part-to-repl
+ (kbd "C-i") 'next-button
+ (kbd "C-m") 'push-button
+ "e" 'sly-inspector-eval
+ "gb" 'sly-inspector-pop
+ "gj" 'sly-inspector-next
+ "gr" 'sly-inspector-reinspect
+ "gR" 'sly-inspector-fetch-all
+ "gv" 'sly-inspector-toggle-verbose
+ "h" 'sly-inspector-history
+ "k" 'backward-button
+ "K" 'sly-inspector-describe-inspectee
+ "p" 'sly-button-pretty-print
+ "q" 'sly-inspector-quit)
+ (evil-define-key 'normal sly-mode-map
+ (kbd "C-t") 'sly-pop-find-definition-stack)
+ (evil-define-key 'normal sly-xref-mode-map
+ [return] 'sly-goto-xref
+ (kbd "S-") 'sly-show-xref
+ (kbd "C-j") 'sly-xref-next-line
+ (kbd "C-k") 'sly-xref-prev-line
+ "]" 'sly-xref-next-line
+ "[" 'sly-xref-prev-line
+ "gj" 'sly-xref-next-line
+ "gk" 'sly-xref-prev-line
+ "go" 'sly-show-xref
+ "gr" 'sly-recompile-xref
+ "gR" 'sly-recompile-all-xrefs
+ "q" 'quit-window
+ "r" 'sly-xref-retract)))
+
+(def-package! sly-repl-ansi-color
+ :defer t
+ :init
+ (add-to-list 'sly-contribs 'sly-repl-ansi-color nil #'eq))
diff --git a/modules/lang/common-lisp/doctor.el b/modules/lang/common-lisp/doctor.el
new file mode 100644
index 000000000..891f5ae8e
--- /dev/null
+++ b/modules/lang/common-lisp/doctor.el
@@ -0,0 +1,6 @@
+;;; lang/common-lisp/doctor.el -*- lexical-binding: t; -*-
+
+(when (require 'sly nil t)
+ (unless (executable-find inferior-lisp-program)
+ (warn! "Couldn't find your `inferior-lisp-program' (%s). Is it installed?"
+ inferior-lisp-program)))
diff --git a/modules/lang/common-lisp/packages.el b/modules/lang/common-lisp/packages.el
new file mode 100644
index 000000000..cc0549ea0
--- /dev/null
+++ b/modules/lang/common-lisp/packages.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/common-lisp/packages.el
+
+(package! sly)
+(package! sly-macrostep)
+(package! sly-repl-ansi-color)
diff --git a/modules/lang/coq/README.org b/modules/lang/coq/README.org
new file mode 100644
index 000000000..57459aa7e
--- /dev/null
+++ b/modules/lang/coq/README.org
@@ -0,0 +1,6 @@
+#+TITLE: :lang coq
+
+This module adds [[https://coq.inria.fr][coq]] support, powered by [[https://proofgeneral.github.io][Proof General]].
+
++ Code completion ([[https://github.com/cpitclaudel/company-coq][company-coq]])
++ [[https://github.com/hlissner/emacs-snippets/tree/master/coq-mode][Snippets]]
diff --git a/modules/lang/coq/autoload.el b/modules/lang/coq/autoload.el
new file mode 100644
index 000000000..624185018
--- /dev/null
+++ b/modules/lang/coq/autoload.el
@@ -0,0 +1,4 @@
+;;; lang/coq/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(add-hook 'coq-mode-hook #'company-coq-mode)
diff --git a/modules/lang/coq/config.el b/modules/lang/coq/config.el
new file mode 100644
index 000000000..8bfdcf90e
--- /dev/null
+++ b/modules/lang/coq/config.el
@@ -0,0 +1,18 @@
+;;; lang/coq/config.el -*- lexical-binding: t; -*-
+
+;; `coq'
+(setq proof-electric-terminator-enable t)
+
+;; We've replaced coq-mode abbrevs with yasnippet snippets (in the snippets
+;; library included with Doom).
+(setq coq-mode-abbrev-table '())
+
+
+(after! company-coq
+ (set-popup-rule! "^\\*\\(?:response\\|goals\\)\\*" :ignore t)
+ (set-lookup-handlers! 'company-coq-mode
+ :definition #'company-coq-jump-to-definition
+ :references #'company-coq-grep-symbol
+ :documentation #'company-coq-doc)
+ (unless (featurep! :completion company)
+ (setq company-coq-disabled-features '(company company-defaults))))
diff --git a/modules/lang/coq/packages.el b/modules/lang/coq/packages.el
new file mode 100644
index 000000000..78a5c61be
--- /dev/null
+++ b/modules/lang/coq/packages.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/coq/packages.el
+
+(package! proof-general)
+
+(package! company-coq)
diff --git a/modules/lang/crystal/config.el b/modules/lang/crystal/config.el
index 61618d2bf..dc38bc408 100644
--- a/modules/lang/crystal/config.el
+++ b/modules/lang/crystal/config.el
@@ -1,15 +1,21 @@
;;; lang/crystal/config.el -*- lexical-binding: t; -*-
-(def-package! crystal-mode
- :mode "\\.cr$"
- :interpreter "crystal"
- :config
- (set! :eval 'crystal-mode
- '((:command . "crystal")
- (:exec . "%c %s")
- (:description . "Run Crystal script"))))
+(after! crystal-mode
+ (set-lookup-handlers! 'crystal-mode
+ :definition #'crystal-def-jump
+ :references #'crystal-tool-imp)
+ (set-eval-handler! 'crystal-mode
+ '((:command . "crystal")
+ (:exec . "%c %s")
+ (:description . "Run Crystal script")))
+ (after! dtrt-indent
+ (add-to-list 'dtrt-indent-hook-mapping-list '(crystal-mode ruby crystal-indent-level))))
(def-package! flycheck-crystal
- :after crystal-mode
- :config (add-hook 'crystal-mode-hook #'flycheck-mode))
+ :when (featurep! :tools flycheck)
+ :after crystal-mode)
+
+
+(def-package! inf-crystal
+ :commands crystal-switch-to-inf)
diff --git a/modules/lang/crystal/doctor.el b/modules/lang/crystal/doctor.el
new file mode 100644
index 000000000..b2f68f86e
--- /dev/null
+++ b/modules/lang/crystal/doctor.el
@@ -0,0 +1,5 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/crystal/doctor.el
+
+(unless (executable-find "icr")
+ (warn! "Couldn't find icr. REPL will not work"))
diff --git a/modules/lang/crystal/packages.el b/modules/lang/crystal/packages.el
index d0fd6e985..e9fd0086b 100644
--- a/modules/lang/crystal/packages.el
+++ b/modules/lang/crystal/packages.el
@@ -3,4 +3,4 @@
(package! crystal-mode)
(package! flycheck-crystal)
-
+(package! inf-crystal)
diff --git a/modules/lang/csharp/README.org b/modules/lang/csharp/README.org
new file mode 100644
index 000000000..6006e1689
--- /dev/null
+++ b/modules/lang/csharp/README.org
@@ -0,0 +1,28 @@
+#+TITLE: :lang csharp
+
+This module adds C# support to Emacs.
+
+#+begin_quote
+I don't use C# for much else than Unity3D and, seldomly, for Mono game
+development on Linux.
+#+end_quote
+
+* Table of Contents :TOC:
+- [[Install][Install]]
+ - [[MacOS][MacOS]]
+ - [[Arch Linux][Arch Linux]]
+
+* Install
+This module needs:
+
++ omnisharp-roslyn (install with ~M-x omnisharp-install-server~)
++ .NET SDKs (on Windows)
++ Mono (on UNIX platforms)
+
+** MacOS
+=TODO=
+
+** Arch Linux
+#+BEGIN_SRC sh :dir /sudo:: :tangle (if (doom-system-os 'arch) "yes")
+sudo pacman --needed --noconfirm -S mono
+#+END_SRC
diff --git a/modules/lang/csharp/autoload.el b/modules/lang/csharp/autoload.el
new file mode 100644
index 000000000..0cdedf0bd
--- /dev/null
+++ b/modules/lang/csharp/autoload.el
@@ -0,0 +1,10 @@
+;;; lang/csharp/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +csharp-sp-point-in-type-p (id action context)
+ "Return t if point is in the right place for C# angle-brackets."
+ (and (sp-in-code-p id action context)
+ (cond ((eq action 'insert)
+ (sp-point-after-word-p id action context))
+ ((eq action 'autoskip)
+ (/= (char-before) 32)))))
diff --git a/modules/lang/csharp/config.el b/modules/lang/csharp/config.el
index 84f05ad68..cb8e40aae 100644
--- a/modules/lang/csharp/config.el
+++ b/modules/lang/csharp/config.el
@@ -1,49 +1,66 @@
;;; lang/csharp/config.el -*- lexical-binding: t; -*-
-(def-package! csharp-mode :mode "\\.cs$")
+(after! csharp-mode
+ (add-hook 'csharp-mode-hook #'rainbow-delimiters-mode)
+
+ (set-electric! 'csharp-mode :chars '(?\n ?\}))
+ (set-rotate-patterns! 'csharp-mode
+ :symbols '(("public" "protected" "private")
+ ("class" "struct")))
+ (sp-local-pair 'csharp-mode "<" ">"
+ :when '(+csharp-sp-point-in-type-p)
+ :post-handlers '(("| " "SPC"))))
(def-package! omnisharp
- :after csharp-mode
+ :hook (csharp-mode . omnisharp-mode)
+ :commands omnisharp-install-server
:preface
(setq omnisharp-auto-complete-want-documentation nil
- omnisharp-server-executable-path (concat doom-local-dir "OmniSharp.exe"))
+ omnisharp-cache-directory (concat doom-cache-dir "omnisharp"))
:config
- (if (file-exists-p omnisharp-server-executable-path)
- (add-hook! csharp-mode #'(eldoc-mode flycheck-mode omnisharp-mode))
- (warn "csharp-mode: omnisharp server isn't installed, completion won't work"))
+ (defun +csharp|cleanup-omnisharp-server ()
+ "Clean up the omnisharp server once you kill the last csharp-mode buffer."
+ (unless (doom-buffers-in-mode 'csharp-mode (buffer-list))
+ (omnisharp-stop-server)))
+ (add-hook! csharp-mode
+ (add-hook 'kill-buffer-hook #'+csharp|cleanup-omnisharp-server nil t))
- (set! :company-backend 'csharp-mode '(company-omnisharp))
+ (set-company-backend! 'csharp-mode 'company-omnisharp)
+ (set-lookup-handlers! 'csharp-mode
+ :definition #'omnisharp-go-to-definition
+ :references #'omnisharp-find-usages
+ :documentation #'omnisharp-current-type-documentation)
- (map! :map omnisharp-mode-map
- :m "gd" #'omnisharp-go-to-definition
-
- (:localleader
- :n "b" #'omnisharp-recompile
-
- (:prefix "r"
- :n "i" #'omnisharp-fix-code-issue-at-point
- :n "u" #'omnisharp-fix-usings
- :n "r" #'omnisharp-rename
- :n "a" #'omnisharp-show-last-auto-complete-result
- :n "o" #'omnisharp-show-overloads-at-point)
-
- (:prefix "f"
- :n "u" #'omnisharp-find-usages
- :n "i" #'omnisharp-find-implementations
- :n "f" #'omnisharp-navigate-to-current-file-member
- :n "m" #'omnisharp-navigate-to-solution-member
- :n "M" #'omnisharp-navigate-to-solution-file-then-file-member
- :n "F" #'omnisharp-navigate-to-solution-file
- :n "r" #'omnisharp-navigate-to-region
- :n "ti" #'omnisharp-current-type-information
- :n "td" #'omnisharp-current-type-documentation)
-
- (:prefix "t"
- :n "r" (λ! (omnisharp-unit-test "fixture"))
- :n "s" (λ! (omnisharp-unit-test "single"))
- :n "a" (λ! (omnisharp-unit-test "all"))))))
+ (map! :localleader
+ :map omnisharp-mode-map
+ "b" #'omnisharp-recompile
+ (:prefix "r"
+ "i" #'omnisharp-fix-code-issue-at-point
+ "u" #'omnisharp-fix-usings
+ "r" #'omnisharp-rename
+ "a" #'omnisharp-show-last-auto-complete-result
+ "o" #'omnisharp-show-overloads-at-point)
+ (:prefix "f"
+ "u" #'omnisharp-find-usages
+ "i" #'omnisharp-find-implementations
+ "f" #'omnisharp-navigate-to-current-file-member
+ "m" #'omnisharp-navigate-to-solution-member
+ "M" #'omnisharp-navigate-to-solution-file-then-file-member
+ "F" #'omnisharp-navigate-to-solution-file
+ "r" #'omnisharp-navigate-to-region
+ "ti" #'omnisharp-current-type-information
+ "td" #'omnisharp-current-type-documentation)
+ (:prefix "t"
+ "r" (λ! (omnisharp-unit-test "fixture"))
+ "s" (λ! (omnisharp-unit-test "single"))
+ "a" (λ! (omnisharp-unit-test "all")))))
-(def-package! shader-mode :mode "\\.shader$") ; unity shaders
+(when (featurep! +unity)
+ ;; `shader-mode' --- unity shaders
+ (add-to-list 'auto-mode-alist '("\\.shader$" . shader-mode))
+ (def-project-mode! +csharp-unity-mode
+ :modes (csharp-mode shader-mode)
+ :files (and "Assets" "Library/MonoManager.asset" "Library/ScriptMapper")))
diff --git a/modules/lang/csharp/doctor.el b/modules/lang/csharp/doctor.el
new file mode 100644
index 000000000..82c1c191d
--- /dev/null
+++ b/modules/lang/csharp/doctor.el
@@ -0,0 +1,7 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/csharp/doctor.el
+
+(require 'omnisharp)
+(let ((omnisharp-bin (or omnisharp-server-executable-path (omnisharp--server-installation-path t))))
+ (unless (file-exists-p omnisharp-bin)
+ (warn! "Omnisharp server isn't installed, completion won't work")))
diff --git a/modules/lang/csharp/packages.el b/modules/lang/csharp/packages.el
index 57ba0f02e..49b94bed4 100644
--- a/modules/lang/csharp/packages.el
+++ b/modules/lang/csharp/packages.el
@@ -3,5 +3,6 @@
(package! csharp-mode)
(package! omnisharp)
-(package! shader-mode)
+(when (featurep! +unity)
+ (package! shader-mode))
diff --git a/modules/lang/csharp/setup.sh b/modules/lang/csharp/setup.sh
deleted file mode 100755
index 9d0ceb478..000000000
--- a/modules/lang/csharp/setup.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env bash
-
-source VARS
-
-#
-echo "Setting up C# (omnisharp)"
-
-git-repo "https://github.com/OmniSharp/omnisharp-server" omnisharp
-cd omnisharp && xbuild
-mv omnisharp/bin/Debug/OmniSharp.exe ./OmniSharp.exe
-rm -rf omnisharp
diff --git a/modules/lang/data/config.el b/modules/lang/data/config.el
index 71f7078e5..c18fb15a9 100644
--- a/modules/lang/data/config.el
+++ b/modules/lang/data/config.el
@@ -1,51 +1,40 @@
;;; lang/data/config.el -*- lexical-binding: t; -*-
-(push '("/sxhkdrc" . conf-mode) auto-mode-alist)
+;; Built in plugins
+(add-to-list 'auto-mode-alist '("/sxhkdrc\\'" . conf-mode))
+(add-to-list 'auto-mode-alist '("\\.\\(?:hex\\|nes\\)\\'" . hexl-mode))
+(add-to-list 'auto-mode-alist '("\\.plist\\'" . nxml-mode))
+
+(after! nxml-mode
+ (set-company-backend! 'nxml-mode '(company-nxml company-yasnippet)))
-(def-package! nxml-mode
- :mode "\\.plist$"
- :config
- (set! :company-backend 'nxml-mode '(company-nxml company-yasnippet)))
+;;
+;; Third-party plugins
+;; `csv-mode'
+(map! :after csv-mode
+ :localleader
+ :map csv-mode-map
+ "a" #'csv-align-fields
+ "u" #'csv-unalign-fields
+ "s" #'csv-sort-fields
+ "S" #'csv-sort-numeric-fields
+ "k" #'csv-kill-fields
+ "t" #'csv-transpose)
-(def-package! toml-mode :mode "\\.toml$")
-
-
-(def-package! yaml-mode :mode "\\.ya?ml$")
-
+(def-package! graphql-mode
+ :mode "\\.gql\\'")
(def-package! json-mode
- :mode "\\.js\\(on\\|[hl]int\\(rc\\)?\\)$"
+ :mode "\\.js\\(?:on\\|[hl]int\\(?:rc\\)?\\)\\'"
:config
- (set! :electric 'json-mode :chars '(?\n ?: ?{ ?})))
-
-
-(def-package! vimrc-mode
- :mode "/\\.?g?vimrc$"
- :mode "\\.vim$"
- :mode "\\.?vimperatorrc$"
- :mode "\\.vimp$")
-
-
-(def-package! dockerfile-mode
- :mode "/Dockerfile$")
-
-
-;; For ROM hacking or debugging
-(def-package! hexl
- :mode ("\\.hex$" . hexl-mode)
- :mode ("\\.nes$" . hexl-mode))
+ (set-electric! 'json-mode :chars '(?\n ?: ?{ ?})))
;;
;; Frameworks
-;;
-
-(def-project-mode! +data-ansible-mode
- :modes (yaml-mode)
- :files "roles/")
(def-project-mode! +data-vagrant-mode
- :files "Vagrantfile")
+ :files ("Vagrantfile"))
diff --git a/modules/lang/data/packages.el b/modules/lang/data/packages.el
index 6de75af15..a8ccfae9e 100644
--- a/modules/lang/data/packages.el
+++ b/modules/lang/data/packages.el
@@ -1,9 +1,9 @@
;; -*- no-byte-compile: t; -*-
;;; lang/data/packages.el
-(package! dockerfile-mode)
+(package! graphql-mode)
(package! json-mode)
(package! toml-mode)
-(package! vimrc-mode)
(package! yaml-mode)
-
+(package! csv-mode)
+(package! dhall-mode)
diff --git a/modules/lang/elixir/config.el b/modules/lang/elixir/config.el
index 7e332d5ce..71c4973e0 100644
--- a/modules/lang/elixir/config.el
+++ b/modules/lang/elixir/config.el
@@ -1,37 +1,57 @@
;;; lang/elixir/config.el -*- lexical-binding: t; -*-
(def-package! elixir-mode
- :mode "\\.exs?$"
- :mode "\\.elixir2$"
+ :defer t
+ :init
+ ;; Disable default smartparens config. There are too many pairs; we only want
+ ;; a subset of them (defined below).
+ (provide 'smartparens-elixir)
:config
- ;; disable standard config; more disruptive than it needs to be
- (dolist (beg '("fn" "do" "def" "defp" "defmodule" "if" "unless" "case" "receive"))
- (sp-local-pair 'elixir-mode beg nil :actions :rem))
- ;; only complete the basics
- (sp-with-modes 'elixir-mode
- (sp-local-pair "do" "end" :when '(("RET" "")) :post-handlers '("||\n[i]"))
- (sp-local-pair "do " " end")
- (sp-local-pair "fn " " end")))
+ (set-pretty-symbols! 'elixir-mode
+ ;; Functional
+ :def "def"
+ :lambda "fn"
+ ;; :src_block "do"
+ ;; :src_block_end "end"
+ ;; Flow
+ :not "!"
+ :in "in" :not-in "not in"
+ :and "and" :or "or"
+ :for "for"
+ :return "return" :yield "use")
+
+ ;; ...and only complete the basics
+ (after! smartparens
+ (sp-with-modes 'elixir-mode
+ (sp-local-pair "do" "end"
+ :when '(("RET" ""))
+ :unless '(sp-in-comment-p sp-in-string-p)
+ :post-handlers '("||\n[i]"))
+ (sp-local-pair "do " " end" :unless '(sp-in-comment-p sp-in-string-p))
+ (sp-local-pair "fn " " end" :unless '(sp-in-comment-p sp-in-string-p))))
+
+ (def-package! alchemist-company
+ :when (featurep! :completion company)
+ :commands alchemist-company
+ :init
+ (set-company-backend! 'elixir-mode '(alchemist-company company-yasnippet))
+ :config
+ ;; Alchemist doesn't use hook symbols to add these backends, so we have to
+ ;; use the entire closure to get rid of it.
+ (let ((fn (byte-compile (lambda () (add-to-list (make-local-variable 'company-backends) 'alchemist-company)))))
+ (remove-hook 'alchemist-mode-hook fn)
+ (remove-hook 'alchemist-iex-mode-hook fn)))
+
+ (def-package! flycheck-credo
+ :when (featurep! :tools flycheck)
+ :config (flycheck-credo-setup)))
(def-package! alchemist
- :after elixir-mode
:hook (elixir-mode . alchemist-mode)
:config
- (set! :jump 'elixir-mode
+ (set-lookup-handlers! 'elixir-mode
:definition #'alchemist-goto-definition-at-point
:documentation #'alchemist-help-search-at-point)
- (set! :eval 'elixir-mode #'alchemist-eval-region))
-
-
-(def-package! alchemist-company
- :when (featurep! :completion company)
- :after elixir-mode
- :config
- ;; Let Doom handle this
- (let ((fn (byte-compile (lambda () (add-to-list (make-local-variable 'company-backends) 'alchemist-company)))))
- (remove-hook 'alchemist-mode-hook fn)
- (remove-hook 'alchemist-iex-mode-hook fn))
-
- (set! :company-backend 'elixir-mode '(alchemist-company company-yasnippet)))
-
+ (set-eval-handler! 'elixir-mode #'alchemist-eval-region)
+ (set-repl-handler! 'elixir-mode #'alchemist-iex-project-run))
diff --git a/modules/lang/elixir/packages.el b/modules/lang/elixir/packages.el
index 8fc1db9dc..a50e54ce7 100644
--- a/modules/lang/elixir/packages.el
+++ b/modules/lang/elixir/packages.el
@@ -4,4 +4,5 @@
;; +elixir.el
(package! elixir-mode)
(package! alchemist)
-(package! ac-alchemist)
+(when (featurep! :tools flycheck)
+ (package! flycheck-credo))
diff --git a/modules/lang/elm/config.el b/modules/lang/elm/config.el
index f205c9b45..b10dde8c1 100644
--- a/modules/lang/elm/config.el
+++ b/modules/lang/elm/config.el
@@ -1,14 +1,25 @@
;;; lang/elm/config.el -*- lexical-binding: t; -*-
-(def-package! elm-mode
- :mode "\\.elm$"
- :config
- (add-hook! 'elm-mode-hook #'(flycheck-mode rainbow-delimiters-mode))
- (set! :company-backend 'elm-mode '(company-elm))
- (setq elm-format-on-save t))
+;; `elm-mode'
+(setq elm-format-on-save t)
+
+(after! elm-mode
+ (add-hook 'elm-mode-hook #'rainbow-delimiters-mode)
+
+ (set-company-backend! 'elm-mode 'company-elm)
+ (set-repl-handler! 'elm-mode #'run-elm-interactive)
+ (set-pretty-symbols! 'elm-mode
+ :null "null"
+ :true "true" :false "false"
+ :int "Int" :str "String"
+ :float "Float"
+ :bool "Bool"
+
+ :not "not"
+ :and "&&" :or "||"))
(def-package! flycheck-elm
- :after (:all flycheck elm-mode)
- :hook (flycheck-mode . flycheck-elm-setup))
-
+ :when (featurep! :tools flycheck)
+ :after elm-mode
+ :config (add-to-list 'flycheck-checkers 'elm nil #'eq))
diff --git a/modules/lang/elm/packages.el b/modules/lang/elm/packages.el
index ec64c5c0e..23bf4d45d 100644
--- a/modules/lang/elm/packages.el
+++ b/modules/lang/elm/packages.el
@@ -1,6 +1,7 @@
;; -*- no-byte-compile: t; -*-
;;; lang/elm/packages.el
-(package! flycheck-elm)
(package! elm-mode)
+(when (featurep! :tools flycheck)
+ (package! flycheck-elm))
diff --git a/modules/lang/emacs-lisp/autoload.el b/modules/lang/emacs-lisp/autoload.el
index 949a401fc..e85c851ff 100644
--- a/modules/lang/emacs-lisp/autoload.el
+++ b/modules/lang/emacs-lisp/autoload.el
@@ -1,7 +1,86 @@
;;; lang/emacs-lisp/autoload.el -*- lexical-binding: t; -*-
+;;
+;; Library
+
;;;###autoload
-(defun +emacs-lisp/repl ()
+(defun +emacs-lisp-eval (beg end)
+ "Evaluate a region and print it to the echo area (if one line long), otherwise
+to a pop up buffer."
+ (require 'pp)
+ (let ((result
+ (let ((debug-on-error t)
+ (doom--current-module (ignore-errors (doom-module-from-path buffer-file-name))))
+ (eval (read
+ (concat "(progn "
+ (buffer-substring-no-properties beg end)
+ "\n)"))
+ t)))
+ (buf (get-buffer-create "*doom eval*"))
+ (inhibit-read-only t))
+ (with-current-buffer buf
+ (read-only-mode +1)
+ (erase-buffer)
+ (setq-local scroll-margin 0)
+ (let (emacs-lisp-mode-hook)
+ (emacs-lisp-mode))
+ (prin1 result buf)
+ (pp-buffer)
+ (let ((lines (count-lines (point-min) (point-max))))
+ (if (> lines 1)
+ (save-selected-window
+ (pop-to-buffer buf)
+ (with-current-buffer buf
+ (goto-char (point-min))))
+ (message "%s" (buffer-substring (point-min) (point-max)))
+ (kill-buffer buf))))))
+
+(defvar +emacs-lisp--face nil)
+;;;###autoload
+(defun +emacs-lisp-highlight-vars-and-faces (end)
+ "Match defined variables and functions.
+
+Functions are differentiated into special forms, built-in functions and
+library/userland functions"
+ (catch 'matcher
+ (while (re-search-forward "\\_<.+?\\_>" end t)
+ (unless (save-excursion
+ (let ((ppss (syntax-ppss)))
+ (or (nth 3 ppss) (nth 4 ppss))))
+ (let ((symbol (intern-soft (match-string-no-properties 0))))
+ (and (cond ((null symbol) nil)
+ ((eq symbol t) nil)
+ ((special-variable-p symbol)
+ (setq +emacs-lisp--face 'font-lock-variable-name-face))
+ ((and (fboundp symbol)
+ (eq (char-before (match-beginning 0)) ?\())
+ (let ((unaliased (indirect-function symbol)))
+ (unless (or (macrop unaliased)
+ (special-form-p unaliased))
+ (let (unadvised)
+ (while (not (eq (setq unadvised (ad-get-orig-definition unaliased))
+ (setq unaliased (indirect-function unadvised)))))
+ unaliased)
+ (setq +emacs-lisp--face
+ (if (subrp unaliased)
+ 'font-lock-constant-face
+ 'font-lock-function-name-face))))))
+ (throw 'matcher t)))))
+ nil))
+
+;; `+emacs-lisp-highlight-vars-and-faces' is a potentially expensive function
+;; and should be byte-compiled, no matter what, to ensure it runs as fast as
+;; possible:
+(when (not (byte-code-function-p (symbol-function '+emacs-lisp-highlight-vars-and-faces)))
+ (with-no-warnings
+ (byte-compile #'+emacs-lisp-highlight-vars-and-faces)))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defun +emacs-lisp/open-repl ()
"Open the Emacs Lisp REPL (`ielm')."
(interactive)
(pop-to-buffer
@@ -11,25 +90,44 @@
(bury-buffer buf)
buf)))))
+
+;;
+;; Hooks
+
;;;###autoload
-(defun +emacs-lisp-eval (beg end)
- "Evaluate a region and print it to the echo area (if one line long), otherwise
-to a pop up buffer."
- (require 'pp)
- (let ((result (eval (read (concat "(progn " (buffer-substring-no-properties beg end) "\n)"))))
- (buf (get-buffer-create "*doom eval*"))
- (inhibit-read-only t)
- lines)
- (with-current-buffer buf
- (read-only-mode +1)
- (erase-buffer)
- (setq-local scroll-margin 0)
- (emacs-lisp-mode)
- (prin1 result buf)
- (pp-buffer)
- (setq lines (count-lines (point-min) (point-max)))
- (goto-char (point-min))
- (if (> lines 1)
- (doom-popup-buffer buf)
- (message "%s" (buffer-substring (point-min) (point-max)))
- (kill-buffer buf)))))
+(defun +emacs-lisp|extend-imenu ()
+ "Improve imenu support with better expression regexps and Doom-specific forms."
+ (setq imenu-generic-expression
+ '(("Evil commands" "^\\s-*(evil-define-\\(?:command\\|operator\\|motion\\) +\\(\\_<[^ ()\n]+\\_>\\)" 1)
+ ("Unit tests" "^\\s-*(\\(?:ert-deftest\\|describe\\) +\"\\([^\")]+\\)\"" 1)
+ ("Package" "^\\s-*(\\(?:def-\\)?package! +\\(\\_<[^ ()\n]+\\_>\\)" 1)
+ ("Package" "^\\s-*;;;###package\\s-+\\(\\_<[^ ()\n]+\\_>\\)$" 1)
+ ("Major modes" "^\\s-*(define-derived-mode +\\([^ ()\n]+\\)" 1)
+ ("Modelines" "^\\s-*(def-modeline! +\\([^ ()\n]+\\)" 1)
+ ("Modeline segments" "^\\s-*(def-modeline-segment! +\\([^ ()\n]+\\)" 1)
+ ("Advice" "^\\s-*(def\\(?:\\(?:ine-\\)?advice\\))")
+ ("Modes" "^\\s-*(define-\\(?:global\\(?:ized\\)?-minor\\|generic\\|minor\\)-mode +\\([^ ()\n]+\\)" 1)
+ ("Macros" "^\\s-*(\\(?:cl-\\)?def\\(?:ine-compile-macro\\|macro\\) +\\([^ )\n]+\\)" 1)
+ ("Inline functions" "\\s-*(\\(?:cl-\\)?defsubst +\\([^ )\n]+\\)" 1)
+ ("Functions" "^\\s-*(\\(?:cl-\\)?def\\(?:un\\|un\\*\\|method\\|generic\\|-memoized!\\) +\\([^ ,)\n]+\\)" 1)
+ ("Variables" "^\\s-*(\\(def\\(?:c\\(?:onst\\(?:ant\\)?\\|ustom\\)\\|ine-symbol-macro\\|parameter\\|var\\(?:-local\\)?\\)\\)\\s-+\\(\\(?:\\sw\\|\\s_\\|\\\\.\\)+\\)" 2)
+ ("Types" "^\\s-*(\\(cl-def\\(?:struct\\|type\\)\\|def\\(?:class\\|face\\|group\\|ine-\\(?:condition\\|error\\|widget\\)\\|package\\|struct\\|t\\(?:\\(?:hem\\|yp\\)e\\)\\)\\)\\s-+'?\\(\\(?:\\sw\\|\\s_\\|\\\\.\\)+\\)" 2))))
+
+;;;###autoload
+(defun +emacs-lisp|disable-flycheck-maybe ()
+ "Disable flycheck-mode if in emacs.d."
+ (when (and (bound-and-true-p flycheck-mode)
+ (eq major-mode 'emacs-lisp-mode)
+ (or (not buffer-file-name)
+ (cl-loop for dir in (list doom-emacs-dir doom-private-dir)
+ if (file-in-directory-p buffer-file-name dir)
+ return t)))
+ (flycheck-mode -1)))
+
+;;;###autoload
+(defun +emacs-lisp-lookup-documentation (thing)
+ "Lookup THING with `helpful-variable' if it's a variable, `helpful-callable'
+if it's callable, `apropos' otherwise."
+ (if thing
+ (doom/describe-symbol thing)
+ (call-interactively #'doom/describe-symbol)))
diff --git a/modules/lang/emacs-lisp/config.el b/modules/lang/emacs-lisp/config.el
index f602a494b..63faeb3bc 100644
--- a/modules/lang/emacs-lisp/config.el
+++ b/modules/lang/emacs-lisp/config.el
@@ -1,12 +1,32 @@
;;; lang/emacs-lisp/config.el -*- lexical-binding: t; -*-
-(def-package! elisp-mode ; built-in
- :mode ("/Cask$" . emacs-lisp-mode)
+(defvar +emacs-lisp-enable-extra-fontification t
+ "If non-nil, highlight special forms, and defined functions and variables.")
+
+(defvar +emacs-lisp-outline-regexp "[ \t]*;;;;* [^ \t\n]"
+ "Regexp to use for `outline-regexp' in `emacs-lisp-mode'.
+This marks a foldable marker for `outline-minor-mode' in elisp buffers.")
+
+
+;; `elisp-mode' is loaded at startup. In order to lazy load its config we need
+;; to pretend it isn't loaded
+(defer-feature! elisp-mode emacs-lisp-mode)
+
+
+;;
+;; Config
+
+(def-package! elisp-mode
+ :mode ("\\.Cask\\'" . emacs-lisp-mode)
:config
- (set! :repl 'emacs-lisp-mode #'+emacs-lisp/repl)
- (set! :eval 'emacs-lisp-mode #'+emacs-lisp-eval)
- (set! :jump 'emacs-lisp-mode :documentation #'describe-symbol)
- (set! :rotate 'emacs-lisp-mode
+ (set-repl-handler! 'emacs-lisp-mode #'+emacs-lisp/open-repl)
+ (set-eval-handler! 'emacs-lisp-mode #'+emacs-lisp-eval)
+ (set-lookup-handlers! 'emacs-lisp-mode
+ :definition #'elisp-def
+ :documentation #'+emacs-lisp-lookup-documentation)
+ (set-docsets! 'emacs-lisp-mode "Emacs Lisp")
+ (set-pretty-symbols! 'emacs-lisp-mode :lambda "lambda")
+ (set-rotate-patterns! 'emacs-lisp-mode
:symbols '(("t" "nil")
("let" "let*")
("when" "unless")
@@ -15,110 +35,111 @@
("add-hook" "remove-hook")
("add-hook!" "remove-hook!")))
+ (setq-hook! 'emacs-lisp-mode-hook
+ ;; shorter name in modeline
+ mode-name "Elisp"
+ ;; Don't treat autoloads or sexp openers as outline headers, we have
+ ;; hideshow for that.
+ outline-regexp +emacs-lisp-outline-regexp)
+
+ ;; variable-width indentation is superior in elisp
+ (add-to-list 'doom-detect-indentation-excluded-modes 'emacs-lisp-mode nil #'eq)
+
(add-hook! 'emacs-lisp-mode-hook
#'(;; 3rd-party functionality
- eldoc-mode auto-compile-on-save-mode doom|enable-delete-trailing-whitespace
- ;; fontification
- rainbow-delimiters-mode highlight-quoted-mode highlight-numbers-mode +emacs-lisp|extra-fontification
+ auto-compile-on-save-mode
+ outline-minor-mode
;; initialization
- +emacs-lisp|init-imenu +emacs-lisp|init-flycheck))
+ +emacs-lisp|extend-imenu))
- ;;
- (defun +emacs-lisp|extra-fontification ()
- "Display lambda as a smybol and fontify doom module functions."
- (font-lock-add-keywords
- nil `(;; Display "lambda" as λ
- ("(\\(lambda\\)" (1 (ignore (compose-region (match-beginning 1) (match-end 1) ?λ #'decompose-region))))
- ;; Highlight doom/module functions
- ("\\(^\\|\\s-\\|,\\)(\\(\\(doom\\|\\+\\)[^) ]+\\)[) \n]" (2 font-lock-keyword-face)))))
+ ;; Flycheck produces a *lot* of false positives in emacs configs, so disable
+ ;; it when you're editing them
+ (add-hook 'flycheck-mode-hook #'+emacs-lisp|disable-flycheck-maybe)
- (defun +emacs-lisp|init-imenu ()
- "Improve imenu support with better expression regexps and Doom-specific forms."
- (setq imenu-generic-expression
- '(("Evil Commands" "^\\s-*(evil-define-\\(?:command\\|operator\\|motion\\) +\\(\\_<[^ ()\n]+\\_>\\)" 1)
- ("Package" "^\\s-*(\\(?:def-\\)?package! +\\(\\_<[^ ()\n]+\\_>\\)" 1)
- ("Settings" "^\\s-*(def-setting! +\\([^ ()\n]+\\)" 1)
- ("Modelines" "^\\s-*(def-modeline! +\\([^ ()\n]+\\)" 1)
- ("Modeline Segments" "^\\s-*(def-modeline-segment! +\\([^ ()\n]+\\)" 1)
- ("Advice" "^\\s-*(def\\(?:\\(?:ine-\\)?advice\\))")
- ("Modes" "^\\s-*(define-\\(?:global\\(?:ized\\)?-minor\\|generic\\|minor\\)-mode +\\([^ ()\n]+\\)" 1)
- ("Macros" "^\\s-*(\\(?:cl-\\)?def\\(?:ine-compile-macro\\|macro\\) +\\([^ )\n]+\\)" 1)
- ("Inline Functions" "\\s-*(\\(?:cl-\\)?defsubst +\\([^ )\n]+\\)" 1)
- ("Functions" "^\\s-*(\\(?:cl-\\)?def\\(?:un\\|un\\*\\|method\\|generic\\|-memoized!\\) +\\([^ ,)\n]+\\)" 1)
- ("Variables" "^\\s-*(\\(def\\(?:c\\(?:onst\\(?:ant\\)?\\|ustom\\)\\|ine-symbol-macro\\|parameter\\)\\)\\s-+\\(\\(?:\\sw\\|\\s_\\|\\\\.\\)+\\)" 2)
- ("Variables" "^\\s-*(defvar\\(?:-local\\)?\\s-+\\(\\(?:\\sw\\|\\s_\\|\\\\.\\)+\\)[[:space:]\n]+[^)]" 1)
- ("Types" "^\\s-*(\\(cl-def\\(?:struct\\|type\\)\\|def\\(?:class\\|face\\|group\\|ine-\\(?:condition\\|error\\|widget\\)\\|package\\|struct\\|t\\(?:\\(?:hem\\|yp\\)e\\)\\)\\)\\s-+'?\\(\\(?:\\sw\\|\\s_\\|\\\\.\\)+\\)" 2))))
+ ;; Special fontification for elisp
+ (font-lock-add-keywords
+ 'emacs-lisp-mode
+ (append `(;; custom Doom cookies
+ ("^;;;###\\(autodef\\|if\\|package\\)[ \n]" (1 font-lock-warning-face t)))
+ ;; highlight defined, special variables & functions
+ (when +emacs-lisp-enable-extra-fontification
+ `((+emacs-lisp-highlight-vars-and-faces . +emacs-lisp--face)))))
- (defun +emacs-lisp|init-flycheck ()
- "Initialize flycheck-mode if not in emacs.d."
- (when (and buffer-file-name
- (not (file-in-directory-p buffer-file-name doom-emacs-dir)))
- (flycheck-mode +1))))
+ (add-hook! 'emacs-lisp-mode-hook #'(rainbow-delimiters-mode highlight-quoted-mode))
+
+ ;; Recenter window after following definition
+ (advice-add #'elisp-def :after #'doom*recenter)
+
+ (map! :localleader
+ :map emacs-lisp-mode-map
+ "e" #'macrostep-expand))
;;
-;; Plugins
-;;
+;; Packages
-(def-package! auto-compile
- :commands auto-compile-on-save-mode
- :config
- (setq auto-compile-display-buffer nil
- auto-compile-use-mode-line nil))
+;; `auto-compile'
+(setq auto-compile-display-buffer nil
+ auto-compile-use-mode-line nil)
-(def-package! highlight-quoted
- :commands highlight-quoted-mode)
+;; `macrostep'
+(when (featurep! :feature evil)
+ (after! macrostep
+ (evil-define-key* 'normal macrostep-keymap
+ [return] #'macrostep-expand
+ "e" #'macrostep-expand
+ "u" #'macrostep-collapse
+ "c" #'macrostep-collapse
+
+ [tab] #'macrostep-next-macro
+ "\C-n" #'macrostep-next-macro
+ "J" #'macrostep-next-macro
+
+ [backtab] #'macrostep-prev-macro
+ "K" #'macrostep-prev-macro
+ "\C-p" #'macrostep-prev-macro
+
+ "q" #'macrostep-collapse-all
+ "C" #'macrostep-collapse-all)
+
+ ;; `evil-normalize-keymaps' seems to be required for macrostep or it won't
+ ;; apply for the very first invocation
+ (add-hook 'macrostep-mode-hook #'evil-normalize-keymaps)))
-(def-package! slime
- :defer t
- :config
- (setq inferior-lisp-program "clisp")
- (require 'slime-fuzzy))
-
-
-(def-package! macrostep
- :commands macrostep-expand
- :config
- (map! :map macrostep-keymap
- :n "RET" #'macrostep-expand
- :n "e" #'macrostep-expand
- :n "u" #'macrostep-collapse
- :n "c" #'macrostep-collapse
-
- :n "TAB" #'macrostep-next-macro
- :n "n" #'macrostep-next-macro
- :n "J" #'macrostep-next-macro
-
- :n "S-TAB" #'macrostep-prev-macro
- :n "K" #'macrostep-prev-macro
- :n "p" #'macrostep-prev-macro
-
- :n "q" #'macrostep-collapse-all
- :n "C" #'macrostep-collapse-all)
- ;; `evil-normalize-keymaps' seems to be required for macrostep or it won't
- ;; apply for the very first invocation
- (add-hook 'macrostep-mode-hook #'evil-normalize-keymaps))
+;; `overseer'
+(autoload 'overseer-test "overseer" nil t)
+(remove-hook 'emacs-lisp-mode-hook 'overseer-enable-mode)
(def-package! flycheck-cask
- :when (featurep! :feature syntax-checker)
- :commands flycheck-cask-setup
+ :when (featurep! :tools flycheck)
+ :defer t
:init
- (add-hook! 'emacs-lisp-hook
+ (add-hook! 'emacs-lisp-mode-hook
(add-hook 'flycheck-mode-hook #'flycheck-cask-setup nil t)))
-(def-package! overseer
- :commands overseer-test
- :init (set! :popup "*overseer*" :size 12))
+(def-package! elisp-demos
+ :defer t
+ :init
+ (advice-add 'describe-function-1 :after #'elisp-demos-advice-describe-function-1)
+ (advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update))
;;
-;;
-;;
+;; Project modes
(def-project-mode! +emacs-lisp-ert-mode
+ :modes (emacs-lisp-mode)
+ :match "/test[/-].+\\.el$"
+ :add-hooks (overseer-enable-mode))
+
+(associate! buttercup-minor-mode
:modes (emacs-lisp-mode)
:match "/test[/-].+\\.el$")
+
+(after! buttercup
+ (set-yas-minor-mode! 'buttercup-minor-mode))
+
diff --git a/modules/lang/emacs-lisp/packages.el b/modules/lang/emacs-lisp/packages.el
index 24b02c21b..67b572ecd 100644
--- a/modules/lang/emacs-lisp/packages.el
+++ b/modules/lang/emacs-lisp/packages.el
@@ -1,11 +1,14 @@
;; -*- no-byte-compile: t; -*-
;;; lang/emacs-lisp/packages.el
+(package! elisp-mode :built-in t)
+
(package! auto-compile)
(package! highlight-quoted)
(package! macrostep)
(package! overseer)
-(package! slime)
+(package! elisp-def)
+(package! elisp-demos)
-(when (featurep! :feature syntax-checker)
+(when (featurep! :tools flycheck)
(package! flycheck-cask))
diff --git a/modules/lang/erlang/config.el b/modules/lang/erlang/config.el
new file mode 100644
index 000000000..ed91e5229
--- /dev/null
+++ b/modules/lang/erlang/config.el
@@ -0,0 +1,25 @@
+;;; lang/erlang/config.el -*- lexical-binding: t; -*-
+
+(def-package! erlang
+ :mode ("\\.erlang$" . erlang-mode)
+ :mode ("/rebar\\.config\\(?:\\.script\\)?$" . erlang-mode)
+ :mode ("/\\(?:app\\|sys\\)\\.config$" . erlang-mode))
+
+
+(def-package! flycheck-rebar3
+ :when (featurep! :tools flycheck)
+ :after flycheck
+ :config (flycheck-rebar3-setup))
+
+
+(def-package! ivy-erlang-complete
+ :when (featurep! :completion ivy)
+ :hook (erlang-mode . ivy-erlang-complete-init)
+ :config
+ (add-hook! 'erlang-mode-hook
+ (add-hook 'after-save-hook #'ivy-erlang-complete-reparse nil t)))
+
+
+(def-package! company-erlang
+ :when (featurep! :completion company)
+ :hook (erlang-mode . company-erlang-init))
diff --git a/modules/lang/erlang/packages.el b/modules/lang/erlang/packages.el
new file mode 100644
index 000000000..00a871c99
--- /dev/null
+++ b/modules/lang/erlang/packages.el
@@ -0,0 +1,13 @@
+;; -*- no-byte-compile: t; -*-
+;;; private/erlang/packages.el
+
+(package! erlang)
+
+(when (featurep! :tools flycheck)
+ (package! flycheck-rebar3))
+
+(when (featurep! :completion ivy)
+ (package! ivy-erlang-complete))
+
+(when (featurep! :completion company)
+ (package! company-erlang))
diff --git a/modules/lang/ess/README.org b/modules/lang/ess/README.org
new file mode 100644
index 000000000..55330d7ab
--- /dev/null
+++ b/modules/lang/ess/README.org
@@ -0,0 +1,50 @@
+#+TITLE: :lang ess
+
+This module adds support for various statistics languages, including R, S-Plus,
+SAS, Julia and Stata.
+
+* Table of Contents :TOC:
+- [[Appendix][Appendix]]
+ - [[Keybindings][Keybindings]]
+
+* Appendix
+** Keybindings
+*** :map ess-doc-map
+| key | command |
+|-----+----------------------------|
+| "h" | ess-display-help-on-object |
+| "p" | ess-R-dv-pprint |
+| "t" | ess-R-dv-ctable |
+*** :map ess-mode-map
+| key | command |
+|--------------+-----------------------|
+| "" | ess-eval-line |
+| "" | comint-next-input |
+| "" | comint-previous-input |
+**** :localleader
+| state | key | command |
+|-------+-------------+---------------------------------------------------|
+| :nv | "," | ess-eval-region-or-function-or-paragraph-and-step |
+| :n | "'" | R |
+| :n | "" | ess-switch-to-inferior-or-script-buffer |
+| :n | "" | ess-switch-process |
+| :n | "B" | ess-eval-buffer-and-go |
+| :n | "b" | ess-eval-buffer |
+| :nv | "d" | ess-eval-region-or-line-and-step |
+| :n | "D" | ess-eval-function-or-paragraph-and-step |
+| :n | "L" | ess-eval-line-and-go |
+| :n | "l" | ess-eval-line |
+| :nv | "R" | ess-eval-region-and-go |
+| :nv | "r" | ess-eval-region |
+| :n | "F" | ess-eval-function-and-go |
+| :n | "f" | ess-eval-function |
+| :n | "h" | ess-doc-map |
+| :n | "x" | ess-extra-map |
+| :n | "p" | ess-r-package-dev-map |
+| :n | "v" | ess-dev-map |
+| :n | "cC" | ess-eval-chunk-and-go |
+| :n | "cc" | ess-eval-chunk |
+| :n | "cd" | ess-eval-chunk-and-step |
+| :n | "cm" | ess-noweb-mark-chunk |
+| :n | "cp" | ess-noweb-previous-chunk |
+| :n | "cn" | ess-noweb-next-chunk |
diff --git a/modules/lang/ess/autoload.el b/modules/lang/ess/autoload.el
new file mode 100644
index 000000000..05575a7b1
--- /dev/null
+++ b/modules/lang/ess/autoload.el
@@ -0,0 +1,11 @@
+;;; lang/ess/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +ess-repl-buffer (&optional start-args)
+ "Returns an R/Julia REPL buffer."
+ (interactive "P")
+ (pcase major-mode
+ ('ess-r-mode (run-ess-r start-args))
+ ((or 'julia-mode 'ess-julia-mode) (run-julia start-args))
+ (_ (inferior-ess nil nil t)))
+ (current-buffer))
diff --git a/modules/lang/ess/config.el b/modules/lang/ess/config.el
new file mode 100644
index 000000000..249c9264a
--- /dev/null
+++ b/modules/lang/ess/config.el
@@ -0,0 +1,72 @@
+;;; lang/ess/config.el -*- lexical-binding: t; -*-
+
+(def-package! ess
+ :commands (stata SAS)
+ :init
+ (setq ess-smart-S-assign-key nil)
+ (unless (featurep! :lang julia)
+ (add-to-list 'auto-mode-alist '("\\.jl\\'" . ess-julia-mode)))
+ :config
+ (setq ess-offset-continued 'straight
+ ess-expression-offset 2
+ ess-use-flymake (not (featurep! :tools flycheck))
+ ess-nuke-trailing-whitespace-p t
+ ess-default-style 'DEFAULT
+ ess-history-directory (expand-file-name "ess-history/" doom-cache-dir))
+
+ (set-repl-handler! '(ess-r-mode ess-julia-mode) #'+ess-repl-buffer)
+ (set-lookup-handlers! '(ess-r-mode ess-julia-mode)
+ :documentation #'ess-display-help-on-object)
+
+ (set-evil-initial-state! 'ess-r-help-mode 'normal)
+ (set-eval-handler! 'ess-help-mode #'ess-eval-region-and-go)
+ (set-eval-handler! 'ess-r-help-mode #'ess-eval-region-and-go)
+
+ (map! (:after ess-help
+ :map ess-help-mode-map
+ :n "q" #'kill-this-buffer
+ :n "Q" #'ess-kill-buffer-and-go
+ :n "K" #'ess-display-help-on-object
+ :n "go" #'ess-display-help-in-browser
+ :n "gO" #'ess-display-help-apropos
+ :n "gv" #'ess-display-vignettes
+ :m "]]" #'ess-skip-to-next-section
+ :m "[[" #'ess-skip-to-previous-section
+ :map ess-doc-map
+ "h" #'ess-display-help-on-object
+ "p" #'ess-R-dv-pprint
+ "t" #'ess-R-dv-ctable
+ [C-return] #'ess-eval-line
+ [up] #'comint-next-input
+ [down] #'comint-previous-input)
+
+ :localleader
+ :map ess-mode-map
+ "," #'ess-eval-region-or-function-or-paragraph-and-step
+ "'" #'R
+ [tab] #'ess-switch-to-inferior-or-script-buffer
+ [backtab] #'ess-switch-process
+ ;; REPL
+ "B" #'ess-eval-buffer-and-go
+ "b" #'ess-eval-buffer
+ "d" #'ess-eval-region-or-line-and-step
+ "D" #'ess-eval-function-or-paragraph-and-step
+ "L" #'ess-eval-line-and-go
+ "l" #'ess-eval-line
+ "R" #'ess-eval-region-and-go
+ "r" #'ess-eval-region
+ "F" #'ess-eval-function-and-go
+ "f" #'ess-eval-function
+ ;; predefined keymaps
+ "h" 'ess-doc-map
+ "x" 'ess-extra-map
+ "p" 'ess-r-package-dev-map
+ "v" 'ess-dev-map
+ ;; noweb
+ :prefix "c"
+ "C" #'ess-eval-chunk-and-go
+ "c" #'ess-eval-chunk
+ "d" #'ess-eval-chunk-and-step
+ "m" #'ess-noweb-mark-chunk
+ "p" #'ess-noweb-previous-chunk
+ "n" #'ess-noweb-next-chunk))
diff --git a/modules/lang/ess/packages.el b/modules/lang/ess/packages.el
new file mode 100644
index 000000000..dc9e8a9f8
--- /dev/null
+++ b/modules/lang/ess/packages.el
@@ -0,0 +1,5 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/ess/packages.el
+
+(package! ess)
+(package! ess-R-data-view)
diff --git a/modules/lang/go/README.org b/modules/lang/go/README.org
index 3447c530e..53cbe4192 100644
--- a/modules/lang/go/README.org
+++ b/modules/lang/go/README.org
@@ -1,5 +1,20 @@
-#+TITLE: :lang go
+#+TITLE: lang/go
+#+DATE: January 16, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
+* Table of Contents :TOC:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Prerequisites][Prerequisites]]
+ - [[Go][Go]]
+ - [[Dependencies][Dependencies]]
+- [[Features][Features]]
+- [[Configuration][Configuration]]
+- [[Troubleshooting][Troubleshooting]]
+
+* Description
This module adds [[https://golang.org][Go]] support.
+ Code completion (~gocode~)
@@ -12,29 +27,28 @@ This module adds [[https://golang.org][Go]] support.
+ [[../../feature/file-templates/templates/go-mode][File templates]]
+ [[https://github.com/hlissner/emacs-snippets/tree/master/go-mode][Snippets]]
-#+begin_quote
-I have mixed feelings about Go. It's a decent compromise between C and higher-level languages, is pleasantly straight-forward and elegant, but lacks /native/ support for luxuries I miss from other languages, like generics, optional arguments, and function overloading. You've got to learn to love ~interface{}~.
+** Module Flags
+This module provides no flags.
-Still, Go is a remarkably useful (and fast!) companion for a variety of small-to-medium backend web and CLI projects.
-#+end_quote
+** Plugins
++ [[https://github.com/dominikh/go-mode.el][go-mode]]
++ [[https://github.com/syohex/emacs-go-eldoc][go-eldoc]]
++ [[https://github.com/dominikh/go-mode.el][go-guru]]
++ [[https://github.com/manute/gorepl-mode][gorepl-mode]]
++ [[https://github.com/mdempsky/gocode][company-go]]*
-* Table of Contents :TOC:
-- [[#install][Install]]
- - [[#go][Go]]
- - [[#dependencies][Dependencies]]
-
-* Install
+* Prerequisites
** Go
To get started with Go, you need the ~go~ tool:
*** MacOS
-#+BEGIN_SRC sh :tangle (if (doom-system-os 'macos) "yes")
+#+BEGIN_SRC bash
brew install go
#+END_SRC
*** Arch Linux
-#+BEGIN_SRC sh :dir /sudo:: :tangle (if (doom-system-os 'arch) "yes")
-sudo pacman --needed --noconfirm -S go
+#+BEGIN_SRC bash
+sudo pacman -S go
#+END_SRC
** Dependencies
@@ -42,19 +56,24 @@ This module requires a valid ~GOPATH~, and the following Go packages:
+ ~gocode~ (for code completion & eldoc support)
+ ~godoc~ (for documentation lookup)
-+ ~goimports~ (for auto-formatting code on save and fixing imports)
+ ~gorename~ (for extra refactoring commands)
+ ~gore~ (for the REPL)
+ ~guru~ (for code navigation & refactoring commands)
++ ~goimports~ (optional: for auto-formatting code on save & fixing imports)
#+BEGIN_SRC sh
export GOPATH=~/work/go
-go get -u github.com/motemen/gore
-go get -u github.com/nsf/gocode
+go get -u github.com/motemen/gore/cmd/gore
+go get -u github.com/mdempsky/gocode
go get -u golang.org/x/tools/cmd/godoc
go get -u golang.org/x/tools/cmd/goimports
go get -u golang.org/x/tools/cmd/gorename
go get -u golang.org/x/tools/cmd/guru
#+END_SRC
+* TODO Features
+
+* TODO Configuration
+
+* TODO Troubleshooting
diff --git a/modules/lang/go/autoload.el b/modules/lang/go/autoload.el
index 18a3e1ca6..8ef830833 100644
--- a/modules/lang/go/autoload.el
+++ b/modules/lang/go/autoload.el
@@ -2,12 +2,12 @@
;;
;; Tests
-;;
(defvar +go-test-last nil
"The last test run.")
(defun +go--run-tests (args)
+ (require 'async)
(save-selected-window
(async-shell-command (concat "go test " args))))
@@ -37,3 +37,10 @@
(+go--run-tests (concat "-run" "='" (match-string-no-properties 2) "'")))
(error "Must be in a _test.go file")))
+;;;###autoload
+(defun +go/play-buffer-or-region (&optional beg end)
+ "TODO"
+ (interactive "r")
+ (if (use-region-p)
+ (go-play-region beg end)
+ (go-play-buffer)))
diff --git a/modules/lang/go/config.el b/modules/lang/go/config.el
index 2cb8222a9..09b2a2c61 100644
--- a/modules/lang/go/config.el
+++ b/modules/lang/go/config.el
@@ -1,95 +1,66 @@
;;; lang/go/config.el -*- lexical-binding: t; -*-
-(def-package! go-mode
- :mode "\\.go$"
- :interpreter "go"
- :config
- (add-hook 'go-mode-hook #'flycheck-mode)
+;;
+;; Packages
- (setq gofmt-command "goimports")
- (if (not (executable-find "goimports"))
- (warn "go-mode: couldn't find goimports; no code formatting/fixed imports on save")
- (add-hook! go-mode (add-hook 'before-save-hook #'gofmt-before-save nil t)))
-
- (set! :repl 'go-mode #'gorepl-run)
- (set! :jump 'go-mode
+(after! go-mode
+ (set-docsets! 'go-mode "Go")
+ (set-repl-handler! 'go-mode #'gorepl-run)
+ (set-lookup-handlers! 'go-mode
:definition #'go-guru-definition
:references #'go-guru-referrers
:documentation #'godoc-at-point)
- (def-menu! +go/refactor-menu
- "Refactoring commands for `go-mode' buffers."
- '(("Add import" :exec go-import-add :region nil)
- ("Remove unused imports" :exec go-remove-unused-imports :region nil)
- ("Format buffer (gofmt)" :exec go-gofmt))
- :prompt "Refactor: ")
+ ;; Redefines default formatter to *not* use goimports if reformatting a
+ ;; region; as it doesn't play well with partial code.
+ (set-formatter! 'gofmt
+ '(("%s" (if (or +format-region-p
+ (not (executable-find "goimports")))
+ "gofmt"
+ "goimports"))))
- (def-menu! +go/build-menu
- "Build/compilation commands for `go-mode' buffers."
- '(("Build project" :exec "go build")
- ("Build & run project" :exec "go run")
- ("Clean files" :exec "go clean"))
- :prompt "Run test: ")
-
- (def-menu! +go/test-menu
- "Test commands for `go-mode' buffers."
- '(("Last test" :exec +go/test-rerun)
- ("All tests" :exec +go/test-all)
- ("Single test" :exec +go/test-single)
- ("Nested test" :exec +go/test-nested))
- :prompt "Run test: ")
-
- (def-menu! +go/help-menu
- "Help and information commands for `go-mode' buffers."
- '(("Go to imports" :exec go-goto-imports)
- ("Lookup in godoc" :exec godoc-at-point)
- ("Describe this" :exec go-guru-describe)
- ("List free variables" :exec go-guru-freevars)
- ("What does this point to" :exec go-guru-pointsto)
- ("Implements relations for package types" :exec go-guru-implements :region nil)
- ("List peers for channel" :exec go-guru-peers)
- ("List references to object" :exec go-guru-referrers)
- ("Which errors" :exec go-guru-whicerrs)
- ("What query" :exec go-guru-what)
- ("Show callers of this function" :exec go-guru-callers :region nil)
- ("Show callees of this function" :exec go-guru-callees :region nil)))
+ (if (featurep! +lsp)
+ (add-hook 'go-mode-hook #'lsp!)
+ (add-hook 'go-mode-hook #'go-eldoc-setup))
(map! :map go-mode-map
:localleader
- "r" #'+go/refactor-menu
- "b" #'+go/build-menu
- "h" #'+go/help-menu
- "t" #'+go/test-menu
- :n "gr" #'go-play-buffer
- :v "gr" #'go-play-region))
-
-
-(def-package! go-eldoc
- :hook (go-mode . go-eldoc-setup))
-
-
-(def-package! go-guru
- :commands (go-guru-describe go-guru-freevars go-guru-implements go-guru-peers
- go-guru-referrers go-guru-definition go-guru-pointsto
- go-guru-callstack go-guru-whicherrs go-guru-callers go-guru-callees
- go-guru-expand-region)
- :config
- (unless (executable-find "guru")
- (warn "go-mode: couldn't find guru, refactoring commands won't work")))
+ "e" #'+go/play-buffer-or-region
+ "i" #'go-goto-imports ; Go to imports
+ (:prefix ("h" . "help")
+ "." #'godoc-at-point ; Lookup in godoc
+ "d" #'go-guru-describe ; Describe this
+ "v" #'go-guru-freevars ; List free variables
+ "i" #'go-guru-implements ; Implements relations for package types
+ "p" #'go-guru-peers ; List peers for channel
+ "P" #'go-guru-pointsto ; What does this point to
+ "r" #'go-guru-referrers ; List references to object
+ "e" #'go-guru-whicherrs ; Which errors
+ "w" #'go-guru-what ; What query
+ "c" #'go-guru-callers ; Show callers of this function
+ "C" #'go-guru-callees) ; Show callees of this function
+ (:prefix ("ri" . "imports")
+ "a" #'go-import-add
+ "r" #'go-remove-unused-imports)
+ (:prefix ( "b" . "build")
+ :desc "go run ." "r" (λ! (compile "go run ."))
+ :desc "go build" "b" (λ! (compile "go build"))
+ :desc "go clean" "c" (λ! (compile "go clean")))
+ (:prefix ("t" . "test")
+ "t" #'+go/test-rerun
+ "a" #'+go/test-all
+ "s" #'+go/test-single
+ "n" #'+go/test-nested)))
(def-package! gorepl-mode
- :commands (gorepl-run gorepl-run-load-current-file)
- :config
- (unless (executable-find "gore")
- (warn "go-mode: couldn't find gore, REPL support disabled")))
+ :commands gorepl-run-load-current-file)
(def-package! company-go
- :init (setq command-go-gocode-command "gocode")
- :when (featurep! :completion company)
+ :when (and (featurep! :completion company)
+ (not (featurep! +lsp)))
:after go-mode
:config
- (if (executable-find command-go-gocode-command)
- (set! :company-backend 'go-mode '(company-go))
- (warn "go-mode: couldn't find gocode, code completion won't work")))
+ (set-company-backend! 'go-mode 'company-go)
+ (setq company-go-show-annotation t))
diff --git a/modules/lang/go/doctor.el b/modules/lang/go/doctor.el
new file mode 100644
index 000000000..512780ba6
--- /dev/null
+++ b/modules/lang/go/doctor.el
@@ -0,0 +1,13 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/go/doctor.el
+
+(unless (executable-find "guru")
+ (warn! "Couldn't find guru. Refactoring commands (go-guru-*) won't work"))
+
+(unless (executable-find "gore")
+ (warn! "Couldn't find gore. REPL will not work"))
+
+(when (featurep! :completion company)
+ (require 'company-go)
+ (unless (executable-find company-go-gocode-command)
+ (warn! "Couldn't find gocode. Code completion won't work")))
diff --git a/modules/lang/haskell/+dante.el b/modules/lang/haskell/+dante.el
index 9e580ae51..2774b8a62 100644
--- a/modules/lang/haskell/+dante.el
+++ b/modules/lang/haskell/+dante.el
@@ -2,14 +2,39 @@
;;;###if (featurep! +dante)
(def-package! dante
- :after haskell-mode
- :hook (haskell-mode . dante-mode)
+ :hook (haskell-mode-local-vars . dante-mode)
+ :init
+ (setq dante-load-flags '(;; defaults:
+ "+c"
+ "-Wwarn=missing-home-modules"
+ "-fno-diagnostics-show-caret"
+ ;; neccessary to make attrap-attrap useful:
+ "-Wall"
+ ;; necessary to make company completion useful:
+ "-fdefer-typed-holes"
+ "-fdefer-type-errors"))
:config
- (add-hook 'haskell-mode-hook #'interactive-haskell-mode)
+ (when (featurep! :tools flycheck)
+ (flycheck-add-next-checker 'haskell-dante '(warning . haskell-hlint)))
- (unless (executable-find "cabal")
- (warn "haskell-mode: couldn't find cabal")
- (remove-hook 'haskell-mode-hook #'dante-mode))
+ (set-company-backend! 'dante-mode #'dante-company)
- (add-hook 'dante-mode-hook #'flycheck-mode))
+ (defun +haskell*restore-modified-state (orig-fn &rest args)
+ "Dante quietly saves the current buffer (without triggering save hooks) before
+invoking flycheck, unexpectedly leaving the buffer in an unmodified state. This
+is annoying if we depend on save hooks to do work on the buffer (like
+reformatting), so we restore a (false) modified state."
+ (let ((modified-p (buffer-modified-p)))
+ (apply orig-fn args)
+ (if modified-p (set-buffer-modified-p t))))
+ (advice-add #'dante-async-load-current-buffer :around #'+haskell*restore-modified-state)
+ (when (featurep 'evil)
+ (add-hook 'dante-mode-hook #'evil-normalize-keymaps))
+ (map! :map dante-mode-map
+ :localleader
+ "t" #'dante-type-at
+ "i" #'dante-info
+ "l" #'haskell-process-load-or-reload
+ "e" #'dante-eval-block
+ "a" #'attrap-attrap))
diff --git a/modules/lang/haskell/+intero.el b/modules/lang/haskell/+intero.el
index 666ef0355..1ba04599d 100644
--- a/modules/lang/haskell/+intero.el
+++ b/modules/lang/haskell/+intero.el
@@ -2,17 +2,29 @@
;;;###if (featurep! +intero)
(def-package! intero
- :hook (haskell-mode . intero-mode)
+ :commands intero-mode
+ :init
+ (defun +haskell|init-intero ()
+ "Initializes `intero-mode' in haskell-mode, unless stack isn't installed.
+This is necessary because `intero-mode' doesn't do its own error checks."
+ (when (derived-mode-p 'haskell-mode)
+ (if (executable-find "stack")
+ (intero-mode +1)
+ (message "Couldn't find stack. Refusing to enable intero-mode."))))
+ (add-hook 'haskell-mode-local-vars-hook #'+haskell|init-intero)
:config
- (unless (executable-find "stack")
- (warn "haskell-mode: couldn't find stack, disabling intero")
- (remove-hook 'haskell-mode-hook #'intero-mode))
+ (setq haskell-compile-cabal-build-command "stack build --fast")
+ (set-lookup-handlers! 'intero-mode :definition #'intero-goto-definition)
+ (set-company-backend! 'intero-mode 'intero-company)
+ (when (featurep! :tools flycheck)
+ (flycheck-add-next-checker 'intero '(warning . haskell-hlint)))
- (add-hook! 'intero-mode-hook #'(flycheck-mode eldoc-mode))
-
- (set! :popup "^intero:backend:" :regex t :size 12)
- (set! :jump 'haskell-mode :definition #'intero-goto-definition))
-
-
-(def-package! hindent
- :hook (haskell-mode . hindent-mode))
+ (when (featurep 'evil)
+ (add-hook 'intero-mode-hook #'evil-normalize-keymaps))
+ (map! :localleader
+ :map intero-mode-map
+ "t" #'intero-type-at
+ "i" #'intero-info
+ "l" #'intero-repl-load
+ "e" #'intero-repl-eval-region
+ "a" #'intero-apply-suggestions))
diff --git a/modules/lang/haskell/+lsp.el b/modules/lang/haskell/+lsp.el
new file mode 100644
index 000000000..576e26e67
--- /dev/null
+++ b/modules/lang/haskell/+lsp.el
@@ -0,0 +1,10 @@
+;;; lang/haskell/+lsp.el -*- lexical-binding: t; -*-
+
+(def-package! lsp-haskell
+ :after haskell-mode
+ :init (add-hook 'haskell-mode-hook #'lsp!)
+ :config
+ (when IS-MAC
+ (setq lsp-haskell-process-path-hie "hie-wrapper"))
+ ;; Does some strange indentation if it pastes in the snippet
+ (setq-hook! 'haskell-mode-hook yas-indent-line 'fixed))
diff --git a/modules/lang/haskell/README.org b/modules/lang/haskell/README.org
index f29b189e2..9f8fe77e6 100644
--- a/modules/lang/haskell/README.org
+++ b/modules/lang/haskell/README.org
@@ -1,6 +1,25 @@
-#+TITLE: :lang haskell
+#+TITLE: lang/haskell
+#+DATE: January 16, 2017
+#+SINCE: v0.7
+#+STARTUP: inlineimages
-This module adds [[https://www.haskell.org/][Haskell]] support, powered by either [[https://haskell-lang.org/intero][intero]] (the default) or [[https://github.com/jyp/dante][dante]].
+* Table of Contents :TOC:
+- [[#description][Description]]
+ - [[#external-resources][External resources]]
+ - [[#module-flags][Module Flags]]
+ - [[#plugins][Plugins]]
+- [[#prerequisites][Prerequisites]]
+ - [[#stack][Stack]]
+ - [[#cabal][Cabal]]
+ - [[#lsp][LSP]]
+ - [[#haskell-packages][Haskell packages]]
+- [[#configuration][Configuration]]
+ - [[#using-the-new-style-cabal-repl][Using the new-style cabal REPL]]
+- [[#troubleshooting][Troubleshooting]]
+
+* Description
+This module adds [[https://www.haskell.org/][Haskell]] support, powered by either [[https://haskell-lang.org/intero][intero]] (the default), [[https://github.com/jyp/dante][dante]]
+or [[https://github.com/emacs-lsp/lsp-haskell][LSP]].
+ Code completion (~company-ghc~)
+ Look up documentation (~hoogle~)
@@ -10,102 +29,130 @@ This module adds [[https://www.haskell.org/][Haskell]] support, powered by eithe
+ Code navigation (~dante~)
+ [[https://github.com/hlissner/emacs-snippets/tree/master/haskell-mode][Snippets]]
-#+begin_quote
-Haskell contends with C and Ruby as my favorite language. My Haskell code will never save the world, but I'll reach for it for small projects and programming exercises (like projecteuler.com or exercism.io).
-
-I'd love to incorporate it into my machine learning work, but Python and Julia hold that crown. For now.
-#+end_quote
-
-* Table of Contents :TOC:
-- [[#install][Install]]
- - [[#intero][Intero]]
- - [[#dante][Dante]]
-- [[#troubleshooting][Troubleshooting]]
-- [[#resources][Resources]]
-
-* Install
-This module has two submodules: *Intero* or *Dante*. To activate one, specify one or the other in your pubilc ~init.el~, e.g.:
-
-#+BEGIN_SRC emacs-lisp
-(doom! :lang (haskell +intero))
-;; or
-(doom! :lang (haskell +dante))
-#+END_SRC
-
-Your dependencies will change slightly, depending on which you choose:
-
-** Intero
-*** Haskell
-To get started you must install *stack*:
-
-**** MacOS
-#+BEGIN_SRC sh :tangle (if (doom-system-os 'macos) "yes")
-brew install haskell-stack
-stack setup
-#+END_SRC
-
-**** Arch Linux
-#+BEGIN_SRC sh :dir /sudo:: :tangle (if (doom-system-os 'arch) "yes")
-sudo pacman --needed --noconfirm -S stack
-# Replace pacaur with your AUR package manager of choice
-pacaur --needed --noconfirm -S ncurses5-compat-lib
-stack setup
-#+END_SRC
-
-*** External dependencies
-This module requires ~ghc-mod~.
-
-#+BEGIN_SRC sh
-stack install ghc-mod
-#+END_SRC
-
-Also ensure that ~\~/.local/bin~ is in ~PATH~:
-
-#+BEGIN_SRC sh
-# place this in your profile file, like ~/.bash_profile or ~/.zshenv
-export PATH="~/.local/bin:$PATH"
-#+END_SRC
-
-** Dante
-*** Haskell
-To get started with Dante and Haskell, you must install cabal
-
-+ cabal (the haskell package builder)
-+ ghc/ghci (the compiler, syntax checker & repl)
-
-**** MacOS
-#+BEGIN_SRC sh
-brew install cabal-install ghc
-#+END_SRC
-
-**** Arch Linux
-#+BEGIN_SRC sh
-sudo pacman --needed --noconfirm -S cabal-install ghc
-#+END_SRC
-
-*** External dependencies
-Dante requires ~ghc-mod~ and ~hoogle~:
-
-#+BEGIN_SRC sh
-cabal update
-cabal install happy haskell-src-exts # ghc-mod/hoogle dependencies
-cabal ghc-mod hoogle
-#+END_SRC
-
-And add Cabal's bin path to $PATH:
-
-#+BEGIN_SRC sh
-export PATH="$HOME/.cabal/bin:$PATH"
-#+END_SRC
-
-* Troubleshooting
-+ Stack users: a ~dist/setup-config~ in your project may cause [[ https://github.com/DanielG/ghc-mod/wiki#known-issues-related-to-stack][ghc-mod to not
- work]].
-
-* Resources
+** External resources
Here are a few resources I've found indespensible in my Haskell adventures:
+ [[http://learnyouahaskell.com/][Learn you a haskell for great good]]
+ [[http://haskellbook.com/][Haskell Programming from first principles]]
+ [[https://github.com/krispo/awesome-haskell][Awesome Haskell]]: an extensive list of haskell resources
+ [[https://docs.haskellstack.org/en/stable/README/][The Haskell Tool Stack docs]]
+
+** Module Flags
++ =+intero= Enables intero; a comprehensive, stack-based development environment
+ for Haskell.
++ =+dante= Enables dante; a fork of intero aimed at lightweightedness. It
+ doesn't depend on =stack=, supports both ~cabal~-only and ~stack~ projects,
+ but lacks eldoc support.
++ =+lsp= Enables lsp-haskell (this requires the ~:tools lsp~ to be enabled).
+
+** Plugins
++ [[https://github.com/haskell/haskell-mode][haskell-mode]]
++ =+dante=
+ + [[https://github.com/jyp/dante][dante]]
+ + [[https://github.com/jyp/attrap][attrap]]
++ =+intero=
+ + [[https://github.com/chrisdone/intero][intero]]
++ =+lsp=
+ + [[https://github.com/emacs-lsp/lsp-haskell][lsp-haskell]]
+
+* Prerequisites
+Depending on whether you use Intero, Dante or LSP, your dependencies will
+differ:
+
++ Intero and LSP users need =stack=
++ Dante users need =cabal=, =ghc= and =ghc-mod=
++ LSP users need the =haskell-ide-engine= LSP server
++ All users will need the =hoogle= package
+
+** Stack
+To use Intero, you need =stack=:
+
+*** MacOS
+#+BEGIN_SRC sh
+brew install haskell-stack
+stack setup
+#+END_SRC
+
+*** Arch Linux
+#+BEGIN_SRC sh
+sudo pacman -S stack
+# Replace pacaur with your AUR package manager of choice
+pacaur -S ncurses5-compat-lib
+stack setup
+#+END_SRC
+
+** Cabal
+To use Dante, you need =cabal= (the haskell package builder) and =ghci= (the
+compiler, syntax checker & repl):
+
+*** MacOS
+#+BEGIN_SRC sh
+brew install cabal-install ghc
+#+END_SRC
+
+*** Arch Linux
+#+BEGIN_SRC sh
+sudo pacman -S cabal-install ghc
+#+END_SRC
+
+** LSP
+You will need =stack= and =git= installed.
+
+You will find a comprehensive [[https://github.com/haskell/haskell-ide-engine#installation][install guide for haskell-ide-engine on its
+project page]], but here's a TL;DR:
+
+*** MacOS
+haskell-ide-engine must be build and installed manually on MacOS, e.g.
+
+#+BEGIN_SRC emacs-lisp
+git clone https://github.com/haskell/haskell-ide-engine
+cd haskell-ide-engine
+make
+#+END_SRC
+
+*** Arch Linux
+=haskell-ide-engine-git= is available on the AUR
+
+#+BEGIN_SRC emacs-lisp
+yay -S haskell-ide-engine-git
+#+END_SRC
+
+** Haskell packages
+You'll need to install the following packages using ~stack~ or ~cabal~:
+
++ (Dante users) =ghc-mod=
+ #+BEGIN_SRC sh
+ stack install ghc-mod
+ # or
+ cabal install ghc-mod
+ #+END_SRC
++ =hoogle=
+ #+BEGIN_SRC sh
+ cabal update
+ cabal install happy haskell-src-exts # ghc-mod/hoogle dependencies
+ cabal ghc-mod hoogle
+ # or
+ stack install ghc-mod
+ stack install hoogle
+ #+END_SRC
+
+And ensure the binaries for these packages are in your ~PATH~, e.g.
+
+#+BEGIN_SRC sh
+# place this in your profile file, like ~/.bash_profile or ~/.zshenv
+export PATH="~/.local/bin:$PATH"
+#+END_SRC
+
+* Configuration
+** Using the new-style cabal REPL
+=haskell-mode= will typically detect what REPL to run based on your project
+(e.g. stack, (old-style) cabal or ghc). If you want the new-style cabal REPL you
+must set ~haskell-process-type~ manually:
+
+#+BEGIN_SRC emacs-lisp
+(setq haskell-process-type 'cabal-new-repl)
+#+END_SRC
+
+* Troubleshooting
++ Stack users: a ~dist/setup-config~ file in your project may cause [[https://github.com/DanielG/ghc-mod/wiki#known-issues-related-to-stack][ghc-mod to
+ not work]].
diff --git a/modules/lang/haskell/autoload.el b/modules/lang/haskell/autoload.el
new file mode 100644
index 000000000..cfcd12be0
--- /dev/null
+++ b/modules/lang/haskell/autoload.el
@@ -0,0 +1,14 @@
+;;; lang/haskell/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +haskell/open-repl (&optional arg)
+ "Opens a Haskell REPL."
+ (interactive "P")
+ (if-let*
+ ((window
+ (display-buffer
+ (if (featurep! +intero)
+ (intero-repl-buffer arg)
+ (haskell-session-interactive-buffer (haskell-session))))))
+ (window-buffer window)
+ (error "Failed to display Haskell REPL")))
diff --git a/modules/lang/haskell/config.el b/modules/lang/haskell/config.el
index 8cc1fc152..6f8b016bd 100644
--- a/modules/lang/haskell/config.el
+++ b/modules/lang/haskell/config.el
@@ -1,40 +1,32 @@
;;; lang/haskell/config.el -*- lexical-binding: t; -*-
-(cond ((featurep! +intero) (load! +intero))
- ((featurep! +dante) (load! +dante)))
+(cond ((featurep! +intero) (load! "+intero"))
+ ((featurep! +dante) (load! "+dante"))
+ ((featurep! +lsp) (load! "+lsp")))
;;
-;; Common plugins
-;;
+;; Common packages
-(def-package! haskell-mode
- :mode "\\.hs$"
- :mode ("\\.ghci$" . ghci-script-mode)
- :mode ("\\.cabal$" . haskell-cabal-mode)
- :interpreter (("runghc" . haskell-mode)
- ("runhaskell" . haskell-mode))
- :config
- (load "haskell-mode-autoloads" nil t)
+(after! haskell-mode
+ (setq haskell-process-suggest-remove-import-lines t ; warnings for redundant imports etc
+ haskell-process-auto-import-loaded-modules t
+ haskell-process-show-overlays (not (featurep! :tools flycheck))) ; redundant with flycheck
- (set! :repl 'haskell-mode #'switch-to-haskell)
- (push ".hi" completion-ignored-extensions)
+ (set-lookup-handlers! 'haskell-mode :definition #'haskell-mode-jump-to-def-or-tag)
+ (set-file-template! 'haskell-mode :trigger #'haskell-auto-insert-module-template :project t)
+ (set-repl-handler! '(haskell-mode haskell-cabal-mode literate-haskell-mode) #'+haskell/open-repl)
- (autoload 'switch-to-haskell "inf-haskell" nil t)
- (after! inf-haskell
- (map! :map inferior-haskell-mode-map "ESC ESC" #'doom/popup-close)))
+ (add-hook! 'haskell-mode-hook
+ #'(haskell-collapse-mode ; support folding haskell code blocks
+ interactive-haskell-mode))
+ (add-to-list 'completion-ignored-extensions ".hi")
-(def-package! company-ghc
- :when (featurep! :completion company)
- :after haskell-mode
- :init
- (add-hook 'haskell-mode-hook #'ghc-comp-init)
- :config
- (if (executable-find "ghc-mod")
- (set! :company-backend 'haskell-mode #'company-ghc)
- (warn "haskell-mode: couldn't find ghc-mode")
- (remove-hook 'haskell-mode-hook #'ghc-comp-init))
-
- (setq company-ghc-show-info 'oneline))
-
+ (map! :localleader
+ :map haskell-mode-map
+ ;; this is set to use cabal for dante users and stack for intero users:
+ "b" #'haskell-process-cabal-build
+ "c" #'haskell-cabal-visit-file
+ "h" #'haskell-hide-toggle
+ "H" #'haskell-hide-toggle-all))
diff --git a/modules/lang/haskell/doctor.el b/modules/lang/haskell/doctor.el
new file mode 100644
index 000000000..fe571bc14
--- /dev/null
+++ b/modules/lang/haskell/doctor.el
@@ -0,0 +1,16 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/haskell/doctor.el
+
+(when (featurep! +dante)
+ (unless (executable-find "cabal")
+ (warn! "Couldn't find cabal, haskell-mode may have issues")))
+
+(when (featurep! +intero)
+ (unless (executable-find "stack")
+ (warn! "Couldn't find stack. Intero will not work")))
+
+(when (or (featurep! +dante) (featurep! +intero))
+ (unless (executable-find "hlint")
+ (warn! "Couldn't find hlint. Flycheck may have issues in haskell-mode")))
+
+
diff --git a/modules/lang/haskell/packages.el b/modules/lang/haskell/packages.el
index 981d218f8..b2fd48e5e 100644
--- a/modules/lang/haskell/packages.el
+++ b/modules/lang/haskell/packages.el
@@ -2,13 +2,11 @@
;;; lang/haskell/packages.el
(package! haskell-mode)
-(when (featurep! :completion company)
- (package! company-ghc))
-;;
(cond ((featurep! +dante)
- (package! dante))
- (t
- (package! intero)
- (package! hindent)))
-
+ (package! dante)
+ (package! attrap))
+ ((featurep! +intero)
+ (package! intero))
+ ((featurep! +lsp)
+ (package! lsp-haskell)))
diff --git a/modules/lang/hy/config.el b/modules/lang/hy/config.el
index a3a8007a2..92163ac89 100644
--- a/modules/lang/hy/config.el
+++ b/modules/lang/hy/config.el
@@ -1,4 +1,8 @@
;;; lang/hy/config.el -*- lexical-binding: t; -*-
(def-package! hy-mode
- :mode "\\.hy$")
+ :mode "\\.hy\\'"
+ :interpreter "hy"
+ :config
+ (set-repl-handler! 'hy-mode #'hy-shell-start-or-switch-to-shell)
+ (set-company-backend! 'hy-mode 'company-hy))
diff --git a/modules/lang/hy/packages.el b/modules/lang/hy/packages.el
index f3ccf2ce3..0b232824d 100644
--- a/modules/lang/hy/packages.el
+++ b/modules/lang/hy/packages.el
@@ -1,3 +1,4 @@
-;;; lang/hy/packages.el -*- no-byte-compile: t; -*-
+;; -*- no-byte-compile: t; -*-
+;;; lang/hy/packages.el
(package! hy-mode)
diff --git a/modules/lang/idris/README.org b/modules/lang/idris/README.org
new file mode 100644
index 000000000..60f8ef1a3
--- /dev/null
+++ b/modules/lang/idris/README.org
@@ -0,0 +1,3 @@
+#+TITLE: :lang idris
+
+Adds support for the [[https://www.idris-lang.org/][idris]] programming language.
diff --git a/modules/lang/idris/config.el b/modules/lang/idris/config.el
new file mode 100644
index 000000000..cd56fb071
--- /dev/null
+++ b/modules/lang/idris/config.el
@@ -0,0 +1,19 @@
+;;; lang/idris/config.el -*- lexical-binding: t; -*-
+
+(after! idris-mode
+ (add-hook! 'idris-mode-hook 'turn-on-idris-simple-indent)
+ (set-repl-handler! 'idris-mode 'idris-pop-to-repl)
+ (set-lookup-handlers! 'idris-mode
+ :documentation #'idris-docs-at-point
+ :file #'idris-load-file)
+ (map! :localleader
+ :map idris-mode-map
+ "r" #'idris-load-file
+ "t" #'idris-type-at-point
+ "d" #'idris-add-clause
+ "l" #'idris-make-lemma
+ "c" #'idris-case-split
+ "w" #'idris-make-with-block
+ "m" #'idris-add-missing
+ "p" #'idris-proof-search
+ "h" #'idris-docs-at-point))
diff --git a/modules/lang/idris/packages.el b/modules/lang/idris/packages.el
new file mode 100644
index 000000000..d600f1309
--- /dev/null
+++ b/modules/lang/idris/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/idris/packages.el
+
+(package! idris-mode)
diff --git a/modules/lang/java/+eclim.el b/modules/lang/java/+eclim.el
index 8bacaa9d6..df30de7bb 100644
--- a/modules/lang/java/+eclim.el
+++ b/modules/lang/java/+eclim.el
@@ -6,7 +6,7 @@
(def-package! eclim
:hook (java-mode . eclim-mode)
:config
- (set! :jump 'java-mode
+ (set-lookup-handlers! 'java-mode
:definition #'eclim-java-find-declaration
:references #'eclim-java-find-references
:documentation #'eclim-java-show-documentation-for-current-element)
@@ -16,43 +16,35 @@
help-at-pt-timer-delay 0.1)
(help-at-pt-set-timer)
- ;;
- (def-menu! +java/refactor-menu
- "Refactoring commands for `java-mode' buffers."
- '(("Generate constructor" :exec eclim-java-constructor)
- ("Generate getter & setter" :exec eclim-java-generate-getter-and-setter)
- ("Organize imports" :exec eclim-java-import-organize)
- ("Reformat" :exec eclim-java-format)
- ("Rename symbol at point" :exec eclim-java-refactor-rename-symbol-at-point :region nil)))
-
- (def-menu! +java/help-menu
- "Help and information commands for `java-mode' buffers."
- '(("Find documentation for current element" :exec eclim-java-show-documentation-for-current-element)
- ("Find references" :exec eclim-java-find-references)
- ("View call hierarchy" :exec eclim-java-call-hierarchy)
- ("View hierarchy" :exec eclim-java-hierarchy)
- ("View problems" :exec eclim-problems)))
-
- (def-menu! +java/project-menu
- "Building/compilation commands for `java-mode' buffers."
- '(("Build project" :exec eclim-project-build)
- ("Create project" :exec eclim-project-create)
- ("Delete project" :exec eclim-project-delete)
- ("Go to project" :exec eclim-project-goto)
- ("Import project" :exec eclim-project-import)
- ("Close project" :exec eclim-project-close)
- ("Open project" :exec eclim-project-open)
- ("Update project" :exec eclim-project-update)))
-
- (map! :map java-mode-map
- :localleader
- "r" #'+java/refactor-menu
- "c" #'+java/compile-menu
- "p" #'+java/project-menu))
+ (map! :localleader
+ :map java-mode-map
+ (:prefix "r"
+ "gc" #'eclim-java-constructor
+ "gg" #'eclim-java-generate-getter-and-setter
+ "oi" #'eclim-java-import-organize
+ "f" #'eclim-java-format
+ "r" #'eclim-java-refactor-rename-symbol-at-point)
+ (:prefix "h"
+ "." #'eclim-java-show-documentation-for-current-element
+ "r" #'eclim-java-find-references
+ "c" #'eclim-java-call-hierarchy
+ "h" #'eclim-java-hierarchy
+ "p" #'eclim-problems
+ "r" #'meghanada-reference
+ "t" #'meghanada-typeinfo)
+ (:prefix "b"
+ "b" #'eclim-project-build
+ "c" #'eclim-project-create
+ "d" #'eclim-project-delete
+ "g" #'eclim-project-goto
+ "i" #'eclim-project-import
+ "k" #'eclim-project-close
+ "o" #'eclim-project-open
+ "u" #'eclim-project-update)))
(def-package! company-emacs-eclim
:when (featurep! :completion company)
:after java-mode
:config
- (set! :company-backend 'java-mode '(company-emacs-eclim)))
+ (set-company-backend! 'java-mode '(company-emacs-eclim)))
diff --git a/modules/lang/java/+lsp.el b/modules/lang/java/+lsp.el
new file mode 100644
index 000000000..cd6f4c15a
--- /dev/null
+++ b/modules/lang/java/+lsp.el
@@ -0,0 +1,10 @@
+;;; lang/java/+lsp.el -*- lexical-binding: t; -*-
+;;;###if (featurep! +lsp)
+
+(def-package! lsp-java
+ :after-call java-mode
+ :init (add-hook 'java-mode-hook #'lsp!)
+ :config
+ ;; TODO keybinds
+ ;; TODO treemacs integration (?)
+ )
diff --git a/modules/lang/java/+meghanada.el b/modules/lang/java/+meghanada.el
index 70fc4d404..e422deb9b 100644
--- a/modules/lang/java/+meghanada.el
+++ b/modules/lang/java/+meghanada.el
@@ -3,39 +3,27 @@
(def-package! meghanada
:hook (java-mode . meghanada-mode)
- :config
+ :init
(setq meghanada-server-install-dir (concat doom-etc-dir "meghanada-server/")
meghanada-use-company (featurep! :completion company)
- meghanada-use-flycheck (featurep! :feature syntax-checker)
+ meghanada-use-flycheck (featurep! :tools flycheck)
meghanada-use-eldoc t
meghanada-use-auto-start t)
-
- (set! :jump 'java-mode
+ :config
+ (set-lookup-handlers! 'java-mode
:definition #'meghanada-jump-declaration
:references #'meghanada-reference)
- (add-hook! 'meghanada-mode-hook #'(flycheck-mode eldoc-mode))
-
- ;;
- (def-menu! +java/refactor-menu
- "Refactoring commands for `java-mode' buffers."
- '(("Add imports for unqualified classes" :exec meghanada-import-all)
- ("Optimize and clean up imports" :exec meghanada-optimize-import)
- ("Introduce local variable" :exec meghanada-local-variable)
- ("Format buffer code" :exec meghanada-code-beautify)))
-
- (def-menu! +java/help-menu
- "Help and information commands for `java-mode' buffers."
- '(("Find usages" :exec meghanada-reference)
- ("Show type hierarchives and implemented interfaces" :exec meghanada-typeinfo)))
-
- (def-menu! +java/project-menu
- "Project commands for `java-mode' buffers."
- '(("Compile current file" :exec meghanada-compile-file)
- ("Compile project" :exec meghanada-compile-project)))
-
- (map! :map java-mode-map
- :localleader
- :nv "r" #'+java/refactor-menu
- :nv "c" #'+java/compile-menu
- :nv "p" #'+java/project-menu))
+ (map! :localleader
+ :map java-mode-map
+ (:prefix "r"
+ "ia" #'meghanada-import-all
+ "io" #'meghanada-optimize-import
+ "l" #'meghanada-local-variable
+ "f" #'meghanada-code-beautify)
+ (:prefix "h"
+ "r" #'meghanada-reference
+ "t" #'meghanada-typeinfo)
+ (:prefix "b"
+ "f" #'meghanada-compile-file
+ "p" #'meghanada-compile-project)))
diff --git a/modules/lang/java/autoload.el b/modules/lang/java/autoload.el
index 3e7a73c78..45c27aa1e 100644
--- a/modules/lang/java/autoload.el
+++ b/modules/lang/java/autoload.el
@@ -24,9 +24,48 @@
;;;###autoload
(defun +java|android-mode-maybe ()
- (when (doom-project-has! (or "local.properties"
- "AndroidManifest.xml"
- "src/main/AndroidManifest.xml"))
+ (when (project-file-exists-p! (or "local.properties"
+ "AndroidManifest.xml"
+ "src/main/AndroidManifest.xml"))
(android-mode +1)
(doom/set-build-command "./gradlew %s" "build.gradle")))
+;;;###autoload
+(defun +java-current-package ()
+ "Converts the current file's path into a namespace.
+
+For example: ~/some/project/src/net/lissner/game/MyClass.java
+Is converted to: net.lissner.game
+
+It does this by ignoring everything before the nearest package root (see
+`+java-project-package-roots' to control what this function considers a package
+root)."
+ (unless (eq major-mode 'java-mode)
+ (user-error "Not in a java-mode buffer"))
+ (let* ((project-root (file-truename (doom-project-root)))
+ (file-path (file-name-sans-extension
+ (file-truename (or buffer-file-name
+ default-directory))))
+ (src-root (cl-loop for root in +java-project-package-roots
+ if (and (stringp root)
+ (locate-dominating-file file-path root))
+ return (file-name-directory (file-relative-name file-path (expand-file-name root it)))
+ if (and (integerp root)
+ (> root 0)
+ (let* ((parts (split-string (file-relative-name file-path project-root) "/"))
+ (fixed-parts (reverse (nbutlast (reverse parts) root))))
+ (when fixed-parts
+ (string-join fixed-parts "/"))))
+ return it)))
+ (when src-root
+ (string-remove-suffix "." (replace-regexp-in-string "/" "." src-root)))))
+
+;;;###autoload
+(defun +java-current-class ()
+ "Get the class name for the current file."
+ (unless (eq major-mode 'java-mode)
+ (user-error "Not in a java-mode buffer"))
+ (unless buffer-file-name
+ (user-error "This buffer has no filepath; cannot guess its class name"))
+ (or (file-name-sans-extension (file-name-base (buffer-file-name)))
+ "ClassName"))
diff --git a/modules/lang/java/config.el b/modules/lang/java/config.el
index 8194c7923..f0fc35743 100644
--- a/modules/lang/java/config.el
+++ b/modules/lang/java/config.el
@@ -1,28 +1,45 @@
;;; lang/java/config.el -*- lexical-binding: t; -*-
+(defvar +java-project-package-roots (list "java/" "test/" "main/" "src/" 1)
+ "A list of relative directories (strings) or depths (integer) used by
+`+java-current-package' to delimit the namespace from the current buffer's full
+file path. Each root is tried in sequence until one is found.
+
+If a directory is encountered in the file path, everything before it (including
+it) will be ignored when converting the path into a namespace.
+
+An integer depth is how many directories to pop off the start of the relative
+file path (relative to the project root). e.g.
+
+Say the absolute path is ~/some/project/src/java/net/lissner/game/MyClass.java
+The project root is ~/some/project
+If the depth is 1, the first directory in src/java/net/lissner/game/MyClass.java
+ is removed: java.net.lissner.game.
+If the depth is 2, the first two directories are removed: net.lissner.game.")
+
+
+;;
+;; java-mode
+
(add-hook 'java-mode-hook #'rainbow-delimiters-mode)
-(cond ((featurep! +meghanada) (load! +meghanada))
- ((featurep! +eclim) ; FIXME lang/java +eclim
- ;;(load! +eclim)
- (warn "java-mode: eclim support isn't implemented yet")))
+(cond ((featurep! +lsp) (load! "+lsp"))
+ ((featurep! +meghanada) (load! "+meghanada")))
;;
-;; Common plugins
-;;
+;; Common packages
(def-package! android-mode
:commands android-mode
:init
(add-hook! (java-mode groovy-mode nxml-mode) #'+java|android-mode-maybe)
:config
- (set! :yas-minor-mode 'android-mode)
- (set! :company-dict-minor-mode 'android-mode))
+ (set-yas-minor-mode! 'android-mode))
(def-package! groovy-mode
- :mode "\\.g\\(radle\\|roovy\\)$"
+ :mode "\\.g\\(?:radle\\|roovy\\)$"
:config
- (set! :eval 'groovy-mode "groovy"))
+ (set-eval-handler! 'groovy-mode "groovy"))
diff --git a/modules/lang/java/packages.el b/modules/lang/java/packages.el
index 03deb4911..c9a0014a2 100644
--- a/modules/lang/java/packages.el
+++ b/modules/lang/java/packages.el
@@ -12,3 +12,5 @@
(when (featurep! :completion company)
(package! company-emacs-eclim)))
+(when (featurep! +lsp)
+ (package! lsp-java))
diff --git a/modules/lang/javascript/+screeps.el b/modules/lang/javascript/+screeps.el
deleted file mode 100644
index 179ebb54b..000000000
--- a/modules/lang/javascript/+screeps.el
+++ /dev/null
@@ -1,174 +0,0 @@
-;;; lang/javascript/+screeps.el -*- lexical-binding: t; -*-
-
-;; TODO Constants may be out-of-date
-
-(defconst screeps-objects
- '("ConstructionSite" "Creep" "Flag" "Game" "Memory" "Mineral" "Nuke"
- "OwnedStructure" "PathFinder" "RawMemory" "Resource" "Room"
- "RoomObject" "RoomPosition" "Source"
-
- "Structure" "StructureController" "StructureExtension"
- "StructureExtractor" "StructureKeeperLair" "StructureLab"
- "StructureLink" "StructureNuker" "StructureObserver"
- "StructurePortal" "StructurePowerBank" "StructurePowerSpawn"
- "StructureRampart" "StructureRoad" "StructureSpawn"
- "StructureStorage" "StructureTerminal" "StructureTower"
- "StructureWall"))
-
-(defconst screeps-constants
- '("OK" "ERR_NOT_OWNER" "ERR_NO_PATH" "ERR_NAME_EXISTS" "ERR_BUSY"
- "ERR_NOT_FOUND" "ERR_NOT_ENOUGH_ENERGY" "ERR_NOT_ENOUGH_RESOURCES"
- "ERR_INVALID_TARGET" "ERR_FULL" "ERR_NOT_IN_RANGE"
- "ERR_INVALID_ARGS" "ERR_TIRED" "ERR_NO_BODYPART"
- "ERR_NOT_ENOUGH_EXTENSIONS" "ERR_RCL_NOT_ENOUGH"
- "ERR_GCL_NOT_ENOUGH"
-
- "FIND_EXIT_TOP" "FIND_EXIT_RIGHT" "FIND_EXIT_BOTTOM"
- "FIND_EXIT_LEFT" "FIND_EXIT" "FIND_CREEPS" "FIND_MY_CREEPS"
- "FIND_HOSTILE_CREEPS" "FIND_SOURCES_ACTIVE" "FIND_SOURCES"
- "FIND_DROPPED_ENERGY" "FIND_DROPPED_RESOURCES" "FIND_STRUCTURES"
- "FIND_MY_STRUCTURES" "FIND_HOSTILE_STRUCTURES" "FIND_FLAGS"
- "FIND_CONSTRUCTION_SITES" "FIND_MY_SPAWNS" "FIND_HOSTILE_SPAWNS"
- "FIND_MY_CONSTRUCTION_SITES" "FIND_HOSTILE_CONSTRUCTION_SITES"
- "FIND_MINERALS" "FIND_NUKES"
-
- "TOP" "TOP_RIGHT" "RIGHT" "BOTTOM_RIGHT" "BOTTOM" "BOTTOM_LEFT"
- "LEFT" "TOP_LEFT"
-
- "COLOR_RED" "COLOR_PURPLE" "COLOR_BLUE" "COLOR_CYAN" "COLOR_GREEN"
- "COLOR_YELLOW" "COLOR_ORANGE" "COLOR_BROWN" "COLOR_GREY"
- "COLOR_WHITE"
-
- "LOOK_CREEPS" "LOOK_ENERGY" "LOOK_RESOURCES" "LOOK_SOURCES"
- "LOOK_MINERALS" "LOOK_STRUCTURES" "LOOK_FLAGS"
- "LOOK_CONSTRUCTION_SITES" "LOOK_NUKES" "LOOK_TERRAIN"
-
- "OBSTACLE_OBJECT_TYPES"
-
- "MOVE" "WORK" "CARRY" "ATTACK" "RANGED_ATTACK" "TOUGH" "HEAL"
- "CLAIM"
-
- "BODYPART_COST"
-
- "CREEP_LIFE_TIME" "CREEP_CLAIM_LIFE_TIME" "CREEP_CORPSE_RATE"
-
- "CARRY_CAPACITY" "HARVEST_POWER" "HARVEST_MINERAL_POWER"
- "REPAIR_POWER" "DISMANTLE_POWER" "BUILD_POWER" "ATTACK_POWER"
- "UPGRADE_CONTROLLER_POWER" "RANGED_ATTACK_POWER" "HEAL_POWER"
- "RANGED_HEAL_POWER" "REPAIR_COST" "DISMANTLE_COST"
-
- "RAMPART_DECAY_AMOUNT" "RAMPART_DECAY_TIME" "RAMPART_HITS"
- "RAMPART_HITS_MAX"
-
- "ENERGY_REGEN_TIME" "ENERGY_DECAY"
-
- "SPAWN_HITS" "SPAWN_ENERGY_START" "SPAWN_ENERGY_CAPACITY"
- "CREEP_SPAWN_TIME"
-
- "SOURCE_ENERGY_CAPACITY" "SOURCE_ENERGY_NEUTRAL_CAPACITY"
- "SOURCE_ENERGY_KEEPER_CAPACITY"
-
- "WALL_HITS" "WALL_HITS_MAX"
-
- "EXTENSION_HITS" "EXTENSION_ENERGY_CAPACITY"
-
- "ROAD_HITS" "ROAD_WEAROUT" "ROAD_DECAY_AMOUNT" "ROAD_DECAY_TIME"
-
- "LINK_HITS" "LINK_HITS_MAX" "LINK_CAPACITY" "LINK_COOLDOWN"
- "LINK_LOSS_RATIO"
-
- "STORAGE_CAPACITY" "STORAGE_HITS"
-
- "STRUCTURE_SPAWN" "STRUCTURE_EXTENSION" "STRUCTURE_ROAD"
- "STRUCTURE_WALL" "STRUCTURE_RAMPART" "STRUCTURE_KEEPER_LAIR"
- "STRUCTURE_PORTAL" "STRUCTURE_CONTROLLER" "STRUCTURE_LINK"
- "STRUCTURE_STORAGE" "STRUCTURE_TOWER" "STRUCTURE_OBSERVER"
- "STRUCTURE_POWER_BANK" "STRUCTURE_POWER_SPAWN" "STRUCTURE_EXTRACTOR"
- "STRUCTURE_LAB" "STRUCTURE_TERMINAL" "STRUCTURE_CONTAINER"
- "STRUCTURE_NUKER"
-
- "CONSTRUCTION_COST"
- "CONSTRUCTION_COST_ROAD_SWAMP_RATIO"
-
- "CONTROLLER_LEVELS" "CONTROLLER_STRUCTURES" "CONTROLLER_DOWNGRADE"
- "CONTROLLER_CLAIM_DOWNGRADE" "CONTROLLER_RESERVE"
- "CONTROLLER_RESERVE_MAX" "CONTROLLER_MAX_UPGRADE_PER_TICK"
- "CONTROLLER_ATTACK_BLOCKED_UPGRADE"
-
- "TOWER_HITS" "TOWER_CAPACITY" "TOWER_ENERGY_COST"
- "TOWER_POWER_ATTACK" "TOWER_POWER_HEAL" "TOWER_POWER_REPAIR"
- "TOWER_OPTIMAL_RANGE" "TOWER_FALLOFF_RANGE" "TOWER_FALLOFF"
-
- "OBSERVER_HITS" "OBSERVER_RANGE"
-
- "POWER_BANK_HITS" "POWER_BANK_CAPACITY_MAX"
- "POWER_BANK_CAPACITY_MIN" "POWER_BANK_CAPACITY_CRIT"
- "POWER_BANK_DECAY" "POWER_BANK_HIT_BACK"
-
- "POWER_SPAWN_HITS" "POWER_SPAWN_ENERGY_CAPACITY"
- "POWER_SPAWN_POWER_CAPACITY" "POWER_SPAWN_ENERGY_RATIO"
-
- "EXTRACTOR_HITS"
-
- "LAB_HITS" "LAB_MINERAL_CAPACITY"
- "LAB_ENERGY_CAPACITY" "LAB_BOOST_ENERGY" "LAB_BOOST_MINERAL"
- "LAB_COOLDOWN"
-
- "GCL_POW" "GCL_MULTIPLY" "GCL_NOVICE"
-
- "MODE_SIMULATION" "MODE_SURVIVAL" "MODE_WORLD" "MODE_ARENA"
-
- "TERRAIN_MASK_WALL" "TERRAIN_MASK_SWAMP" "TERRAIN_MASK_LAVA"
-
- "MAX_CONSTRUCTION_SITES" "MAX_CREEP_SIZE"
-
- "MINERAL_REGEN_TIME" "MINERAL_MIN_AMOUNT" "MINERAL_RANDOM_FACTOR"
-
- "TERMINAL_CAPACITY" "TERMINAL_HITS" "TERMINAL_SEND_COST"
- "TERMINAL_MIN_SEND"
-
- "CONTAINER_HITS" "CONTAINER_CAPACITY" "CONTAINER_DECAY"
- "CONTAINER_DECAY_TIME" "CONTAINER_DECAY_TIME_OWNED"
-
- "NUKER_HITS" "NUKER_COOLDOWN" "NUKER_ENERGY_CAPACITY"
- "NUKER_GHODIUM_CAPACITY" "NUKE_LAND_TIME" "NUKE_RANGE" "NUKE_DAMAGE"
-
- "RESOURCE_ENERGY" "RESOURCE_POWER"
-
- "RESOURCE_HYDROGEN" "RESOURCE_OXYGEN" "RESOURCE_UTRIUM"
- "RESOURCE_LEMERGIUM" "RESOURCE_KEANIUM" "RESOURCE_ZYNTHIUM"
- "RESOURCE_CATALYST" "RESOURCE_GHODIUM"
-
- "RESOURCE_HYDROXIDE" "RESOURCE_ZYNTHIUM_KEANITE"
- "RESOURCE_UTRIUM_LEMERGITE"
-
- "RESOURCE_UTRIUM_HYDRIDE" "RESOURCE_UTRIUM_OXIDE"
- "RESOURCE_KEANIUM_HYDRIDE" "RESOURCE_KEANIUM_OXIDE"
- "RESOURCE_LEMERGIUM_HYDRIDE" "RESOURCE_LEMERGIUM_OXIDE"
- "RESOURCE_ZYNTHIUM_HYDRIDE" "RESOURCE_ZYNTHIUM_OXIDE"
- "RESOURCE_GHODIUM_HYDRIDE" "RESOURCE_GHODIUM_OXIDE"
-
- "RESOURCE_UTRIUM_ACID" "RESOURCE_UTRIUM_ALKALIDE"
- "RESOURCE_KEANIUM_ACID" "RESOURCE_KEANIUM_ALKALIDE"
- "RESOURCE_LEMERGIUM_ACID" "RESOURCE_LEMERGIUM_ALKALIDE"
- "RESOURCE_ZYNTHIUM_ACID" "RESOURCE_ZYNTHIUM_ALKALIDE"
- "RESOURCE_GHODIUM_ACID" "RESOURCE_GHODIUM_ALKALIDE"
-
- "RESOURCE_CATALYZED_UTRIUM_ACID"
- "RESOURCE_CATALYZED_UTRIUM_ALKALIDE"
- "RESOURCE_CATALYZED_KEANIUM_ACID"
- "RESOURCE_CATALYZED_KEANIUM_ALKALIDE"
- "RESOURCE_CATALYZED_LEMERGIUM_ACID"
- "RESOURCE_CATALYZED_LEMERGIUM_ALKALIDE"
- "RESOURCE_CATALYZED_ZYNTHIUM_ACID"
- "RESOURCE_CATALYZED_ZYNTHIUM_ALKALIDE"
- "RESOURCE_CATALYZED_GHODIUM_ACID"
- "RESOURCE_CATALYZED_GHODIUM_ALKALIDE"
-
- "REACTIONS" "BODYPARTS_ALL" "RESOURCES_ALL" "COLORS_ALL"))
-
-(defun +javascript|init-screeps-mode ()
- (when (eq major-mode 'js2-mode)
- (push 'javascript-jshint flycheck-disabled-checkers)
- (setq js2-additional-externs (append '("_") screeps-objects screeps-constants))))
-
diff --git a/modules/lang/javascript/README.org b/modules/lang/javascript/README.org
index dfc55ebbe..442514a84 100644
--- a/modules/lang/javascript/README.org
+++ b/modules/lang/javascript/README.org
@@ -2,7 +2,7 @@
This module adds Javascript support.
-+ Code completion (tern)
++ Code completion (tide)
+ REPL support (nodejs-repl)
+ Refactoring commands (js2-refactor)
+ Syntax checking (flycheck)
@@ -13,7 +13,6 @@ This module adds Javascript support.
* Table of Contents :TOC:
- [[#install][Install]]
- [[#node--npm][Node & NPM]]
- - [[#dependencies][Dependencies]]
- [[#appendix][Appendix]]
- [[#commands][Commands]]
@@ -31,16 +30,78 @@ brew install node
sudo pacman --needed --noconfirm -S nodejs npm
#+END_SRC
-** Dependencies
-This module optionally requires ~tern~ for code completion.
-
-#+BEGIN_SRC sh
-npm -g install tern
-#+END_SRC
-
* Appendix
** Commands
+*** JS2-mode
| command | key / ex command | description |
|----------------------------------+------------------+------------------------------------------------------------|
-| ~+javascript/repl~ | =:repl= | Open the NodeJS REPL (or send the current selection to it) |
+| ~+javascript/open-repl~ | =:repl= | Open the NodeJS REPL (or send the current selection to it) |
| ~+javascript/skewer-this-buffer~ | =SPC m S= | Attaches a browser to the current buffer |
+*** Tide
+| command | key / ex command | description |
+|-------------------------+------------------+------------------------|
+| ~tide-restart-server~ | =SPC m R= | Restart tide server |
+| ~tide-reformat~ | =SPC m f= | Reformat region |
+| ~tide-rename-symbol~ | =SPC m r s= | Rename symbol at point |
+| ~tide-organize-imports~ | =SPC m r o i= | Organize imports |
+*** Refactoring (js2-refactor-mode)
+| command | key / ex command | description |
+|---------------------------------------------------+------------------+--------------------------------------------------------------------------------------------------------------------|
+| ~js2r-expand-node-at-point~ | =SPC m r e e= | Expand bracketed list according to node type at point |
+| ~js2r-contract-node-at-point~ | =SPC m r c c= | Contract bracketed list according to node type at point |
+| ~js2r-extract-function~ | =SPC m r e f= | Extracts the marked expressions out into a new named function. |
+| ~js2r-extract-method~ | =SPC m r e m= | Extracts the marked expressions out into a new named method in an object literal. |
+| ~js2r-toggle-function-expression-and-declaration~ | =SPC m r t f= | Toggle between function name() {} and var name = function (); |
+| ~js2r-toggle-arrow-function-and-expression~ | =SPC m r t a= | Toggle between function expression to arrow function. |
+| ~js2r-toggle-function-async~ | =SPC m r t s= | Toggle between an async and a regular function. |
+| ~js2r-introduce-parameter~ | =SPC m r i p= | Changes the marked expression to a parameter in a local function. |
+| ~js2r-localize-parameter~ | =SPC m r l p= | Changes a parameter to a local var in a local function. |
+| ~js2r-wrap-buffer-in-iife~ | =SPC m r w i= | Wraps the entire buffer in an immediately invoked function expression |
+| ~js2r-inject-global-in-iife~ | =SPC m r i g= | Creates a shortcut for a marked global by injecting it in the wrapping immediately invoked function expression |
+| ~js2r-add-to-globals-annotation~ | =SPC m r a g= | Creates a /*global */ annotation if it is missing, and adds the var at point to it. |
+| ~js2r-extract-var~ | =SPC m r e v= | Takes a marked expression and replaces it with a var. |
+| ~js2r-extract-let~ | =SPC m r e l= | Similar to extract-var but uses a let-statement. |
+| ~js2r-extract-const~ | =SPC m r e c= | Similar to extract-var but uses a const-statement. |
+| ~js2r-inline-var~ | =SPC m r i v= | Replaces all instances of a variable with its initial value. |
+| ~js2r-rename-var~ | =SPC m r r v= | Renames the variable on point and all occurrences in its lexical scope. |
+| ~js2r-var-to-this~ | =SPC m r v t= | Changes local var a to be this.a instead. |
+| ~js2r-arguments-to-object~ | =SPC m r a o= | Replaces arguments to a function call with an object literal of named arguments. |
+| ~js2r-ternary-to-if~ | =SPC m r 3 i= | Converts ternary operator to if-statement. |
+| ~js2r-split-var-declaration~ | =SPC m r s v= | Splits a var with multiple vars declared, into several var statements. |
+| ~js2r-split-string~ | =SPC m r s s= | Splits a string. |
+| ~js2r-string-to-template~ | =SPC m r s t= | Converts a string into a template string. |
+| ~js2r-unwrap~ | =SPC m r u w= | Replaces the parent statement with the selected region. |
+| ~js2r-log-this~ | =SPC m r l t= | Adds a console.log() statement for what is at point (or region). With a prefix argument, use JSON pretty-printing. |
+| ~js2r-debug-this~ | =SPC m r d t= | Adds a debug() statement for what is at point (or region). |
+| ~js2r-forward-slurp~ | =SPC m r s l= | Moves the next statement into current function, if-statement, for-loop or while-loop. |
+| ~js2r-forward-barf~ | =SPC m r b a= | Moves the last child out of current function, if-statement, for-loop or while-loop. |
+| ~js2r-kill~ | =SPC m r k= | Kills to the end of the line, but does not cross semantic boundaries. |
+*** skewer-mode
+**** general
+| command | key / ex command | description |
+|-------------------------------+------------------+---------------------------------------|
+| ~skewer-eval-last-expression~ | =SPC m s E= | Evaluate last expression |
+| ~skewer-eval-defun~ | =SPC m s e= | Evaluate function definition at point |
+| ~skewer-load-buffer~ | =SPC m s f= | Load buffer into REPL |
+**** css
+| command | key / ex command | description |
+|---------------------------------------+------------------+-------------------------------|
+| ~skewer-css-eval-current-declaration~ | =SPC m s e= | Evaluate declaration at point |
+| ~skewer-css-eval-current-rule~ | =SPC m s r= | Evaluate rule at point |
+| ~skewer-css-eval-buffer~ | =SPC m s b= | Evaluate buffer |
+| ~skewer-css-clear-all~ | =SPC m s c= | Clear all rules |
+**** html
+| command | key / ex command | description |
+|------------------------+------------------+-----------------------|
+| ~skewer-html-eval-tag~ | =SPC m s e= | Evaluate tag at point |
+*** npm-mode
+| command | key / ex command | description |
+|---------------------------------+------------------+------------------------------------------------------------------|
+| ~npm-mode-npm-init~ | =SPC m n n= | Initialize npm project |
+| ~npm-mode-npm-install~ | =SPC m n i= | Install npm package |
+| ~npm-mode-npm-install-save~ | =SPC m n s= | Install npm package and save to package.json |
+| ~npm-mode-npm-install-save-dev~ | =SPC m n d= | Install npm package and save to package.json as a dev dependency |
+| ~npm-mode-npm-uninstall~ | =SPC m n u= | Uninstall npm package |
+| ~npm-mode-npm-list~ | =SPC m n l= | List npm packages |
+| ~npm-mode-npm-run~ | =SPC m n r= | Run npm task |
+| ~npm-mode-visit-project-file~ | =SPC m n v= | Find file in npm project |
diff --git a/modules/lang/javascript/autoload.el b/modules/lang/javascript/autoload.el
index 3df1ecd03..330aef52a 100644
--- a/modules/lang/javascript/autoload.el
+++ b/modules/lang/javascript/autoload.el
@@ -11,6 +11,7 @@ ignore the cache."
(gethash project-root +javascript-npm-conf))
(let ((package-file (expand-file-name "package.json" project-root)))
(when-let* ((json (and (file-exists-p package-file)
+ (require 'json)
(json-read-file package-file))))
(puthash project-root json +javascript-npm-conf))))))
@@ -30,16 +31,20 @@ ignore the cache."
(assq packages deps))
(t (error "Expected a package symbol or list, got %s" packages))))))
+
+;;
+;; Commands
+
;;;###autoload
-(defun +javascript/repl ()
+(defun +javascript/open-repl ()
"Open a Javascript REPL. Meaning either `skewer-repl', if any of the
skewer-*-mode's are enabled, or `nodejs-repl' otherwise."
(interactive)
(call-interactively
(if (and (featurep 'skewer-mode)
(or skewer-mode skewer-css-mode skewer-html-mode))
- 'skewer-repl
- 'nodejs-repl)))
+ #'skewer-repl
+ #'nodejs-repl)))
;;;###autoload
(defun +javascript/skewer-this-buffer ()
@@ -73,3 +78,44 @@ Run this for any buffer you want to skewer."
(if skewer-css-mode (skewer-css-mode -1))
(if skewer-html-mode (skewer-html-mode -1)))))
+
+;;
+;; Hooks
+
+;;;###autoload
+(defun +javascript|add-node-modules-path ()
+ "Add current project's `node_modules/.bin` to `exec-path', so js tools
+prioritize project-local packages over global ones."
+ (make-local-variable 'exec-path)
+ (cl-pushnew (expand-file-name "node_modules/.bin/"
+ (or (locate-dominating-file
+ (or (buffer-file-name) default-directory)
+ "node_modules")
+ (doom-project-root)))
+ exec-path :test #'string=))
+
+;;;###autoload
+(defun +javascript|cleanup-tide-processes ()
+ "Clean up dangling tsserver processes if there are no more buffers with
+`tide-mode' active that belong to that server's project."
+ (when tide-mode
+ (unless (cl-loop with project-name = (tide-project-name)
+ for buf in (delq (current-buffer) (buffer-list))
+ if (and (buffer-local-value 'tide-mode buf)
+ (with-current-buffer buf
+ (string= (tide-project-name) project-name)))
+ return buf)
+ (kill-process (tide-current-server)))))
+
+
+;;
+;; Advice
+
+;;;###autoload
+(defun +javascript*tide-project-root ()
+ "Resolve to `doom-project-root' if `tide-project-root' fails."
+ (or tide-project-root
+ (or (locate-dominating-file default-directory "tsconfig.json")
+ (locate-dominating-file default-directory "jsconfig.json"))
+ (or (doom-project-root)
+ default-directory)))
diff --git a/modules/lang/javascript/config.el b/modules/lang/javascript/config.el
index 772976ae9..dd2e2fa31 100644
--- a/modules/lang/javascript/config.el
+++ b/modules/lang/javascript/config.el
@@ -1,209 +1,250 @@
;;; lang/javascript/config.el -*- lexical-binding: t; -*-
+(after! (:any js2-mode rjsx-mode web-mode)
+ (set-docsets! '(js2-mode rjsx-mode) "JavaScript"
+ "AngularJS" "Backbone" "BackboneJS" "Bootstrap" "D3JS" "EmberJS" "Express"
+ "ExtJS" "JQuery" "JQuery_Mobile" "JQuery_UI" "KnockoutJS" "Lo-Dash"
+ "MarionetteJS" "MomentJS" "NodeJS" "PrototypeJS" "React" "RequireJS"
+ "SailsJS" "UnderscoreJS" "VueJS" "ZeptoJS")
+
+ (set-pretty-symbols! '(js2-mode rjsx-mode web-mode)
+ ;; Functional
+ :def "function"
+ :lambda "() =>"
+ :composition "compose"
+ ;; Types
+ :null "null"
+ :true "true" :false "false"
+ ;; Flow
+ :not "!"
+ :and "&&" :or "||"
+ :for "for"
+ :return "return"
+ ;; Other
+ :yield "import"))
+
+(after! projectile
+ (pushnew! projectile-project-root-files "package.json")
+ (pushnew! projectile-globally-ignored-directories "node_modules" "flow-typed"))
+
+
+;;
+;; Major modes
+
(def-package! js2-mode
- :mode "\\.js$"
+ :mode "\\.m?js\\'"
:interpreter "node"
+ :commands js2-line-break
:config
(setq js2-skip-preprocessor-directives t
- js2-highlight-external-variables nil
- js2-mode-show-parse-errors nil)
+ js-chain-indent t
+ ;; let flycheck handle this
+ js2-mode-show-parse-errors nil
+ js2-mode-show-strict-warnings nil
+ ;; Flycheck provides these features, so disable them: conflicting with
+ ;; the eslint settings.
+ js2-strict-trailing-comma-warning nil
+ js2-strict-missing-semi-warning nil
+ ;; maximum fontification
+ js2-highlight-level 3
+ js2-highlight-external-variables t)
- (add-hook! 'js2-mode-hook
- #'(flycheck-mode highlight-indentation-mode rainbow-delimiters-mode))
+ (add-hook 'js2-mode-hook #'rainbow-delimiters-mode)
+ ;; Indent switch-case another step
+ (setq-hook! 'js2-mode-hook
+ js-switch-indent-offset js2-basic-offset
+ mode-name "JS2")
- (set! :repl 'js2-mode #'+javascript/repl)
- (set! :electric 'js2-mode :chars '(?\} ?\) ?.))
- (set! :jump 'js2-mode :xref-backend #'xref-js2-xref-backend)
-
- ;; Conform switch-case indentation to js2 normal indent
- (defvaralias 'js-switch-indent-offset 'js2-basic-offset)
-
- (sp-with-modes '(js2-mode rjsx-mode)
- (sp-local-pair "/* " " */" :post-handlers '(("| " "SPC"))))
-
- ;; If it's available globally, use eslint_d
- (setq flycheck-javascript-eslint-executable (executable-find "eslint_d"))
-
- (defun +javascript|init-flycheck-eslint ()
- "Favor local eslint over global installs and configure flycheck for eslint."
- (when (derived-mode-p 'js-mode)
- (when-let* ((exec-path (list (doom-project-expand "node_modules/.bin")))
- (eslint (executable-find "eslint")))
- (setq-local flycheck-javascript-eslint-executable eslint))
- (when (flycheck-find-checker-executable 'javascript-eslint)
- ;; Flycheck has it's own trailing command and semicolon warning that was
- ;; conflicting with the eslint settings.
- (setq-local js2-strict-trailing-comma-warning nil)
- (setq-local js2-strict-missing-semi-warning nil))))
- (add-hook 'flycheck-mode-hook #'+javascript|init-flycheck-eslint)
+ (set-electric! 'js2-mode :chars '(?\} ?\) ?. ?:))
+ (set-repl-handler! 'js2-mode #'+javascript/open-repl)
(map! :map js2-mode-map
:localleader
- "r" #'+javascript/refactor-menu
"S" #'+javascript/skewer-this-buffer))
-;; 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! nodejs-repl :commands nodejs-repl)
-
-
-(def-package! js2-refactor
- :commands
- (js2r-extract-function js2r-extract-method js2r-introduce-parameter
- js2r-localize-parameter js2r-expand-object js2r-contract-object
- js2r-expand-function js2r-contract-function js2r-expand-array
- js2r-contract-array js2r-wrap-buffer-in-iife js2r-inject-global-in-iife
- js2r-add-to-globals-annotation js2r-extract-var js2r-inline-var
- js2r-rename-var js2r-var-to-this js2r-arguments-to-object js2r-ternary-to-if
- js2r-split-var-declaration js2r-split-string js2r-unwrap js2r-log-this
- js2r-debug-this js2r-forward-slurp js2r-forward-barf)
- :init
- (def-menu! +javascript/refactor-menu
- "Refactoring commands for `js2-mode' buffers."
- '(("Extract into function" :exec js2r-extract-function :region t)
- ("Extract into method" :exec js2r-extract-method :region t)
- ("Introduce parameter to function" :exec js2r-introduce-parameter :region t)
- ("Localize parameter" :exec js2r-localize-parameter :region nil)
- ("Expand object" :exec js2r-expand-object :region nil)
- ("Expand function" :exec js2r-expand-function :region nil)
- ("Expand array" :exec js2r-expand-array :region nil)
- ("Contract object" :exec js2r-contract-object :region nil)
- ("Contract function" :exec js2r-contract-function :region nil)
- ("Contract array" :exec js2r-contract-array :region nil)
- ("Wrap buffer in IIFE" :exec js2r-wrap-buffer-in-iife :region nil)
- ("Inject global into IIFE" :exec js2r-inject-global-in-iife :region t)
- ("Add to globals annotation" :exec js2r-add-to-globals-annotation :region nil)
- ("Extract variable" :exec js2r-extract-var :region t)
- ("Inline variable" :exec js2r-inline-var :region t)
- ("Rename variable" :exec js2r-rename-var :region nil)
- ("Replace var with this" :exec js2r-var-to-this :region nil)
- ("Arguments to object" :exec js2r-arguments-to-object :region nil)
- ("Ternary to if" :exec js2r-ternary-to-if :region nil)
- ("Split var declaration" :exec js2r-split-var-declaration :region nil)
- ("Split string" :exec js2r-split-string :region nil)
- ("Unwrap" :exec js2r-unwrap :region t)
- ("Log this" :exec js2r-log-this)
- ("Debug this" :exec js2r-debug-this)
- ("Reformat buffer (eslint_d)" :exec eslintd-fix :region nil :when (fboundp 'eslintd-fix)))
- :prompt "Refactor: "))
-
-
-(def-package! tern
- :hook (js2-mode . tern-mode)
- :config
- (advice-add #'tern-project-dir :override #'doom-project-root))
-
-
-(def-package! company-tern
- :when (featurep! :completion company)
- :after tern
- :config
- (set! :company-backend 'js2-mode '(company-tern)))
-
-
(def-package! rjsx-mode
- :commands rjsx-mode
- :mode "\\.jsx$"
:mode "components/.+\\.js$"
:init
(defun +javascript-jsx-file-p ()
+ "Detect React or preact imports early in the file."
(and buffer-file-name
- (equal (file-name-extension buffer-file-name) "js")
- (re-search-forward "\\(^\\s-*import React\\|\\( from \\|require(\\)[\"']react\\)"
+ (string= (file-name-extension buffer-file-name) "js")
+ (re-search-forward "\\(^\\s-*import +React\\|\\( from \\|require(\\)[\"']p?react\\)"
magic-mode-regexp-match-limit t)
(progn (goto-char (match-beginning 1))
(not (sp-point-in-string-or-comment)))))
-
- (push (cons #'+javascript-jsx-file-p 'rjsx-mode) magic-mode-alist)
-
+ (add-to-list 'magic-mode-alist '(+javascript-jsx-file-p . rjsx-mode))
:config
- (set! :electric 'rjsx-mode :chars '(?\} ?\) ?. ?>))
+ (set-electric! 'rjsx-mode :chars '(?\} ?\) ?. ?>))
+ (when (featurep! :tools flycheck)
+ (add-hook! 'rjsx-mode-hook
+ ;; jshint doesn't know how to deal with jsx
+ (push 'javascript-jshint flycheck-disabled-checkers)))
- ;; disable electric keys (I use snippets and `emmet-mode' instead)
- (map! :map rjsx-mode-map
- "<" nil
- "C-d" nil)
- (add-hook! rjsx-mode
- ;; jshint doesn't really know how to deal with jsx
- (push 'javascript-jshint flycheck-disabled-checkers)))
+ ;; `rjsx-electric-gt' relies on js2's parser to tell it when the cursor is in
+ ;; a self-closing tag, so that it can insert a matching ending tag at point.
+ ;; However, the parser doesn't run immediately, so a fast typist can outrun
+ ;; it, causing tags to stay unclosed, so we force it to parse.
+ (defun +javascript|reparse (n)
+ ;; if n != 1, rjsx-electric-gt calls rjsx-maybe-reparse itself
+ (if (= n 1) (rjsx-maybe-reparse)))
+ (advice-add #'rjsx-electric-gt :before #'+javascript|reparse))
-(def-package! coffee-mode
- :mode "\\.coffee$"
- :init (setq coffee-indent-like-python-mode t))
+(after! typescript-mode
+ (add-hook 'typescript-mode-hook #'rainbow-delimiters-mode)
+ (setq-hook! 'typescript-mode-hook
+ comment-line-break-function #'js2-line-break)
+ (set-electric! 'typescript-mode
+ :chars '(?\} ?\)) :words '("||" "&&"))
+ (set-docsets! 'typescript-mode "TypeScript" "AngularTS")
+ (set-pretty-symbols! 'typescript-mode
+ ;; Functional
+ :def "function"
+ :lambda "() =>"
+ :composition "compose"
+ ;; Types
+ :null "null"
+ :true "true" :false "false"
+ :int "number"
+ :str "string"
+ :bool "boolean"
+ ;; Flow
+ :not "!"
+ :and "&&" :or "||"
+ :for "for"
+ :return "return" :yield "import"))
-(def-package! web-beautify
- :commands web-beautify-js
- :init
- (map! :map* (json-mode js2-mode-map) :n "gQ" #'web-beautify-js))
-
-
-(def-package! eslintd-fix
- :commands (eslintd-fix-mode eslintd-fix))
-
-
-;;
-;; Skewer-mode
-;;
-
-(def-package! skewer-mode
- :commands (skewer-mode run-skewer)
- :config
- (map! :map skewer-mode-map
- :localleader
- :n "sE" #'skewer-eval-last-expression
- :n "se" #'skewer-eval-defun
- :n "sf" #'skewer-load-buffer))
-
-(def-package! skewer-css ; in skewer-mode
- :commands skewer-css-mode
- :config
- (map! :map skewer-css-mode-map
- :localleader
- :n "se" #'skewer-css-eval-current-declaration
- :n "sr" #'skewer-css-eval-current-rule
- :n "sb" #'skewer-css-eval-buffer
- :n "sc" #'skewer-css-clear-all))
-
-(def-package! skewer-html ; in skewer-mode
- :commands skewer-html-mode
- :config
- (map! :map skewer-html-mode-map
- :localleader
- :n "se" #'skewer-html-eval-tag))
-
-
-;;
-;; Projects
-;;
-
-(def-project-mode! +javascript-screeps-mode
- :match "/screeps\\(-ai\\)?/.+$"
- :modes (+javascript-npm-mode)
- :add-hooks (+javascript|init-screeps-mode)
- :on-load (load! +screeps))
-
-(def-project-mode! +javascript-gulp-mode
- :files "gulpfile.js")
-
-(def-project-mode! +javascript-npm-mode
- :modes (html-mode css-mode web-mode js2-mode markdown-mode)
- :files "package.json"
- :on-enter
- (when (make-local-variable 'exec-path)
- (push (doom-project-expand "node_modules/.bin")
- exec-path)))
+;; `coffee-mode'
+(setq coffee-indent-like-python-mode t)
+(after! coffee-mode
+ (set-docsets! 'coffee-mode "CoffeeScript"))
;;
;; Tools
+
+(when (featurep! +lsp)
+ (add-hook! (js2-mode rjsx-mode typescript-mode) #'lsp!))
+
+
+(def-package! tide
+ :unless (featurep! +lsp)
+ :defer t
+ :init
+ ;; Don't let hard errors stop the user from opening js files.
+ (defun +javascript|init-tide ()
+ "Enable `tide-mode' if node is available."
+ (cond ((not buffer-file-name)
+ (add-hook 'after-save-hook #'+javascript|init-tide nil t))
+ ((executable-find "node")
+ (tide-setup))
+ ((message "Couldn't find `node', aborting tide server"))))
+ (add-hook! (js2-mode typescript-mode) #'+javascript|init-tide)
+
+ (defun +javascript|init-tide-in-web-mode ()
+ "Enable `tide-mode' if in a *.tsx file."
+ (when (string= (file-name-extension (or buffer-file-name "")) "tsx")
+ (tide-setup)))
+ (add-hook 'web-mode-hook #'+javascript|init-tide-in-web-mode)
+ :config
+ (setq tide-completion-detailed t
+ tide-always-show-documentation t)
+ ;; code completion
+ (after! company
+ ;; tide affects the global `company-backends', undo this so doom can handle
+ ;; it buffer-locally
+ (setq-default company-backends (delq 'company-tide (default-value 'company-backends))))
+ (set-company-backend! 'tide-mode 'company-tide)
+ ;; navigation
+ (set-lookup-handlers! 'tide-mode :async t
+ :definition #'tide-jump-to-definition
+ :references #'tide-references)
+ ;; resolve to `doom-project-root' if `tide-project-root' fails
+ (advice-add #'tide-project-root :override #'+javascript*tide-project-root)
+ ;; cleanup tsserver when no tide buffers are left
+ (add-hook! 'tide-mode-hook
+ (add-hook 'kill-buffer-hook #'+javascript|cleanup-tide-processes nil t))
+
+ (define-key tide-mode-map [remap +lookup/documentation] #'tide-documentation-at-point)
+
+ (map! :localleader
+ :map tide-mode-map
+ "R" #'tide-restart-server
+ "f" #'tide-format
+ "rs" #'tide-rename-symbol
+ "roi" #'tide-organize-imports))
+
+
+(def-package! xref-js2
+ :when (featurep! :feature lookup)
+ :after (:or js2-mode rjsx-mode)
+ :config
+ (set-lookup-handlers! '(js2-mode rjsx-mode)
+ :xref-backend #'xref-js2-xref-backend))
+
+
+(def-package! js2-refactor
+ :hook ((js2-mode rjsx-mode) . js2-refactor-mode)
+ :config
+ (when (featurep! :feature evil +everywhere)
+ (let ((js2-refactor-mode-map (evil-get-auxiliary-keymap js2-refactor-mode-map 'normal t t)))
+ (js2r-add-keybindings-with-prefix (format "%s r" doom-localleader-key)))))
+
+
+(def-package! eslintd-fix
+ :commands eslintd-fix
+ :config
+ (defun +javascript|set-flycheck-executable-to-eslint ()
+ (setq flycheck-javascript-eslint-executable eslintd-fix-executable))
+ (add-hook 'eslintd-fix-mode-hook #'+javascript|set-flycheck-executable-to-eslint))
+
+
+;; `skewer-mode'
+(map! :localleader
+ :prefix "s"
+ (:after skewer-mode
+ :map skewer-mode-map
+ "E" #'skewer-eval-last-expression
+ "e" #'skewer-eval-defun
+ "f" #'skewer-load-buffer)
+
+ (:after skewer-css
+ :map skewer-css-mode-map
+ "e" #'skewer-css-eval-current-declaration
+ "r" #'skewer-css-eval-current-rule
+ "b" #'skewer-css-eval-buffer
+ "c" #'skewer-css-clear-all)
+
+ (:after skewer-html
+ :map skewer-html-mode-map
+ "e" #'skewer-html-eval-tag))
+
+
+;; `npm-mode'
+(map! :after npm-mode
+ :localleader
+ :map npm-mode-keymap
+ :prefix "n"
+ "n" #'npm-mode-npm-init
+ "i" #'npm-mode-npm-install
+ "s" #'npm-mode-npm-install-save
+ "d" #'npm-mode-npm-install-save-dev
+ "u" #'npm-mode-npm-uninstall
+ "l" #'npm-mode-npm-list
+ "r" #'npm-mode-npm-run
+ "v" #'npm-mode-visit-project-file)
+
+
;;
+;; Projects
-(def-project-mode! +javascript-eslintd-fix-mode
- :add-hooks (eslintd-fix-mode))
+(def-project-mode! +javascript-npm-mode
+ :modes (html-mode css-mode web-mode typescript-mode js2-mode rjsx-mode json-mode markdown-mode)
+ :when (locate-dominating-file default-directory "package.json")
+ :add-hooks (+javascript|add-node-modules-path npm-mode))
+(def-project-mode! +javascript-gulp-mode
+ :when (locate-dominating-file default-directory "gulpfile.js"))
diff --git a/modules/lang/javascript/packages.el b/modules/lang/javascript/packages.el
index c89f3bd09..c7474537c 100644
--- a/modules/lang/javascript/packages.el
+++ b/modules/lang/javascript/packages.el
@@ -1,21 +1,21 @@
;; -*- no-byte-compile: t; -*-
;;; lang/javascript/packages.el
-;; requires node npm tern js-beautify eslint eslint-plugin-react
-
+;; major modes
(package! coffee-mode)
(package! js2-mode)
-(package! js2-refactor)
(package! rjsx-mode)
-(package! nodejs-repl)
-(package! tern)
-(package! web-beautify)
-(package! skewer-mode)
+(package! typescript-mode)
+
+;; tools
(package! eslintd-fix)
+(package! js2-refactor)
+(package! nodejs-repl)
+(package! npm-mode)
+(package! skewer-mode)
-(when (featurep! :completion company)
- (package! company-tern))
-
-(when (featurep! :feature jump)
+(when (featurep! :feature lookup)
(package! xref-js2))
+(unless (featurep! +lsp)
+ (package! tide))
diff --git a/modules/lang/julia/autoload.el b/modules/lang/julia/autoload.el
index e74ff2d3a..b999631ad 100644
--- a/modules/lang/julia/autoload.el
+++ b/modules/lang/julia/autoload.el
@@ -9,6 +9,5 @@
(apply #'make-comint-in-buffer "Julia" "*Julia*" julia-program julia-arguments))
(pop-to-buffer buffer)
(with-current-buffer buffer
- (inferior-julia-mode))))
-
-
+ (inferior-julia-mode))
+ buffer))
diff --git a/modules/lang/julia/config.el b/modules/lang/julia/config.el
index 2656fd4c8..385b49b7a 100644
--- a/modules/lang/julia/config.el
+++ b/modules/lang/julia/config.el
@@ -1,31 +1,30 @@
;;; lang/julia/config.el -*- lexical-binding: t; -*-
(use-package julia-mode
- :mode "\\.jl$"
:interpreter "julia"
:config
- (set! :repl 'julia-mode #'+julia/repl)
+ (set-repl-handler! 'julia-mode #'+julia/repl)
;; Borrow matlab.el's fontification of math operators
;; From
- (font-lock-add-keywords
- 'julia-mode
- `((,(let ((OR "\\|"))
- (concat "\\(" ;; stolen `matlab.el' operators first
- "[<>!]=?" OR
- "\\.[/*^']" OR
- "==" OR
- "=>" OR
- "\\" OR
- "[-+*\\/^&|$]=?" OR ;; this has to come before next (updating operators)
- "[-!^&|*+\\/~:]" OR
- ;; more extra julia operators follow
- "[%$]" OR
- ;; bitwise operators
- ">>>" OR ">>" OR "<<" OR
- ">>>=" OR ">>" OR "<<" OR
- ;; comparison
- "[<>!]=?" OR
- "\\)"))
- 1 font-lock-type-face))))
-
+ (dolist (mode '(julia-mode ess-julia-mode))
+ (font-lock-add-keywords
+ mode
+ `((,(let ((OR "\\|"))
+ (concat "\\(" ;; stolen `matlab.el' operators first
+ "[<>!]=?" OR
+ "\\.[/*^']" OR
+ "==" OR
+ "=>" OR
+ "\\" OR
+ "[-+*\\/^&|$]=?" OR ;; this has to come before next (updating operators)
+ "[-!^&|*+\\/~:]" OR
+ ;; more extra julia operators follow
+ "[%$]" OR
+ ;; bitwise operators
+ ">>>" OR ">>" OR "<<" OR
+ ">>>=" OR ">>" OR "<<" OR
+ ;; comparison
+ "[<>!]=?" OR
+ "\\)"))
+ 1 font-lock-type-face)))))
diff --git a/modules/lang/kotlin/autoload.el b/modules/lang/kotlin/autoload.el
new file mode 100644
index 000000000..2eee6cd90
--- /dev/null
+++ b/modules/lang/kotlin/autoload.el
@@ -0,0 +1,15 @@
+;;; lang/kotlin/autoload.el -*- lexical-binding: t; -*-
+
+;;;autoload
+(defun +kotlin-locate-gradlew-file ()
+ "Gradlew file location for this project."
+ (locate-dominating-file buffer-file-name "gradlew"))
+
+;;;###autoload
+(defun +kotlin/run-gradlew (command)
+ "Run gradlew in this project."
+ (interactive "sCommand: ")
+ (let ((default-directory (+kotlin-locate-gradlew-file))
+ (compilation-read-command nil)
+ (compile-command (format "sh gradlew %s" command)))
+ (call-interactively #'compile)))
diff --git a/modules/lang/kotlin/config.el b/modules/lang/kotlin/config.el
new file mode 100644
index 000000000..dc02f1035
--- /dev/null
+++ b/modules/lang/kotlin/config.el
@@ -0,0 +1,16 @@
+;;; lang/kotlin/config.el -*- lexical-binding: t; -*-
+
+(after! kotlin-mode
+ (set-docsets! 'kotlin-mode "Kotlin")
+
+ (map! :map kotlin-mode-map
+ :localleader
+ :prefix ("b" . "build")
+ :desc "gradlew assemble" "a" (λ! (+kotlin/run-gradlew "assemble"))
+ :desc "gradlew build" "b" (λ! (+kotlin/run-gradlew "build"))
+ :desc "gradlew test" "t" (λ! (+kotlin/run-gradlew "test"))))
+
+(def-package! flycheck-kotlin
+ :when (featurep! :tools flycheck)
+ :after kotlin-mode
+ :config (add-hook 'kotlin-mode-hook #'flycheck-kotlin-setup))
diff --git a/modules/lang/kotlin/doctor.el b/modules/lang/kotlin/doctor.el
new file mode 100644
index 000000000..63eabc017
--- /dev/null
+++ b/modules/lang/kotlin/doctor.el
@@ -0,0 +1,4 @@
+;;; lang/kotlin/doctor.el -*- lexical-binding: t; -*-
+
+(unless (executable-find "ktlint")
+ (warn! "ktlint not found. flycheck-kotlin won't work."))
diff --git a/modules/lang/kotlin/packages.el b/modules/lang/kotlin/packages.el
new file mode 100644
index 000000000..f7c3361dc
--- /dev/null
+++ b/modules/lang/kotlin/packages.el
@@ -0,0 +1,7 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/kotlin/packages.el
+
+(package! kotlin-mode)
+
+(when (featurep! :tools flycheck)
+ (package! flycheck-kotlin))
diff --git a/modules/lang/latex/+fontification.el b/modules/lang/latex/+fontification.el
new file mode 100644
index 000000000..dfa92377d
--- /dev/null
+++ b/modules/lang/latex/+fontification.el
@@ -0,0 +1,88 @@
+;;; lang/latex/+fontification.el -*- lexical-binding: t; -*-
+
+;; Fontification taken from https://tex.stackexchange.com/a/86119/81279
+(setq font-latex-match-reference-keywords
+ '(;; biblatex
+ ("printbibliography" "[{")
+ ("addbibresource" "[{")
+ ;; Standard commands
+ ("cite" "[{")
+ ("citep" "[{")
+ ("citet" "[{")
+ ("Cite" "[{")
+ ("parencite" "[{")
+ ("Parencite" "[{")
+ ("footcite" "[{")
+ ("footcitetext" "[{")
+ ;; Style-specific commands
+ ("textcite" "[{")
+ ("Textcite" "[{")
+ ("smartcite" "[{")
+ ("Smartcite" "[{")
+ ("cite*" "[{")
+ ("parencite*" "[{")
+ ("supercite" "[{")
+ ;; Qualified citation lists
+ ("cites" "[{")
+ ("Cites" "[{")
+ ("parencites" "[{")
+ ("Parencites" "[{")
+ ("footcites" "[{")
+ ("footcitetexts" "[{")
+ ("smartcites" "[{")
+ ("Smartcites" "[{")
+ ("textcites" "[{")
+ ("Textcites" "[{")
+ ("supercites" "[{")
+ ;; Style-independent commands
+ ("autocite" "[{")
+ ("Autocite" "[{")
+ ("autocite*" "[{")
+ ("Autocite*" "[{")
+ ("autocites" "[{")
+ ("Autocites" "[{")
+ ;; Text commands
+ ("citeauthor" "[{")
+ ("Citeauthor" "[{")
+ ("citetitle" "[{")
+ ("citetitle*" "[{")
+ ("citeyear" "[{")
+ ("citedate" "[{")
+ ("citeurl" "[{")
+ ;; Special commands
+ ("fullcite" "[{")
+ ;; cleveref
+ ("cref" "{")
+ ("Cref" "{")
+ ("cpageref" "{")
+ ("Cpageref" "{")
+ ("cpagerefrange" "{")
+ ("Cpagerefrange" "{")
+ ("crefrange" "{")
+ ("Crefrange" "{")
+ ("labelcref" "{")))
+
+(setq font-latex-match-textual-keywords
+ '(;; biblatex brackets
+ ("parentext" "{")
+ ("brackettext" "{")
+ ("hybridblockquote" "[{")
+ ;; Auxiliary Commands
+ ("textelp" "{")
+ ("textelp*" "{")
+ ("textins" "{")
+ ("textins*" "{")
+ ;; subcaption
+ ("subcaption" "[{")))
+
+(setq font-latex-match-variable-keywords
+ '(;; amsmath
+ ("numberwithin" "{")
+ ;; enumitem
+ ("setlist" "[{")
+ ("setlist*" "[{")
+ ("newlist" "{")
+ ("renewlist" "{")
+ ("setlistdepth" "{")
+ ("restartlist" "{")
+ ("crefname" "{")))
diff --git a/modules/lang/latex/+ref.el b/modules/lang/latex/+ref.el
new file mode 100644
index 000000000..321337ef7
--- /dev/null
+++ b/modules/lang/latex/+ref.el
@@ -0,0 +1,42 @@
+;;; lang/latex/+ref.el -*- lexical-binding: t; -*-
+
+(when (stringp +latex-bibtex-file)
+ (setq bibtex-completion-bibliography (list (expand-file-name +latex-bibtex-file))
+ reftex-default-bibliography bibtex-completion-bibliography))
+
+
+(def-package! reftex
+ :hook (LaTeX-mode . reftex-mode)
+ :config
+ ;; set up completion for citations and references
+ (set-company-backend! 'reftex-mode 'company-reftex-labels 'company-reftex-citations)
+ ;; Get ReTeX working with biblatex
+ ;; http://tex.stackexchange.com/questions/31966/setting-up-reftex-with-biblatex-citation-commands/31992#31992
+ (setq reftex-cite-format
+ '((?a . "\\autocite[]{%l}")
+ (?b . "\\blockcquote[]{%l}{}")
+ (?c . "\\cite[]{%l}")
+ (?f . "\\footcite[]{%l}")
+ (?n . "\\nocite{%l}")
+ (?p . "\\parencite[]{%l}")
+ (?s . "\\smartcite[]{%l}")
+ (?t . "\\textcite[]{%l}"))
+ reftex-plug-into-AUCTeX t
+ reftex-toc-split-windows-fraction 0.3)
+ (map! :map reftex-mode-map
+ :localleader
+ ";" 'reftex-toc)
+ (add-hook! 'reftex-toc-mode-hook
+ (reftex-toc-rescan)
+ (map! :map 'local
+ :e "j" #'next-line
+ :e "k" #'previous-line
+ :e "q" #'kill-buffer-and-window
+ :e "ESC" #'kill-buffer-and-window)))
+
+;; set up mode for bib files
+(after! bibtex
+ (setq bibtex-dialect 'biblatex
+ bibtex-align-at-equal-sign t
+ bibtex-text-indentation 20)
+ (define-key bibtex-mode-map (kbd "C-c \\") #'bibtex-fill-entry))
diff --git a/modules/lang/latex/+viewers.el b/modules/lang/latex/+viewers.el
new file mode 100644
index 000000000..89251647f
--- /dev/null
+++ b/modules/lang/latex/+viewers.el
@@ -0,0 +1,49 @@
+;;; lang/latex/+viewers.el -*- lexical-binding: t; -*-
+
+(catch 'found-viewer
+ (dolist (viewer +latex-viewers)
+ (if (pcase viewer
+ (`skim
+ (when (and IS-MAC
+ (file-exists-p! (or "/Applications/Skim.app"
+ "~/Applications/Skim.app")))
+ (add-to-list 'TeX-view-program-selection '(output-pdf "Skim"))))
+ (`sumatrapdf
+ (when (and IS-WINDOWS
+ (executable-find "SumatraPDF"))
+ (add-to-list 'TeX-view-program-selection '(output-pdf "SumatraPDF"))))
+
+ (`okular
+ (when (executable-find "okular")
+ ;; Configure Okular as viewer. Including a bug fix
+ ;; (https://bugs.kde.org/show_bug.cgi?id=373855)
+ (add-to-list 'TeX-view-program-list '("Okular" ("okular --unique file:%o" (mode-io-correlate "#src:%n%a"))))
+ (add-to-list 'TeX-view-program-selection '(output-pdf "Okular"))))
+
+ (`zathura
+ (when (executable-find "zathura")
+ (add-to-list 'TeX-view-program-selection '(output-pdf "Zathura"))))
+
+ (`pdf-tools
+ (when (featurep! :tools pdf)
+ (add-to-list 'TeX-view-program-selection '(output-pdf "PDF Tools"))
+ (when IS-MAC
+ ;; PDF Tools isn't in `TeX-view-program-list-builtin' on macs
+ (add-to-list 'TeX-view-program-list '("PDF Tools" TeX-pdf-tools-sync-view)))
+ ;; Update PDF buffers after successful LaTeX runs
+ (add-hook 'TeX-after-compilation-finished-function #'TeX-revert-document-buffer))))
+
+ (throw 'found-viewer t)))
+
+ ;; fall back to latex-preview-pane
+ (add-to-list 'TeX-view-program-list '("preview-pane" latex-preview-pane-mode))
+ (add-to-list 'TeX-view-program-selection '(output-pdf "preview-pane")))
+
+
+(after! latex-preview-pane
+ (setq latex-preview-pane-multifile-mode 'auctex)
+
+ (define-key! doc-view-mode-map
+ "ESC" #'delete-window
+ "q" #'delete-window
+ "k" (λ! (quit-window) (delete-window))))
diff --git a/modules/lang/latex/README.org b/modules/lang/latex/README.org
new file mode 100644
index 000000000..202c69c11
--- /dev/null
+++ b/modules/lang/latex/README.org
@@ -0,0 +1,73 @@
+#+TITLE: lang/latex
+#+DATE: January 16, 2017
+#+SINCE: v1.3
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC_3:noexport:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Features][Features]]
+- [[Customization][Customization]]
+ - [[Specifying the location of a bibtex file & corresponding PDFs][Specifying the location of a bibtex file & corresponding PDFs]]
+ - [[Changing the PDFs viewer][Changing the PDFs viewer]]
+
+* Description
+Provide a helping hand when working with LaTeX documents.
+
++ Sane defaults
++ Fontification of many popular commands
++ Pretty indentation of wrapped lines using the ~adaptive-wrap~ package
++ Spell checking with ~flycheck~
++ Change PDF viewer to Okular or ~latex-preview-pane~
++ Bibtex editor
++ Autocompletion using ~company-mode~
++ Ivy or Helm for selecting bibliography
++ Compile your .tex code only once using LatexMk
+
+** Module Flags
++ ~+latexmk~ Use LatexMk instead of LaTeX to compile documents.
+
+** Plugins
++ [[http://www.gnu.org/software/auctex/][auctex]]
++ [[http://elpa.gnu.org/packages/adaptive-wrap.html][adaptive-wrap]]
++ [[https://github.com/jsinglet/latex-preview-pane][latex-preview-pane]]
++ [[https://github.com/tom-tan/auctex-latexmk][auctex-latexmk]]*
++ [[https://github.com/alexeyr/company-auctex][company-auctex]]*
++ [[https://github.com/TheBB/company-reftex][company-reftex]]*
++ [[https://github.com/vspinu/company-math][company-math]]*
++ [[https://github.com/tmalsburg/helm-bibtex][ivy-bibtex]]* or [[https://github.com/tmalsburg/helm-bibtex][helm-bibtex]]*
+
+* TODO Features
+
+* Customization
+** Specifying the location of a bibtex file & corresponding PDFs
+The reftex and bibtex-completion packages have two variables that allow you to
+specify where it should find your bibliography file(s) and their corresponding
+PDFs:
+
+#+BEGIN_SRC emacs-lisp
+(setq reftex-default-bibliography "/your/bib/file.bib")
+;; Optionally specifying a location for the corresponding PDFs
+(setq bibtex-completion-library-path (list "/your/bib/pdfs"))
+#+END_SRC
+
+** Changing the PDFs viewer
+This module provides integration for four supported pdf viewers. They are
+
++ [[https://skim-app.sourceforge.io/][Skim.app]] (MacOS only)
++ Okular
++ Zathura
++ pdf-tools (requires =:tools pdf= module)
+
+They are searched for in this order. See ~+latex-viewers~ to change the order,
+or remove tools from the search altogether. If you want to exclusively use one
+tool, for instance:
+
+#+BEGIN_SRC emacs-lisp
+(setq +latex-viewers '(zathura))
+#+END_SRC
+
+If none of these tools are found, ~latex-preview-pane~ (uses ~DocView~ in Emacs)
+is used as a fallback. You can use this exclusively by setting ~+latex-viewers~
+to ~nil~.
diff --git a/modules/lang/latex/autoload.el b/modules/lang/latex/autoload.el
new file mode 100644
index 000000000..c63b7b6be
--- /dev/null
+++ b/modules/lang/latex/autoload.el
@@ -0,0 +1,49 @@
+;;; lang/latex/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +latex/LaTeX-indent-item ()
+ "Provide proper indentation for LaTeX \"itemize\",\"enumerate\", and \"description\" environments.
+
+ \"\\item\" is indented `LaTeX-indent-level' spaces relative to
+ the the beginning of the environment.
+
+ Continuation lines are indented either twice
+ `LaTeX-indent-level', or `LaTeX-indent-level-item-continuation'
+ if the latter is bound."
+ (save-match-data
+ (let* ((offset LaTeX-indent-level)
+ (contin (or (and (boundp '+latex-indent-level-item-continuation)
+ +latex-indent-level-item-continuation)
+ (* 4 offset)))
+ (re-beg "\\\\begin{")
+ (re-end "\\\\end{")
+ (re-env "\\(itemize\\|\\enumerate\\|description\\)")
+ (indent (save-excursion
+ (when (looking-at (concat re-beg re-env "}"))
+ (end-of-line))
+ (LaTeX-find-matching-begin)
+ (current-column))))
+ (cond ((looking-at (concat re-beg re-env "}"))
+ (or (save-excursion
+ (beginning-of-line)
+ (ignore-errors
+ (LaTeX-find-matching-begin)
+ (+ (current-column)
+ (if (looking-at (concat re-beg re-env "}"))
+ contin
+ offset))))
+ indent))
+ ((looking-at (concat re-end re-env "}"))
+ indent)
+ ((looking-at "\\\\item")
+ (+ offset indent))
+ ((+ contin indent))))))
+
+;;;###autoload
+(defun +latex-symbols-company-backend (command &optional arg &rest _ignored)
+ "A wrapper backend for `company-mode' that either uses
+`company-math-symbols-unicode' or `company-math-symbols-latex'. If
+`+latex-enable-unicode-math' is non-nil use the former, otherwise the latter."
+ (if +latex-enable-unicode-math
+ (company-math-symbols-unicode command arg)
+ (company-math-symbols-latex command arg)))
diff --git a/modules/lang/latex/config.el b/modules/lang/latex/config.el
index 75d10c658..82efbbecf 100644
--- a/modules/lang/latex/config.el
+++ b/modules/lang/latex/config.el
@@ -1,88 +1,141 @@
;;; lang/latex/config.el -*- lexical-binding: t; -*-
-(defvar +latex-bibtex-dir "~/work/writing/biblio/"
- "Where bibtex files are kept.")
+(defvar +latex-indent-level-item-continuation 4
+ "Custom indentation level for items in enumeration-type environments")
-(defvar +latex-bibtex-default-file "default.bib"
- "TODO")
+(defvar +latex-bibtex-file nil
+ "File AUCTeX (specifically RefTeX) uses to search for citations.")
+
+(defvar +latex-enable-unicode-math nil
+ "If non-nil, use `company-math-symbols-unicode' backend in LaTeX-mode,
+enabling unicode symbols in math regions. This requires the unicode-math latex
+package to be installed.")
+
+(defvar +latex-viewers `(skim sumatrapdf zathura okular pdf-tools)
+ "A list of enabled latex viewers to use, in this order. If they don't exist,
+they will be ignored. Recognized viewers are skim, zathura, okular and
+pdf-tools.
+
+If no viewers are found, `latex-preview-pane' is used.")
+
+;;
+(defvar +latex--company-backends nil)
;;
-;; Plugins
-;;
+;; Packages
-;; Because tex-mode is built-in and AucTex has conflicting components, we need
-;; to ensure that auctex gets loaded instead of tex-mode.
-(load "auctex" nil t)
-(load "auctex-autoloads" nil t)
-(push '("\\.[tT]e[xX]\\'" . TeX-latex-mode) auto-mode-alist)
+(add-to-list 'auto-mode-alist '("\\.tex\\'" . TeX-latex-mode))
-(add-transient-hook! 'LaTeX-mode-hook
- (setq TeX-auto-save t
- TeX-parse-self t
- TeX-save-query nil
+
+(after! tex
+ (setq TeX-parse-self t ; parse on load
+ TeX-auto-save t ; parse on save
+ ;; use hidden dirs for auctex files
+ TeX-auto-local ".auctex-auto"
+ TeX-style-local ".auctex-style"
+ TeX-source-correlate-mode t
+ TeX-source-correlate-method 'synctex
+ ;; don't start the emacs server when correlating sources
TeX-source-correlate-start-server nil
- LaTeX-fill-break-at-separators nil
- LaTeX-section-hook
+ ;; automatically insert braces after sub/superscript in math mode
+ TeX-electric-sub-and-superscript t)
+ ;; fontify common latex commands
+ (load! "+fontification")
+ ;; select viewer
+ (load! "+viewers")
+ ;; prompt for master
+ (setq-default TeX-master nil)
+ ;; set-up chktex
+ (setcar (cdr (assoc "Check" TeX-command-list)) "chktex -v6 -H %s")
+ ;; tell emacs how to parse tex files
+ (setq-hook! 'TeX-mode-hook ispell-parser 'tex)
+ ;; Enable word wrapping
+ (add-hook 'TeX-mode-hook #'visual-line-mode)
+ ;; Fold TeX macros
+ (add-hook 'TeX-mode-hook #'TeX-fold-mode)
+ ;; Enable rainbow mode after applying styles to the buffer
+ (add-hook 'TeX-mode-hook #'rainbow-delimiters-mode)
+ ;; display output of latex commands in popup
+ (set-popup-rule! " output\\*$" :size 15)
+ ;; Do not prompt for Master files, this allows auto-insert to add templates to
+ ;; .tex files
+ (add-hook! 'TeX-mode-hook
+ ;; Necessary because it is added as an anonymous, byte-compiled function
+ (remove-hook 'find-file-hook
+ (cl-find-if #'byte-code-function-p find-file-hook)
+ 'local))
+ (add-hook 'latex-mode-local-vars-hook #'flyspell-mode!)
+ ;; All these excess pairs dramatically slow down typing in latex buffers, so
+ ;; we remove them. Let snippets do their job.
+ (after! smartparens-latex
+ (let ((modes '(tex-mode plain-tex-mode latex-mode LaTeX-mode)))
+ (dolist (open '("\\left(" "\\left[" "\\left\\{" "\\left|"
+ "\\bigl(" "\\biggl(" "\\Bigl(" "\\Biggl(" "\\bigl["
+ "\\biggl[" "\\Bigl[" "\\Biggl[" "\\bigl\\{" "\\biggl\\{"
+ "\\Bigl\\{" "\\Biggl\\{"
+ "\\lfloor" "\\lceil" "\\langle"
+ "\\lVert" "\\lvert" "`"))
+ (sp-local-pair modes open nil :actions :rem))
+ (sp-local-pair modes "``" nil :unless '(:add sp-in-math-p)))))
+
+
+(after! latex
+ (setq LaTeX-section-hook ; Add the toc entry to the sectioning hooks.
'(LaTeX-section-heading
LaTeX-section-title
LaTeX-section-toc
LaTeX-section-section
- LaTeX-section-label))
-
- (add-hook! (latex-mode LaTeX-mode) #'turn-on-auto-fill)
- (add-hook! 'LaTeX-mode-hook #'(LaTeX-math-mode TeX-source-correlate-mode))
-
- (set! :popup " output\\*$" :regexp t :size 15 :noselect t :autoclose t :autokill t)
-
- (map! :map LaTeX-mode-map "C-j" nil)
-
- (def-package! company-auctex
- :when (featurep! :completion company)
- :init
- (set! :company-backend 'LaTeX-mode '(company-auctex))))
+ LaTeX-section-label)
+ LaTeX-fill-break-at-separators nil
+ LaTeX-item-indent 0)
+ (when +latex--company-backends
+ (set-company-backend! 'latex-mode +latex--company-backends))
+ ;; Set custom item indentation
+ (dolist (env '("itemize" "enumerate" "description"))
+ (add-to-list 'LaTeX-indent-environment-list `(,env +latex/LaTeX-indent-item))))
-(def-package! reftex ; built-in
- :commands (turn-on-reftex reftex-mode)
+(def-package! preview
+ :hook (LaTeX-mode . LaTeX-preview-setup)
+ :config
+ (setq-default preview-scale 1.4
+ preview-scale-function
+ (lambda () (* (/ 10.0 (preview-document-pt)) preview-scale))))
+
+
+;; Nicely indent lines that have wrapped when visual line mode is activated
+(def-package! adaptive-wrap
+ :hook (LaTeX-mode . adaptive-wrap-prefix-mode)
+ :init (setq-default adaptive-wrap-extra-indent 0))
+
+
+(def-package! auctex-latexmk
+ :when (featurep! +latexmk)
+ :after latex
:init
- (setq reftex-plug-into-AUCTeX t
- reftex-default-bibliography (list +latex-bibtex-default-file)
- reftex-toc-split-windows-fraction 0.2)
-
- (add-hook! (latex-mode LaTeX-mode) #'turn-on-reftex)
-
+ ;; Pass the -pdf flag when TeX-PDF-mode is active
+ (setq auctex-latexmk-inherit-TeX-PDF-mode t)
+ ;; Set LatexMk as the default
+ (setq-hook! LaTeX-mode TeX-command-default "LatexMk")
:config
- (map! :map reftex-mode-map
- :localleader :n ";" 'reftex-toc)
-
- (add-hook! 'reftex-toc-mode-hook
- (reftex-toc-rescan)
- (doom-hide-modeline-mode +1)
- (map! :local
- :e "j" #'next-line
- :e "k" #'previous-line
- :e "q" #'kill-buffer-and-window
- :e "ESC" #'kill-buffer-and-window)))
+ ;; Add latexmk as a TeX target
+ (auctex-latexmk-setup))
-(def-package! bibtex ; built-in
+(def-package! company-auctex
+ :when (featurep! :completion company)
:defer t
- :config
- (setq bibtex-dialect 'biblatex
- bibtex-align-at-equal-sign t
- bibtex-text-indentation 20
- bibtex-completion-bibliography (list +latex-bibtex-default-file))
+ :init
+ (add-to-list '+latex--company-backends #'company-auctex-environments nil #'eq)
+ (add-to-list '+latex--company-backends #'company-auctex-macros nil #'eq))
- (map! :map bibtex-mode-map "C-c \\" #'bibtex-fill-entry))
+(def-package! company-math
+ :when (featurep! :completion company)
+ :defer t
+ :init
+ (add-to-list '+latex--company-backends #'+latex-symbols-company-backend nil #'eq))
-(def-package! ivy-bibtex
- :when (featurep! :completion ivy)
- :commands ivy-bibtex)
-
-
-(def-package! helm-bibtex
- :when (featurep! :completion helm)
- :commands helm-bibtex)
-
+;; bibtex + reftex
+(load! "+ref")
diff --git a/modules/lang/latex/packages.el b/modules/lang/latex/packages.el
index ebf26c035..b181f5785 100644
--- a/modules/lang/latex/packages.el
+++ b/modules/lang/latex/packages.el
@@ -2,11 +2,22 @@
;;; lang/latex/packages.el
(package! auctex)
-;; (package! auctex-latexmk)
+(package! adaptive-wrap)
+(package! latex-preview-pane)
+
+;; Optional module features:
+
+(when (featurep! +latexmk)
+ (package! auctex-latexmk))
+
+;; Features according to other user selected options
(when (featurep! :completion company)
- (package! company-auctex))
+ (package! company-auctex)
+ (package! company-reftex)
+ (package! company-math))
(when (featurep! :completion ivy)
(package! ivy-bibtex))
(when (featurep! :completion helm)
(package! helm-bibtex))
+
diff --git a/modules/lang/ledger/config.el b/modules/lang/ledger/config.el
index 5db4e7c71..26fd8c4b8 100644
--- a/modules/lang/ledger/config.el
+++ b/modules/lang/ledger/config.el
@@ -1,15 +1,57 @@
;;; lang/ledger/config.el -*- lexical-binding: t; -*-
-(def-package! ledger-mode
- :mode "\\.ledger$"
- :config (setq ledger-clear-whole-transactions 1))
+;; `ledger-mode'
+(setq ledger-clear-whole-transactions 1)
+(defun +ledger*check-version (orig-fn)
+ "Fail gracefully if ledger binary isn't available."
+ (if (executable-find ledger-binary-path)
+ (funcall orig-fn)
+ (message "Couldn't find '%s' executable" ledger-binary-path)))
+(advice-add #'ledger-check-version :around #'+ledger*check-version)
-(def-package! evil-ledger
- :when (featurep! :feature evil)
- :hook (ledger-mode . evil-ledger-mode))
+;; Restore leader key in ledger reports
+(map! :after ledger-mode
+ :map ledger-report-mode-map
+ "C-c C-c" #'ledger-report-edit-report
+ "C-c C-r" #'ledger-report-redo
+ "C-c C-s" #'ledger-report-save
+ :map ledger-reconcile-mode-map
+ [tab] #'ledger-reconcile-toggle)
(def-package! flycheck-ledger
- :when (featurep! :feature syntax-checker)
- :init (add-hook 'ledger-mode-hook #'flycheck-mode))
+ :when (featurep! :tools flycheck)
+ :after ledger-mode)
+
+
+(def-package! evil-ledger
+ :when (featurep! :feature evil +everywhere)
+ :hook (ledger-mode . evil-ledger-mode)
+ :config
+ (set-evil-initial-state! 'ledger-report-mode 'normal)
+ (map! :map ledger-report-mode-map
+ :n "q" #'ledger-report-quit
+ :n "RET" #'ledger-report-edit-report
+ :n "gd" #'ledger-report-visit-source
+ :n "gr" #'ledger-report-redo
+ :map ledger-mode-map
+ :m "]]" #'ledger-navigate-next-xact-or-directive
+ :m "[[" #'ledger-navigate-prev-xact-or-directive
+
+ :localleader
+ :map ledger-mode-map
+ "a" #'ledger-add-transaction
+ "t" #'ledger-toggle-current
+ "d" #'ledger-delete-current-transaction
+ "r" #'ledger-report
+ "R" #'ledger-reconcile
+ "s" #'ledger-sort-region
+ "S" #'ledger-schedule-upcoming
+ (:prefix "g"
+ "s" #'ledger-display-ledger-stats
+ "b" #'ledger-display-balance-at-point))
+ ;; Fix inaccurate keybind message
+ (defun +ledger*fix-key-help (&rest _)
+ (message "q to quit; gr to redo; RET to edit; C-c C-s to save"))
+ (advice-add #'ledger-report :after #'+ledger*fix-key-help))
diff --git a/modules/lang/ledger/packages.el b/modules/lang/ledger/packages.el
index b58a40382..4661ee7a8 100644
--- a/modules/lang/ledger/packages.el
+++ b/modules/lang/ledger/packages.el
@@ -6,5 +6,5 @@
(when (featurep! :feature evil)
(package! evil-ledger))
-(when (featurep! :feature syntax-checker)
+(when (featurep! :tools flycheck)
(package! flycheck-ledger))
diff --git a/modules/lang/lua/autoload.el b/modules/lang/lua/autoload.el
index 2fc165a41..0100d1808 100644
--- a/modules/lang/lua/autoload.el
+++ b/modules/lang/lua/autoload.el
@@ -1,7 +1,7 @@
;;; lang/lua/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
-(defun +lua/repl ()
+(defun +lua/open-repl ()
"Open Lua REPL."
(interactive)
(lua-start-process "lua" "lua")
@@ -11,9 +11,10 @@
(defun +lua/run-love-game ()
"Run the current project with Love2D."
(interactive)
- (async-shell-command
- (format "%s %s"
- (or (executable-find "love")
- (if IS-MAC "open -a love.app"))
- (shell-quote-argument (doom-project-root)))))
+ (when-let* ((root (locate-dominating-file buffer-file-name "main.lua")))
+ (async-shell-command
+ (format "%s %s"
+ (or (executable-find "love")
+ (if IS-MAC "open -a love.app"))
+ (shell-quote-argument (file-name-directory root))))))
diff --git a/modules/lang/lua/config.el b/modules/lang/lua/config.el
index fdc6ad623..99e148747 100644
--- a/modules/lang/lua/config.el
+++ b/modules/lang/lua/config.el
@@ -1,42 +1,35 @@
-;;; lang/lua/config.el --- lua + Love2D -*- lexical-binding: t; -*-
+;;; lang/lua/config.el -*- lexical-binding: t; -*-
+
+;; sp's default rules are obnoxious, so disable them
+(provide 'smartparens-lua)
+
+
+;;
+;; Major modes
(def-package! lua-mode
- :mode "\\.lua$"
- :interpreter "lua"
+ :defer t
+ :init
+ ;; lua-indent-level defaults to 3 otherwise. Madness.
+ (setq lua-indent-level tab-width)
:config
- (add-hook 'lua-mode-hook #'flycheck-mode)
-
- (set! :electric 'lua-mode :words '("else" "end"))
- (set! :repl 'lua-mode #'+lua/repl)
- ;; sp's lua-specific rules are obnoxious, so we disable them
- (setq sp-pairs (delete (assq 'lua-mode sp-pairs) sp-pairs))
-
- (def-menu! +lua/build-menu
- "Build/compilation commands for `lua-mode' buffers."
- '(("Run Love app" :exec +lua/run-love-game :when +lua-love-mode))
- :prompt "Build tasks: ")
-
- (map! :map lua-mode-map
- :localleader
- "b" #'+lua/build-menu))
+ (set-lookup-handlers! 'lua-mode :documentation 'lua-search-documentation)
+ (set-electric! 'lua-mode :words '("else" "end"))
+ (set-repl-handler! 'lua-mode #'+lua/open-repl)
+ (set-company-backend! 'lua-mode '(company-lua company-yasnippet)))
-(def-package! company-lua
- :after (:all company lua-mode)
- :config
- (set! :company-backend 'lua-mode '(company-lua company-yasnippet)))
-
-
-(def-package! moonscript
- :mode ("\\.moon$" . moonscript-mode)
- :config (defvaralias 'moonscript-indent-offset 'tab-width))
+;;;###package moonscript
+(setq-hook! 'moonscript-mode-hook moonscript-indent-offset tab-width)
;;
-;; Frameworks
-;;
+;;; Frameworks
(def-project-mode! +lua-love-mode
:modes (lua-mode markdown-mode json-mode)
- :files (and "main.lua" "conf.lua"))
-
+ :files (and "main.lua" "conf.lua")
+ :on-load
+ (map! :localleader
+ :map +lua-love-mode-map
+ "b" #'+lua/run-love-game))
diff --git a/modules/lang/markdown/autoload.el b/modules/lang/markdown/autoload.el
index 9576e360d..f1c2c3904 100644
--- a/modules/lang/markdown/autoload.el
+++ b/modules/lang/markdown/autoload.el
@@ -11,7 +11,7 @@
(let ((delim "~~"))
(if (markdown-use-region-p)
;; Active region
- (cl-destructuring-bind (beg end)
+ (cl-destructuring-bind (beg . end)
(markdown-unwrap-things-in-region
(region-beginning) (region-end)
+markdown--regex-del 2 4)
@@ -21,3 +21,22 @@
(markdown-unwrap-thing-at-point nil 2 4)
(markdown-wrap-or-insert delim delim 'word nil nil)))))
+;;;###autoload
+(defun +markdown-flyspell-word-p ()
+ "Return t if point is on a word that should be spell checked.
+
+Return nil if on a link url, markup, html, or references."
+ (let ((faces (doom-enlist (get-text-property (point) 'face))))
+ (or (and (memq 'font-lock-comment-face faces)
+ (memq 'markdown-code-face faces))
+ (not (cl-loop with unsafe-faces = '(markdown-reference-face
+ markdown-url-face
+ markdown-markup-face
+ markdown-comment-face
+ markdown-html-attr-name-face
+ markdown-html-attr-value-face
+ markdown-html-tag-name-face
+ markdown-code-face)
+ for face in faces
+ if (memq face unsafe-faces)
+ return t)))))
diff --git a/modules/lang/markdown/config.el b/modules/lang/markdown/config.el
index 9c7293b0c..c17879cc5 100644
--- a/modules/lang/markdown/config.el
+++ b/modules/lang/markdown/config.el
@@ -1,53 +1,57 @@
;;; lang/markdown/config.el -*- lexical-binding: t; -*-
(def-package! markdown-mode
- :mode "/README$"
- :mode "\\.m\\(d\\|arkdown\\)$"
- :mode ("/README\\.md$" . gfm-mode)
+ :mode ("/README\\(?:\\.\\(?:markdown\\|md\\)\\)?\\'" . gfm-mode)
:init
(setq markdown-enable-wiki-links t
- markdown-enable-math t
markdown-italic-underscore t
markdown-asymmetric-header t
markdown-make-gfm-checkboxes-buttons t
markdown-gfm-additional-languages '("sh")
markdown-fontify-code-blocks-natively t
- markdown-hide-urls nil) ; trigger with `markdown-toggle-url-hiding'
+ markdown-hide-urls nil ; trigger with `markdown-toggle-url-hiding'
+ markdown-enable-math t ; syntax highlighting for latex fragments
+ markdown-gfm-uppercase-checkbox t) ; for compat with org-mode
:config
- (add-hook! markdown-mode
- (auto-fill-mode +1)
- (setq line-spacing 2
- fill-column 80))
+ (set-flyspell-predicate! '(markdown-mode gfm-mode)
+ #'+markdown-flyspell-word-p)
+ (set-lookup-handlers! '(markdown-mode gfm-mode)
+ :file #'markdown-follow-thing-at-point)
- (map! (:map markdown-mode-map
- [remap find-file-at-point] #'markdown-follow-thing-at-point
- "M-*" #'markdown-insert-list-item
- "M-b" #'markdown-insert-bold
- "M-i" #'markdown-insert-italic
- "M-`" #'+markdown/insert-del
- :m "gj" #'markdown-next-visible-heading
- :m "gk" #'markdown-previous-visible-heading
- ;; Assumes you have a markdown renderer plugin in chrome
- :n "M-r" #'browse-url-of-file
+ (add-hook 'markdown-mode-hook #'auto-fill-mode)
+
+ (sp-with-modes '(markdown-mode gfm-mode)
+ (sp-local-pair "```" "```" :post-handlers '(:add ("||\n[i]" "RET"))))
+
+ (map! :map markdown-mode-map
+ :i "M-*" #'markdown-insert-list-item
+ :i "M-b" #'markdown-insert-bold
+ :i "M-i" #'markdown-insert-italic
+ :i "M-`" #'+markdown/insert-del
+ (:when (featurep! :feature evil +everywhere)
+ :m "gj" #'markdown-next-visible-heading
+ :m "gk" #'markdown-previous-visible-heading
;; TODO: Make context sensitive
- :m "]h" #'markdown-next-visible-heading
- :m "[h" #'markdown-previous-visible-heading
- :m "[p" #'markdown-promote
- :m "]p" #'markdown-demote
- :m "[l" #'markdown-next-link
- :m "]l" #'markdown-previous-link
- :i "M--" #'markdown-insert-hr
-
- (:localleader
- :nv "o" #'markdown-open
- :nv "b" #'markdown-preview
- (:prefix "i"
- :nv "t" #'markdown-toc-generate-toc
- :nv "i" #'markdown-insert-image
- :nv "l" #'markdown-insert-link)))))
+ :m "]h" #'markdown-next-visible-heading
+ :m "[h" #'markdown-previous-visible-heading
+ :m "[p" #'markdown-promote
+ :m "]p" #'markdown-demote
+ :m "[l" #'markdown-previous-link
+ :m "]l" #'markdown-next-link
+ :i "M--" #'markdown-insert-hr
+ :n "M-r" #'browse-url-of-file)
+ (:localleader
+ "o" #'markdown-open
+ "b" #'markdown-preview
+ (:prefix "i"
+ "t" #'markdown-toc-generate-toc
+ "i" #'markdown-insert-image
+ "l" #'markdown-insert-link))))
-(def-package! markdown-toc
- :commands markdown-toc-generate-toc)
-
+(def-package! pandoc-mode
+ :when (featurep! +pandoc)
+ :commands pandoc-mode
+ :hook (markdown-mode . conditionally-turn-on-pandoc)
+ :init (setq markdown-command "pandoc --from=markdown --to=html --standalone --mathjax --highlight-style=pygments"))
diff --git a/modules/lang/markdown/doctor.el b/modules/lang/markdown/doctor.el
new file mode 100644
index 000000000..00167df56
--- /dev/null
+++ b/modules/lang/markdown/doctor.el
@@ -0,0 +1,11 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/markdown/doctor.el
+
+(when (featurep! +pandoc)
+ (unless (executable-find "pandoc")
+ (warn! "Couldn't find pandoc, markdown-mode may have issues")))
+
+(when (require 'markdown-mode nil t)
+ (unless (executable-find markdown-command)
+ (warn! "Couldn't find %S, can't export markdown to html"
+ markdown-command)))
diff --git a/modules/lang/markdown/packages.el b/modules/lang/markdown/packages.el
index 72eee45f7..bccae4844 100644
--- a/modules/lang/markdown/packages.el
+++ b/modules/lang/markdown/packages.el
@@ -4,3 +4,7 @@
(package! markdown-mode)
(package! markdown-toc)
+(when (featurep! +pandoc)
+ (package! pandoc-mode))
+
+
diff --git a/modules/lang/nim/README.org b/modules/lang/nim/README.org
new file mode 100644
index 000000000..bd12a1fc3
--- /dev/null
+++ b/modules/lang/nim/README.org
@@ -0,0 +1,47 @@
+#+TITLE: :lang Nim
+
+#+begin_quote
+This module is a work in progress.
+#+end_quote
+
+This module adds [[https://nim-lang.org][Nim]] support to Emacs.
+
++ Code completion (nimsuggest + company)
++ Syntax checking (nimsuggest + flycheck)
++ Babel support (~ob-nim~)
+
+* Table of Contents :TOC:
+- [[Module Flags][Module Flags]]
+- [[Prerequisites][Prerequisites]]
+ - [[Nim][Nim]]
+- [[Configuration][Configuration]]
+
+* Module Flags
+This module provides no flags.
+
+* Prerequisites
++ ~nim~ (for building & evaluation)
++ ~nimsuggest~ (for code completion, syntax checking & jump-to-definition functionality)
+
+** Nim
+=choosenim= is an installer and version manager for the Nim programming
+language. You can install the latest stable release of Nim by running the
+following in your terminal and following the onscreen instructions:
+
+#+BEGIN_SRC bash
+curl https://nim-lang.org/choosenim/init.sh -sSf | sh
+#+END_SRC
+
+Alternatively, nim is usually available through your OS's package manager:
+
+*** MacOS
+#+BEGIN_SRC sh :tangle (if (doom-system-os 'macos) "yes")
+brew install nim
+#+END_SRC
+
+*** Arch Linux
+#+BEGIN_SRC sh :dir /sudo:: :tangle (if (doom-system-os 'arch) "yes")
+sudo pacman --needed --noconfirm -S nim nimble
+#+END_SRC
+
+* Configuration
diff --git a/modules/lang/nim/config.el b/modules/lang/nim/config.el
new file mode 100644
index 000000000..2f6680694
--- /dev/null
+++ b/modules/lang/nim/config.el
@@ -0,0 +1,42 @@
+;;; lang/nim/config.el -*- lexical-binding: t; -*-
+
+(after! nim-mode
+ (defun +nim|init-nimsuggest-mode ()
+ "Conditionally load `nimsuggest-mode', instead of clumsily erroring out if
+nimsuggest isn't installed."
+ (unless (stringp nimsuggest-path)
+ (setq nimsuggest-path (executable-find "nimsuggest")))
+ (when (and nimsuggest-path (file-executable-p nimsuggest-path))
+ (nimsuggest-mode)))
+ (add-hook 'nim-mode-hook #'+nim|init-nimsuggest-mode)
+
+ (when IS-WINDOWS
+ ;; TODO File PR/report upstream (https://github.com/nim-lang/nim-mode)
+ (defun doom*nimsuggest--get-dirty-dir ()
+ "The original `nimsuggest--get-dirty-dir' incorrectly extracts the frame
+number from the string representation of `selected-frame', which can contain
+characters that are illegal on Windows, causing invalid argument errors when
+`nimsuggest--make-tempdir' tries to use it."
+ (let* ((frame-str (format "%s" (selected-frame)))
+ (frame-num-str (if (string-match " \\(0x[0-9a-z]+\\)>$" frame-str)
+ (match-string 1 frame-str))))
+ (file-name-as-directory (concat nimsuggest-dirty-directory frame-num-str))))
+ (advice-add #'nimsuggest--get-dirty-dir :override #'doom*nimsuggest--get-dirty-dir)
+
+ ;; TODO File PR/report upstream (https://github.com/nim-lang/nim-mode)
+ (defun doom*nimsuggest--get-temp-file-name (path)
+ "Removes invalid characters from the temp file path, including the unicode
+character that colon is replaced with, which is known to cause issues on
+windows."
+ (replace-regexp-in-string "[꞉* |<>\"?*]" "" path))
+ (advice-add #'nimsuggest--get-temp-file-name :filter-return #'doom*nimsuggest--get-temp-file-name))
+
+ (map! :localleader
+ :map nim-mode-map
+ "b" #'nim-compile))
+
+
+(def-package! flycheck-nim
+ :when (featurep! :tools flycheck)
+ :after nim-mode)
+
diff --git a/modules/lang/nim/doctor.el b/modules/lang/nim/doctor.el
new file mode 100644
index 000000000..788238c98
--- /dev/null
+++ b/modules/lang/nim/doctor.el
@@ -0,0 +1,9 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/nim/doctor.el
+
+(unless (executable-find "nimsuggest")
+ (warn! "Could not find nimsuggest executable; code-completion, syntax checking and jump-to-definition functionality will be disabled."))
+
+(unless (executable-find "nim")
+ (warn! "Could not find nim executable; build commands will be disabled."))
+
diff --git a/modules/lang/nim/packages.el b/modules/lang/nim/packages.el
new file mode 100644
index 000000000..ba994148a
--- /dev/null
+++ b/modules/lang/nim/packages.el
@@ -0,0 +1,9 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/nim/packages.el
+
+;;; requires nim nimsuggest nimble
+
+(package! nim-mode)
+
+(when (featurep! :tools flycheck)
+ (package! flycheck-nim))
diff --git a/modules/lang/nix/config.el b/modules/lang/nix/config.el
index e9d090fc3..e4f5ee0c1 100644
--- a/modules/lang/nix/config.el
+++ b/modules/lang/nix/config.el
@@ -1,4 +1,28 @@
;;; lang/nix/config.el -*- lexical-binding: t; -*-
(def-package! nix-mode
- :mode "\\.nix$")
+ :mode "\\.nix\\'"
+ :config
+ (set-company-backend! 'nix-mode 'company-nixos-options)
+
+ (setq nix-indent-function #'nix-indent-line)
+
+ (map! :localleader
+ :map nix-mode-map
+ "f" #'nix-update-fetch
+ "p" #'nix-format-buffer
+ "r" #'nix-repl-show
+ "s" #'nix-shell
+ "b" #'nix-build
+ "u" #'nix-unpack
+ (:when (featurep! :completion helm)
+ "o" #'helm-nixos-options)))
+
+(def-package! nix-drv-mode
+ :mode "\\.drv\\'")
+
+(def-package! nix-update
+ :commands nix-update-fetch)
+
+(def-package! nix-repl
+ :commands nix-repl-show)
diff --git a/modules/lang/nix/doctor.el b/modules/lang/nix/doctor.el
new file mode 100644
index 000000000..93f4de0c3
--- /dev/null
+++ b/modules/lang/nix/doctor.el
@@ -0,0 +1,9 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/nix/doctor.el
+
+(unless (executable-find "nix")
+ (warn! "Couldn't find the nix package manager. nix-mode won't work."))
+
+(unless (executable-find "nixfmt")
+ (warn! "Couldn't find nixfmt. nix-format-buffer won't work."))
+
diff --git a/modules/lang/nix/packages.el b/modules/lang/nix/packages.el
new file mode 100644
index 000000000..eb3256b84
--- /dev/null
+++ b/modules/lang/nix/packages.el
@@ -0,0 +1,11 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/nix/packages.el
+
+(package! nix-mode)
+(package! nix-update)
+
+(when (featurep! :completion company)
+ (package! company-nixos-options))
+
+(when (featurep! :completion helm)
+ (package! helm-nixos-options))
diff --git a/modules/lang/ocaml/README.org b/modules/lang/ocaml/README.org
new file mode 100644
index 000000000..1ba199387
--- /dev/null
+++ b/modules/lang/ocaml/README.org
@@ -0,0 +1,87 @@
+#+TITLE: :lang ocaml
+
+This module adds [[https://ocaml.org/][OCaml]] support, powered by [[https://github.com/ocaml/tuareg][tuareg-mode]].
+
++ Code completion, documentation look-up, code navigation and refactoring ([[https://github.com/ocaml/merlin/wiki/emacs-from-scratch][merlin]])
++ Type, documentation and function argument display on idle ([[https://github.com/Khady/merlin-eldoc][merlin-eldoc]])
++ REPL ([[https://github.com/ocaml-community/utop][utop]])
++ Syntax-checking (~merlin~ with [[https://github.com/flycheck/flycheck-ocaml][flycheck-ocaml]])
++ Auto-indentation ([[https://github.com/OCamlPro/ocp-indent][ocp-indent]])
++ Code formatting ([[https://github.com/ocaml-ppx/ocamlformat][ocamlformat]])
++ Dune file format ([[http://dune.build/][dune]])
+
+* Table of Contents :TOC:
+- [[Module Flags][Module Flags]]
+- [[Prerequisites][Prerequisites]]
+- [[Features][Features]]
+- [[Configuration][Configuration]]
+- [[Appendix][Appendix]]
+ - [[Commands][Commands]]
+ - [[Hacks][Hacks]]
+
+* Module Flags
+This module provides the ~+opam-site-lisp~ flag to compile editor support ~.el~
+files from opam's =site-lisp= directory.
+By default all editor plugins are installed from MELPA/GitHub even if the
+corresponding =opam= package isn't installed.
+If this flag is enabled then you must have all of the packages listed in
+=package.el= installed via =opam=.
+
+To enable this, use:
+#+BEGIN_SRC emacs-lisp
+(doom! :lang (ocaml +opam-site-lisp))
+#+END_SRC
+
+* Prerequisites
+It is highly recommended to install [[http://opam.ocaml.org/][opam]].
+To get all the features you should also install these ~opam~ packages, however
+they are not all required (features should be disabled gracefully when a tool is
+missing):
+#+BEGIN_SRC shell
+opam install merlin utop ocp-indent dune ocamlformat
+#+END_SRC
+
+* Features
++ the following files should have syntax highlighting support:
+~.ml{i,p,y,}~, ~.eliom{i,}~, ~jbuild~, ~dune~, ~opam~
++ =merlin-mode= is activated whenever a =.merlin= file is found (including in a
+ parent directory) and =ocamlmerlin= executable is present
++ line-based auto-indentation is provided by =ocp-indent= when installed
+
+* Configuration
++ if =:completion company= is enabled then autocomplete is provided by =merlin=
++ when =:tools flycheck= is enabled then =flycheck-ocaml= is activated to do
+ on-the-fly syntax/type checking via =merlin=, otherwise this is only done when
+ the file is saved.
++ spell checking is activated in comments if =:tools flyspell= is active
++ a REPL is provided if =utop= is installed and =:feature eval= is active
++ if =:editor format= is enabled, the =ocamlformat= executable is available and
+ there is an =.ocamlformat= file present then =format-all-buffer= is bound to
+ =ocamlformat=, otherwise to =ocp-indent=
++ if =:editor multiple-cursors= is enabled then identifiers can be refactored
+ with =v R= and multiple cursors (this correctly matches identifier occurrences
+ according to scope, it is not purely a textual match)
++ if =:emacs imenu= is enabled then top level symbols (modules, type, functions, etc.) can
+ be looked up using =SPC / i=
+
+Run =make install= to install all packages, and =make doctor= to diagnose missing tools.
+
+* Appendix
+** Commands
+ | command | key / ex command | description |
+ |------------------------------+------------------+-----------------------------------------------------------|
+ | =merlin-type-enclosing= | =SPC m t= | display type under point |
+ | =tuareg-find-alternate-file= | =SPC m a= | switch between =.ml= and =.mli= |
+ | =merlin-locate= | =gd= | lookup definition |
+ | =merlin-occurences= | =SPC c D= | lookup references |
+ | =merlin-document= | =K= | lookup documentation |
+ | =merlin-imenu= | =SPC / i= | symbol lookup in file |
+ | =merlin-iedit-occurrences= | =v R= | visual refactor identifier under point (multiple cursors) |
+ | =utop= | =SPC o r= | open =utop= as REPL |
+ | =utop-eval-region= | =SPC c e= | evaluate selected region in =utop= |
+
+** Hacks
++ =set-pretty-symbols!= is called with the full tuareg prettify symbol list, this
+ can cause columns to change as certain keywords are shortened (e.g. =fun=
+ becomes \lambda.
++ =tuareg-opam-update-env= is called the first time =tuareg= is loaded
diff --git a/modules/lang/ocaml/config.el b/modules/lang/ocaml/config.el
index 22067cffb..db17fbdbb 100644
--- a/modules/lang/ocaml/config.el
+++ b/modules/lang/ocaml/config.el
@@ -1,9 +1,110 @@
;;; lang/ocaml/config.el -*- lexical-binding: t; -*-
-(def-package! tuareg
- :mode ("\\.ml[4ilpy]?$" . tuareg-mode))
+(when (featurep! +lsp)
+ (add-hook! (tuareg-mode reason-mode) #'lsp!))
-(def-package! merlin
- :after tuareg
- :hook (tuareg-mode . merlin-mode))
+(after! tuareg
+ ;; tuareg-mode has the prettify symbols itself
+ (set-pretty-symbols! 'tuareg-mode :alist
+ (append tuareg-prettify-symbols-basic-alist
+ tuareg-prettify-symbols-extra-alist))
+ ;; harmless if `prettify-symbols-mode' isn't active
+ (setq tuareg-prettify-symbols-full t)
+
+ ;; Use opam to set environment
+ (setq tuareg-opam-insinuate t)
+ (tuareg-opam-update-env (tuareg-opam-current-compiler))
+
+ ;; Spell-check comments
+ (when (featurep! :tools flyspell)
+ (add-hook 'tuareg-mode-local-vars-hook #'flyspell-prog-mode))
+
+
+ (def-package! merlin
+ :unless (featurep! +lsp)
+ :defer t
+ :init
+ (defun +ocaml|init-merlin ()
+ "Activate `merlin-mode' if the ocamlmerlin executable exists."
+ (when (executable-find "ocamlmerlin")
+ (merlin-mode)))
+ (add-hook 'tuareg-mode-hook #'+ocaml|init-merlin)
+
+ (set-company-backend! 'tuareg-mode 'merlin-company-backend)
+ (set-lookup-handlers! 'tuareg-mode
+ :definition #'merlin-locate
+ :references #'merlin-occurrences
+ :documentation #'merlin-document)
+ :config
+ (setq merlin-completion-with-doc t)
+
+ (map! :localleader
+ :map tuareg-mode-map
+ "t" #'merlin-type-enclosing
+ "a" #'tuareg-find-alternate-file)
+
+ (def-package! merlin-eldoc
+ :hook (merlin-mode . merlin-eldoc-setup))
+
+ (def-package! merlin-iedit
+ :when (featurep! :editor multiple-cursors)
+ :config
+ (map! :map tuareg-mode-map
+ :v "R" #'merlin-iedit-occurrences))
+
+ (def-package! merlin-imenu
+ :when (featurep! :emacs imenu)
+ :hook (merlin-mode . merlin-use-merlin-imenu)))
+
+
+ (def-package! flycheck-ocaml
+ :when (featurep! :tools flycheck)
+ :init
+ (defun +ocaml|init-flycheck ()
+ "Activate `flycheck-ocaml` if the current project possesses a .merlin file."
+ (when (projectile-locate-dominating-file default-directory ".merlin")
+ ;; Disable Merlin's own error checking
+ (setq merlin-error-after-save nil)
+ ;; Enable Flycheck checker
+ (flycheck-ocaml-setup)))
+ (add-hook 'merlin-mode-hook #'+ocaml|init-flycheck))
+
+
+ (def-package! utop
+ :when (featurep! :feature eval)
+ :defer t ; loaded by hook below
+ :init
+ (set-repl-handler! 'tuareg-mode #'utop)
+ (set-eval-handler! 'tuareg-mode #'utop-eval-region)
+ (defun +ocaml|init-utop ()
+ (when (executable-find "utop")
+ (utop-minor-mode)))
+ (add-hook 'tuareg-mode-hook #'+ocaml|init-utop))
+
+
+ (def-package! ocp-indent
+ ;; must be careful to always defer this, it has autoloads that adds hooks
+ ;; which we do not want if the executable can't be found
+ :defer t
+ :init
+ (defun +ocaml|init-ocp-indent ()
+ "Run `ocp-setup-indent', so long as the ocp-indent binary exists."
+ (when (executable-find "ocp-indent")
+ (ocp-setup-indent)))
+ (add-hook 'tuareg-mode-hook #'+ocaml|init-ocp-indent))
+
+
+ (def-package! ocamlformat
+ :when (featurep! :editor format)
+ :commands ocamlformat
+ :init
+ (set-formatter! 'ocamlformat #'ocamlformat
+ :modes '(caml-mode tuareg-mode))
+ ;; TODO Fix region-based formatting support
+ (defun +ocaml|init-ocamlformat ()
+ (setq +format-with 'ocp-indent)
+ (when (and (executable-find "ocamlformat")
+ (locate-dominating-file default-directory ".ocamlformat"))
+ (setq +format-with 'ocamlformat)))
+ (add-hook 'tuareg-mode-hook #'+ocaml|init-ocamlformat)))
diff --git a/modules/lang/ocaml/doctor.el b/modules/lang/ocaml/doctor.el
new file mode 100644
index 000000000..7429c68c6
--- /dev/null
+++ b/modules/lang/ocaml/doctor.el
@@ -0,0 +1,20 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/ocaml/doctor.el
+
+(unless (executable-find "ocamlmerlin")
+ (warn! "Couldn't find ocamlmerlin. Lookup, completion and syntax checking won't work"))
+
+;; Tuareg can still indent
+(unless (executable-find "ocp-indent")
+ (warn! "Couldn't find ocp-indent. Auto-indentation will be less precise"))
+
+(when (featurep! :feature eval)
+ (unless (executable-find "utop")
+ (warn! "Couldn't find utop. REPL won't be available")))
+
+(unless (executable-find "dune")
+ (warn! "Couldn't find dune. Won't be able to highlight dune files"))
+
+(when (featurep! :editor format)
+ (unless (executable-find "ocamlformat")
+ (warn! "Couldn't find ocamlformat. Code-formatting will be unavailable")))
diff --git a/modules/lang/ocaml/packages.el b/modules/lang/ocaml/packages.el
index 3da213bfd..748274499 100644
--- a/modules/lang/ocaml/packages.el
+++ b/modules/lang/ocaml/packages.el
@@ -3,3 +3,31 @@
(package! tuareg)
(package! merlin)
+(package! merlin-eldoc)
+(package! ocp-indent)
+
+(when (featurep! :tools flycheck)
+ (package! flycheck-ocaml))
+
+(when (featurep! :feature eval)
+ (package! utop))
+
+(when (featurep! :editor format)
+ ;; by default quelpa generated a version 0pre0.20180929.192844, which got
+ ;; parsed into (0 -1 0 ...), which when compared with version nil (0) in
+ ;; package-installed-p always yielded false
+ (package! ocamlformat :recipe (:fetcher github :repo "ocaml-ppx/ocamlformat" :files ("emacs/*.el"))))
+
+(package! dune :recipe (:fetcher github :repo "ocaml/dune" :files ("editor-integration/emacs/*.el")))
+
+
+;; (defvar +ocaml-elisp-dir
+;; (when (executable-find "opam")
+;; (let ((opam-share (ignore-errors (car (process-lines "opam" "config" "var" "share" "--safe")))))
+;; (when (and opam-share (file-directory-p opam-share))
+;; (expand-file-name "emacs/site-lisp" opam-share)))))
+;;
+;; (defmacro localpackage! (name)
+;; `(package! ,name :recipe (:fetcher file :path ,+ocaml-elisp-dir)))
+;;
+;; (localpackage! opam-site-lisp)
diff --git a/modules/lang/org/+attach.el b/modules/lang/org/+attach.el
index 564bb02e8..bade75ea5 100644
--- a/modules/lang/org/+attach.el
+++ b/modules/lang/org/+attach.el
@@ -3,36 +3,67 @@
;; I believe Org's native attachment system is over-complicated and litters
;; files with metadata I don't want. So I wrote my own, which:
;;
-;; + Causes attachments to be placed in a centralized location,
+;; + Places attachments in a centralized location (`+org-attach-dir' in
+;; `org-directory'), using an attach:* link abbreviation.
+;; + Use `+org-attach/sync' to index all attachments in `org-directory' that use
+;; the attach:* abbreviation and delete orphaned ones that are no longer
+;; referenced.
;; + Adds drag-and-drop support for images (with inline image preview)
;; + Adds drag-and-drop support for media files (pdfs, zips, etc) with a
;; filetype icon and short link.
-;; + TODO Offers an attachment management system.
;; Some commands of interest:
;; + `org-download-screenshot'
;; + `+org-attach/file'
;; + `+org-attach/url'
-;; + :org [FILE/URL]
+;; + `+org-attach/sync'
(defvar +org-attach-dir ".attach/"
- "Where to store attachments (relative to current org file).")
+ "Where to store attachments relative to `org-directory'.")
+;;
+;;; Bootstrap
+
+(setq org-attach-directory (expand-file-name +org-attach-dir org-directory)
+ org-download-image-dir org-attach-directory
+ org-download-heading-lvl nil
+ org-download-timestamp "_%Y%m%d_%H%M%S")
+
+;; A shorter link to attachments
+(add-to-list 'org-link-abbrev-alist (cons "attach" (abbreviate-file-name org-attach-directory)))
+
+(org-link-set-parameters
+ "attach"
+ :follow (lambda (link) (find-file (expand-file-name link org-attach-directory)))
+ :complete (lambda (&optional _arg)
+ (+org--relpath (+org-link-read-file "attach" org-attach-directory)
+ org-attach-directory))
+ :face (lambda (link)
+ (if (file-exists-p (expand-file-name link org-attach-directory))
+ 'org-link
+ 'error)))
+
+(after! projectile
+ (add-to-list 'projectile-globally-ignored-directories
+ (car (last (split-string +org-attach-dir "/" t)))))
+
+(after! recentf
+ (add-to-list 'recentf-exclude (format "%s.+$" (regexp-quote org-attach-directory))))
+
+
+;;
+;;; Packages
+
(def-package! org-download
:commands (org-download-dnd org-download-dnd-base64)
:init
- ;; Add these myself, so that org-download is lazy-loaded...
- (setq dnd-protocol-alist
- `(("^\\(https?\\|ftp\\|file\\|nfs\\):" . +org-attach-download-dnd)
- ("^data:" . org-download-dnd-base64)
- ,@dnd-protocol-alist))
+ ;; Add these manually so that org-download is lazy-loaded...
+ (add-to-list 'dnd-protocol-alist '("^\\(https?\\|ftp\\|file\\|nfs\\):" . +org-attach-download-dnd))
+ (add-to-list 'dnd-protocol-alist '("^data:" . org-download-dnd-base64))
(advice-add #'org-download-enable :override #'ignore)
:config
- (setq-default org-download-image-dir org-attach-directory
- org-download-heading-lvl nil
- org-download-timestamp "_%Y%m%d_%H%M%S")
(setq org-download-screenshot-method
(cond (IS-MAC "screencapture -i %s")
@@ -40,33 +71,22 @@
(cond ((executable-find "maim") "maim -s %s")
((executable-find "scrot") "scrot -s %s")))))
- ;; Ensure that relative inline image paths are relative to the attachment folder.
- (advice-add #'org-display-inline-images :around #'+org-attach*relative-to-attach-dir)
-
;; Handle non-image files a little differently. Images should be inserted
;; as-is, as image previews. Other files, like pdfs or zips, should be linked
;; to, with an icon indicating the type of file.
(advice-add #'org-download-insert-link :override #'+org-attach*insert-link)
(defun +org-attach*download-subdir ()
- (when (file-in-directory-p buffer-file-name +org-dir)
- (file-relative-name buffer-file-name +org-dir)))
+ (when (file-in-directory-p buffer-file-name org-directory)
+ (file-relative-name buffer-file-name org-directory)))
- ;; Write download paths relative to current file
(defun +org-attach*download-fullname (path)
- (file-relative-name path (file-name-directory buffer-file-name)))
+ "Write PATH relative to current file."
+ (let ((dir (or (if buffer-file-name (file-name-directory buffer-file-name))
+ default-directory)))
+ (if (file-in-directory-p dir org-directory)
+ (file-relative-name path dir)
+ path)))
(advice-add #'org-download--dir-2 :override #'ignore)
(advice-add #'org-download--fullname
:filter-return #'+org-attach*download-fullname))
-
-;;
-(after! org
- (setq org-attach-directory (expand-file-name +org-attach-dir +org-dir))
-
- (push (car (last (split-string +org-attach-dir "/" t)))
- projectile-globally-ignored-directories)
-
- (after! recentf
- (push (format "%s.+$" (regexp-quote org-attach-directory))
- recentf-exclude)))
-
diff --git a/modules/lang/org/+babel.el b/modules/lang/org/+babel.el
index 73a7dba78..14fa91882 100644
--- a/modules/lang/org/+babel.el
+++ b/modules/lang/org/+babel.el
@@ -1,48 +1,103 @@
;;; lang/org/+babel.el -*- lexical-binding: t; -*-
-(defvar +org-babel-languages
- '(calc
- css
- emacs-lisp
- haskell
- js
- latex
- ledger
- lilypond
- lisp
- matlab
- plantuml
- python
- restclient ; ob-restclient
- ruby
- rust ; ob-rust
- shell
- sqlite
- sql-mode ; ob-sql-mode
- translate) ; ob-translate
- "A list of org-babel languages to load.")
+(defvar +org-babel-mode-alist
+ '((cpp . C)
+ (C++ . C)
+ (D . C)
+ (sh . shell)
+ (bash . shell)
+ (matlab . octave))
+ "An alist mapping languages to babel libraries. This is necessary for babel
+libraries (ob-*.el) that don't match the name of the language.
+
+For example, with (fish . shell) will cause #+BEGIN_SRC fish to load ob-shell.el
+when executed.")
+
+(defvar +org-babel-load-functions ()
+ "A list of functions executed to load the current executing src block. They
+take one argument (the language specified in the src block, as a string). Stops
+at the first function to return non-nil.")
-(after! org
- (setq org-src-fontify-natively t ; make code pretty
- org-src-preserve-indentation t ; use native major-mode indentation
- org-src-tab-acts-natively t
- org-src-window-setup 'current-window
- org-confirm-babel-evaluate nil) ; you don't need my permission
+;;
+;;; Bootstrap
- (org-babel-do-load-languages
- 'org-babel-load-languages
- (cl-loop for sym in +org-babel-languages
- collect (cons sym t)))
+(setq org-src-fontify-natively t ; make code pretty
+ org-src-preserve-indentation t ; use native major-mode indentation
+ org-src-tab-acts-natively t
+ org-src-window-setup 'current-window
+ org-confirm-babel-evaluate nil) ; you don't need my permission
- ;; I prefer C-c C-c for confirming over the default C-c '
- (map! :map org-src-mode-map "C-c C-c" #'org-edit-src-exit)
+;; Use major-mode native TAB indentation in SRC blocks
+(advice-add #'org-return-indent :after #'+org*fix-newline-and-indent-in-src-blocks)
- ;; In a recent update, `org-babel-get-header' was removed from org-mode, which
- ;; is something a fair number of babel plugins use. So until those plugins
- ;; update, this polyfill will do:
- (defun org-babel-get-header (params key &optional others)
- (cl-loop with fn = (if others #'not #'identity)
- for p in params
- if (funcall fn (eq (car p) key))
- collect p)))
+(defun +org*babel-lazy-load-library (info)
+ "Load babel libraries lazily when babel blocks are executed."
+ (let* ((lang (nth 0 info))
+ (lang (if (symbolp lang) lang (intern lang)))
+ (lang (or (cdr (assq lang +org-babel-mode-alist))
+ lang)))
+ (when (and (not (cdr (assq lang org-babel-load-languages)))
+ (or (run-hook-with-args-until-success '+org-babel-load-functions lang)
+ (require (intern (format "ob-%s" lang)) nil t)))
+ (when (assq :async (nth 2 info))
+ ;; ob-async has its own agenda for lazy loading packages (in the
+ ;; child process), so we only need to make sure it's loaded.
+ (require 'ob-async nil t))
+ (add-to-list 'org-babel-load-languages (cons lang t)))
+ t))
+(advice-add #'org-babel-confirm-evaluate :after-while #'+org*babel-lazy-load-library)
+
+;; I prefer C-c C-c over C-c ' (more consistent)
+(define-key org-src-mode-map (kbd "C-c C-c") #'org-edit-src-exit)
+
+;; `org-babel-get-header' was removed from org in 9.0. Quite a few babel
+;; plugins use it, so until those plugins update, this polyfill will do:
+(defun org-babel-get-header (params key &optional others)
+ (cl-loop with fn = (if others #'not #'identity)
+ for p in params
+ if (funcall fn (eq (car p) key))
+ collect p))
+
+
+;;
+;;; Packages
+
+(def-package! ob-ipython
+ :when (featurep! +ipython)
+ :defer t
+ :init
+ (defvar +ob-ipython-local-runtime-dir nil
+ "TODO")
+
+ (setq ob-ipython-resources-dir ".ob-ipython-resrc")
+
+ (defun +org|babel-load-ipython (lang)
+ (and (string-prefix-p "jupyter-" (symbol-name lang))
+ (require 'ob-ipython nil t)))
+ (add-hook '+org-babel-load-functions #'+org|babel-load-ipython)
+ :config
+ (set-popup-rules!
+ '(("\\*ob-ipython.*"
+ :slot 2 :side right :size 100 :height 0.2
+ :select nil :quit nil :transient nil)
+ ("^\\*Python"
+ :slot 0 :side right :size 100
+ :select nil :quit nil :ttl nil)
+ ("\\*Python:.*"
+ :slot 0 :side right :size 100
+ :select nil :quit nil :transient nil)))
+
+ ;; advices for remote kernel and org-src-edit
+ (advice-add 'org-babel-edit-prep:ipython :override #'+org*org-babel-edit-prep:ipython)
+ (advice-add 'org-babel-ipython-initiate-session :override #'+org*org-babel-ipython-initiate-session)
+ (advice-add 'ob-ipython--create-repl :override #'+org*ob-ipython--create-repl)
+ (advice-add 'org-babel-execute:ipython :override #'+org*org-babel-execute:ipython)
+
+ ;; retina resolution image hack
+ (when (eq window-system 'ns)
+ (advice-add 'ob-ipython--write-base64-string :around #'+org*ob-ipython--write-base64-string))
+
+ ;; ipython has its own async keyword, disable ipython in ob-async.
+ (after! ob-async
+ (add-to-list 'ob-async-no-async-languages-alist "ipython")))
diff --git a/modules/lang/org/+capture.el b/modules/lang/org/+capture.el
index 5debb99a4..bec8bdf71 100644
--- a/modules/lang/org/+capture.el
+++ b/modules/lang/org/+capture.el
@@ -1,40 +1,93 @@
;;; lang/org/+capture.el -*- lexical-binding: t; -*-
-;; Sets up two `org-capture' workflows that I like:
-;;
-;; 1. The traditional way: invoking `org-capture' directly (or through a
-;; command, like :org).
+;; Sets up some reasonable defaults, as well as two `org-capture' workflows that
+;; I like:
;;
+;; 1. The traditional way: invoking `org-capture' directly, via SPC X, or
+;; through the :cap ex command.
;; 2. Through a org-capture popup frame that is invoked from outside Emacs (the
-;; script is in ~/.emacs.d/bin). This lets me open an org-capture box
-;; anywhere I can call org-capture (whether or not Emacs is open/running),
-;; like, say, from qutebrowser, vimperator, dmenu or a global keybinding.
+;; ~/.emacs.d/bin/org-capture script). This can be invoked from qutebrowser,
+;; vimperator, dmenu or a global keybinding.
-(defvar +org-default-todo-file "todo.org"
- "TODO")
+(defvar +org-capture-todo-file "todo.org"
+ "Default target for todo entries.
-(defvar +org-default-notes-file "notes.org"
- "TODO")
+Is relative to `org-directory', unless it is absolute. Is used in Doom's default
+`org-capture-templates'.")
-(defvar org-capture-templates
- '(("t" "Todo" entry
- (file+headline +org-default-todo-file "Inbox")
- "* [ ] %?\n%i" :prepend t :kill-buffer t)
+(defvar +org-capture-changelog-file "changelog.org"
+ "Default target for changelog entries.
- ("n" "Notes" entry
- (file+headline +org-default-notes-file "Inbox")
- "* %u %?\n%i" :prepend t :kill-buffer t)))
+Is relative to `org-directory' unless it is absolute. Is used in Doom's default
+`org-capture-templates'.")
+
+(defvar +org-capture-notes-file "notes.org"
+ "Default target for storing notes.
+
+Used as a fall back file for org-capture.el, for templates that do not specify a
+target file.
+
+Is relative to `org-directory', unless it is absolute. Is used in Doom's default
+`org-capture-templates'.")
+
+(setq org-default-notes-file (expand-file-name +org-capture-notes-file org-directory))
-(after! org
- (defvaralias 'org-default-notes-file '+org-default-notes-file)
+;;
+;;; Bootstrap
- (setq org-default-notes-file (expand-file-name +org-default-notes-file +org-dir))
+(setq org-capture-templates
+ '(("t" "Personal todo" entry
+ (file+headline +org-capture-todo-file "Inbox")
+ "* TODO %?\n%i\n%a" :prepend t :kill-buffer t)
+ ("n" "Personal notes" entry
+ (file+headline +org-capture-notes-file "Inbox")
+ "* %u %?\n%i\n%a" :prepend t :kill-buffer t)
- (add-hook 'org-capture-after-finalize-hook #'+org-capture|cleanup-frame)
+ ;; Will use {project-root}/{todo,notes,changelog}.org, unless a
+ ;; {todo,notes,changelog}.org file is found in a parent directory.
+ ;; Uses the basename from `+org-capture-todo-file',
+ ;; `+org-capture-changelog-file' and `+org-capture-notes-file'.
+ ("p" "Templates for projects")
+ ("pt" "Project todo" entry ; {project-root}/todo.org
+ (file+headline +org-capture-project-todo-file "Inbox")
+ "* TODO %?\n%i\n%a" :prepend t :kill-buffer t)
+ ("pn" "Project notes" entry ; {project-root}/notes.org
+ (file+headline +org-capture-project-notes-file "Inbox")
+ "* TODO %?\n%i\n%a" :prepend t :kill-buffer t)
+ ("pc" "Project changelog" entry ; {project-root}/changelog.org
+ (file+headline +org-capture-project-notes-file "Unreleased")
+ "* TODO %?\n%i\n%a" :prepend t :kill-buffer t)))
- (when (featurep! :feature evil)
- (add-hook 'org-capture-mode-hook #'evil-insert-state))
+(add-hook 'org-capture-after-finalize-hook #'+org-capture|cleanup-frame)
- (when (featurep! :ui doom-dashboard)
- (add-hook '+doom-dashboard-inhibit-functions #'+org-capture-frame-p)))
+(defun +org*capture-expand-variable-file (file)
+ "If a variable is used for a file path in `org-capture-template', it is used
+as is, and expanded relative to `default-directory'. This changes it to be
+relative to `org-directory', unless it is an absolute path."
+ (if (and (symbolp file) (boundp file))
+ (expand-file-name (symbol-value file) org-directory)
+ file))
+(advice-add #'org-capture-expand-file :filter-args #'+org*capture-expand-variable-file)
+
+(defun +org*prevent-save-prompts-when-refiling (&rest _)
+ "Fix #462: when refiling from org-capture, Emacs prompts to kill the
+underlying, modified buffer. This fixes that."
+ (when (bound-and-true-p org-capture-is-refiling)
+ (org-save-all-org-buffers)))
+(advice-add 'org-refile :after #'+org*prevent-save-prompts-when-refiling)
+
+(defun +org|show-target-in-capture-header ()
+ (setq header-line-format
+ (format "%s%s%s"
+ (propertize (abbreviate-file-name (buffer-file-name (buffer-base-buffer)))
+ 'face 'font-lock-string-face)
+ org-eldoc-breadcrumb-separator
+ header-line-format)))
+(add-hook 'org-capture-mode-hook #'+org|show-target-in-capture-header)
+
+(when (featurep! :feature evil)
+ (add-hook 'org-capture-mode-hook #'evil-insert-state))
+
+(when (featurep! :ui doom-dashboard)
+ (add-hook '+doom-dashboard-inhibit-functions #'+org-capture-frame-p))
diff --git a/modules/lang/org/+export.el b/modules/lang/org/+export.el
index d2e69db4d..1e93bfe48 100644
--- a/modules/lang/org/+export.el
+++ b/modules/lang/org/+export.el
@@ -1,36 +1,40 @@
;;; lang/org/+export.el -*- lexical-binding: t; -*-
;; I don't have any beef with org's built-in export system, but I do wish it
-;; would export to a central directory, rather than `default-directory'. This is
-;; because all my org files are usually in one place, and I want to be able to
-;; refer back to old exports if needed.
+;; would export to a central directory (by default), rather than
+;; `default-directory'. This is because all my org files are usually in one
+;; place, and I want to be able to refer back to old exports if needed.
-(def-package! ox-pandoc
- :defer t
- :config
- (when (executable-find "pandoc")
- (push 'pandoc org-export-backends))
+(defvar +org-export-dir ".export/"
+ "Where to store exported files relative to `org-directory'. Can be an absolute
+path too.")
+(define-obsolete-variable-alias 'org-export-directory '+org-export-dir "2.1.0")
+
+
+;;
+(setq org-export-backends '(ascii html latex md)
+ org-publish-timestamp-directory (concat doom-cache-dir "org-timestamps/"))
+
+(when (and (executable-find "pandoc")
+ (require 'ox-pandoc nil t))
+ (add-to-list 'org-export-backends 'pandoc nil #'eq)
(setq org-pandoc-options
'((standalone . t)
(mathjax . t)
- (parse-raw . t))))
+ (variable . "revealjs-url=https://cdn.jsdelivr.net/npm/reveal.js@3/"))))
-;;
-(after! org
- (add-transient-hook! #'org-export-dispatch (require 'ox-pandoc))
-
- (setq org-export-directory (expand-file-name ".export" +org-dir)
- org-export-backends '(ascii html latex md)
- org-export-with-toc t
- org-export-with-author t)
-
- ;; Always export to a central location
- (unless (file-directory-p org-export-directory)
- (make-directory org-export-directory t))
- (defun +org*export-output-file-name (args)
- "Return a centralized export location."
- (unless (nth 2 args)
- (setq args (append args (list org-export-directory))))
- args)
- (advice-add #'org-export-output-file-name
- :filter-args #'+org*export-output-file-name))
+;; Export to a central location by default or if target isn't in
+;; `org-directory'.
+(defun +org*export-output-file-name (args)
+ "Return a centralized export location unless one is provided or the current
+file isn't in `org-directory'."
+ (when (and (not (nth 2 args))
+ buffer-file-name
+ (file-in-directory-p buffer-file-name org-directory))
+ (cl-destructuring-bind (extension &optional subtreep _pubdir) args
+ (let ((dir (expand-file-name +org-export-dir org-directory)))
+ (unless (file-directory-p dir)
+ (make-directory dir t))
+ (setq args (list extension subtreep dir)))))
+ args)
+(advice-add #'org-export-output-file-name :filter-args #'+org*export-output-file-name)
diff --git a/modules/lang/org/+habit.el b/modules/lang/org/+habit.el
new file mode 100644
index 000000000..02963d06d
--- /dev/null
+++ b/modules/lang/org/+habit.el
@@ -0,0 +1,28 @@
+;;; lang/org/+habit.el -*- lexical-binding: t; -*-
+
+(defvar +org-habit-graph-padding 2
+ "The padding added to the end of the consistency graph")
+
+(defvar +org-habit-min-width 30
+ "Hides the consistency graph if the `org-habit-graph-column' is less than this value")
+
+(defvar +org-habit-graph-window-ratio 0.3
+ "The ratio of the consistency graphs relative to the window width")
+
+(defun +org-habit|resize-graph()
+ "Right align and resize the consistency graphs based on `+org-habit-graph-window-ratio'"
+ (require 'org-habit)
+ (let* ((total-days (float (+ org-habit-preceding-days org-habit-following-days)))
+ (preceding-days-ratio (/ org-habit-preceding-days total-days))
+ (graph-width (floor (* (window-width) +org-habit-graph-window-ratio)))
+ (preceding-days (floor (* graph-width preceding-days-ratio)))
+ (following-days (- graph-width preceding-days))
+ (graph-column (- (window-width) (+ preceding-days following-days)))
+ (graph-column-adjusted (if (> graph-column +org-habit-min-width)
+ (- graph-column +org-habit-graph-padding)
+ nil)))
+ (setq-local org-habit-preceding-days preceding-days)
+ (setq-local org-habit-following-days following-days)
+ (setq-local org-habit-graph-column graph-column-adjusted)))
+
+(add-hook 'org-agenda-mode-hook #'+org-habit|resize-graph)
diff --git a/modules/lang/org/+present.el b/modules/lang/org/+present.el
index daec968aa..bc6938a4f 100644
--- a/modules/lang/org/+present.el
+++ b/modules/lang/org/+present.el
@@ -5,13 +5,16 @@
;;
-;; Plugins
-;;
+;; Packages
(def-package! ox-reveal
- :defer t
+ :after ox
+ :init
+ ;; Fix #1127, where ox-reveal adds an errant entry to
+ ;; `org-structure-template-alist'
+ (setq org-reveal-note-key-char nil)
:config
- (setq org-reveal-root "http://cdn.jsdelivr.net/reveal.js/3.0.0/"
+ (setq org-reveal-root "https://cdn.jsdelivr.net/npm/reveal.js@3/"
org-reveal-mathjax t))
@@ -29,21 +32,10 @@
:n [left] #'org-tree-slide-move-previous-tree)
(add-hook! 'org-tree-slide-mode-after-narrow-hook
- #'(+org-present|detect-slide +org-present|add-overlays org-display-inline-images))
+ #'(+org-present|detect-slide
+ +org-present|add-overlays
+ org-display-inline-images))
(add-hook 'org-tree-slide-mode-hook #'+org-present|init-org-tree-window)
(advice-add #'org-tree-slide--display-tree-with-narrow
:around #'+org-present*narrow-to-subtree))
-
-
-(def-package! centered-window-mode :commands centered-window-mode)
-
-
-;;
-;; Bootstrap
-;;
-
-(after! org
- (require 'ox-reveal)
- (map! :map org-mode-map "" #'+org-present/start))
-
diff --git a/modules/lang/org/+protocol.el b/modules/lang/org/+protocol.el
new file mode 100644
index 000000000..9fd46fa50
--- /dev/null
+++ b/modules/lang/org/+protocol.el
@@ -0,0 +1,36 @@
+;;; lang/org/+protocol.el -*- lexical-binding: t; -*-
+
+;; Brings lazy-loaded support for org-protocol, so external programs (like
+;; browsers) can invoke specialized behavior from Emacs. Normally you'd simply
+;; require `org-protocol' and use it, but the package loads all of org for no
+;; compelling reason, so...
+(defun +org*server-visit-files (args)
+ "Advise `server-visit-flist' to invoke `org-protocol' lazily."
+ (cl-destructuring-bind (files proc &optional nowait) args
+ (catch 'greedy
+ (let ((flist (reverse files)))
+ (dolist (var flist)
+ (when (string-match-p ":/+" (car var))
+ (require 'server)
+ (require 'org-protocol)
+ ;; `\' to `/' on windows
+ (let ((fname (org-protocol-check-filename-for-protocol
+ (expand-file-name (car var))
+ (member var flist)
+ proc)))
+ (cond ((eq fname t) ; greedy? We need the t return value.
+ (setq files nil)
+ (throw 'greedy t))
+ ((stringp fname) ; probably filename
+ (setcar var fname))
+ ((setq files (delq var files)))))))))
+ (list files proc nowait)))
+(advice-add #'server-visit-files :filter-args #'+org*server-visit-files)
+
+;; Disable built-in, clumsy advice
+(after! org-protocol
+ (ad-disable-advice 'server-visit-files 'before 'org-protocol-detect-protocol-server))
+
+
+;; TODO org-board or better link grabbing support
+;; TODO org-capture + org-protocol instead of bin/org-capture
diff --git a/modules/lang/org/autoload/evil.el b/modules/lang/org/autoload/evil.el
deleted file mode 100644
index 62bfca831..000000000
--- a/modules/lang/org/autoload/evil.el
+++ /dev/null
@@ -1,11 +0,0 @@
-;;; lang/org/autoload/evil.el -*- lexical-binding: t; -*-
-;;;###if (featurep! :feature evil)
-
-;; TODO +org-attach:find
-
-;;;###autoload (autoload '+org-attach:uri "lang/org/autoload/evil" nil t)
-(evil-define-command +org-attach:uri (uri)
- "Downloads the file at URL and places an org link to it at the cursor."
- (interactive "")
- (+org-attach/uri uri))
-
diff --git a/modules/lang/org/autoload/ipython.el b/modules/lang/org/autoload/ipython.el
new file mode 100644
index 000000000..ba28a7cda
--- /dev/null
+++ b/modules/lang/org/autoload/ipython.el
@@ -0,0 +1,156 @@
+;;; lang/org/autoload/ipython.el -*- lexical-binding: t; -*-
+;;;###if (featurep! +ipython)
+
+;;;###autoload
+(defun +org*org-babel-ipython-initiate-session (&optional session params)
+ "Create a session named SESSION according to PARAMS."
+ (if (string= session "none")
+ (error
+ "ob-ipython currently only supports evaluation using a session.
+Make sure your src block has a :session param.")
+ (when (not (string-suffix-p ".json" session t))
+ (ob-ipython--create-kernel
+ (ob-ipython--normalize-session
+ session)
+ (cdr (assoc :kernel params))))
+ (ob-ipython--create-repl
+ (ob-ipython--normalize-session
+ session)
+ params)))
+
+(defun +org--ob-ipython-generate-local-path-from-remote (session host params)
+ "Given a remote SESSION with PARAMS and corresponding HOST, copy remote config to local, start a jupyter console to generate a new one."
+ (let* ((runtime-dir
+ (substring (shell-command-to-string (concat "ssh " host " jupyter --runtime-dir")) 0 -1))
+ (runtime-file (concat runtime-dir "/" "kernel-" session ".json"))
+ (tramp-path (concat "/ssh:" host ":" runtime-file))
+ (tramp-copy (concat (or +ob-ipython-local-runtime-dir
+ (substring (shell-command-to-string "jupyter --runtime-dir")
+ 0 -1))
+ "/remote-" host "-kernel-" session ".json"))
+ (local-path
+ (concat
+ "Python:ob-ipython-"
+ (file-name-sans-extension (file-name-nondirectory tramp-copy))
+ "-ssh.json")))
+ ;; scp remote file to local
+ (copy-file tramp-path tramp-copy t)
+ ;; connect to remote use new config
+ (let* ((python-shell-interpreter-interactive-arg " console --simple-prompt")
+ (python-shell-prompt-detect-enabled nil)
+ (python-shell-completion-native-enable nil)
+ (buf (python-shell-make-comint
+ (concat ob-ipython-command
+ " console --simple-prompt --existing "
+ tramp-copy " --ssh " host)
+ (concat "" local-path)
+ t))
+ (proc (get-buffer-process buf))
+ (dir (cdr (assoc :pydir params))))
+ (sleep-for 3)
+ (when dir
+ (with-current-buffer buf
+ (setq-local default-directory dir)))
+ (format "*%s*" proc))))
+
+;;;###autoload
+(defun +org*ob-ipython--create-repl (name &optional params)
+ "Create repl based on NAME and PARAMS.
+If PARAMS specifies remote kernel, copy the kernel config from remote server and
+create a repl connecting to remote session."
+ (let ((cmd (string-join (ob-ipython--kernel-repl-cmd name) " ")))
+ (cond ((string= "default" name)
+ (run-python cmd nil nil)
+ (format "*%s*" python-shell-buffer-name))
+ ((string-match "^remote-.*ssh.json" name)
+ (when (not (ignore-errors
+ (process-live-p
+ (get-process
+ (format
+ "Python:ob-ipython-%s"
+ name)))))
+ (let* ((remote (s-split "-" name))
+ (remote-host (nth 1 remote))
+ (remote-session (nth 3 remote)))
+ (+org--ob-ipython-generate-local-path-from-remote
+ remote-session
+ remote-host
+ params))))
+ ((let* ((process-name (format "Python:ob-ipython-%s" name))
+ (python-shell-prompt-detect-enabled nil)
+ (python-shell-completion-native-enable nil)
+ (buf (python-shell-make-comint cmd process-name t))
+ (dir (cdr (assoc :pydir params))))
+ (if dir
+ (with-current-buffer buf
+ (setq-local default-directory dir)))
+ (sleep-for 1)
+ (format "*%s*" process-name))))))
+
+;;;###autoload
+(defun +org*org-babel-execute:ipython (body params)
+ "Execute a BODY of IPython code with PARAMS in org-babel.
+This function is called by `org-babel-execute-src-block'."
+ (message default-directory)
+ (let ((session (cdr (assoc :session params))))
+ (org-babel-ipython-initiate-session session params))
+ (ob-ipython--clear-output-buffer)
+ (if (cdr (assoc :async params))
+ (ob-ipython--execute-async body params)
+ (ob-ipython--execute-sync body params)))
+
+
+;;
+;; * org-src-edit
+
+;;;###autoload
+(defun +org*org-babel-edit-prep:ipython (info)
+ (let* ((params (nth 2 info))
+ (session (cdr (assoc :session params))))
+ (org-babel-ipython-initiate-session session params))
+ ;; Support for python.el's "send-code" commands within edit buffers.
+ (setq-local python-shell-buffer-name
+ (format "Python:ob-ipython-%s"
+ (ob-ipython--normalize-session
+ (cdr (assoc :session (nth 2 info))))))
+ (setq-local default-directory
+ (format "%s"
+ (ob-ipython--normalize-session
+ (cdr (assoc :pydir (nth 2 info))))))
+ (ob-ipython-mode 1)
+ ;; hack on company mode to use company-capf rather than company-anaconda
+ (when (featurep! :completion company)
+ (setq-local company-backends
+ '(company-capf
+ company-dabbrev
+ company-files
+ company-yasnippet))
+ (setq-local company-idle-delay nil))
+ (when (featurep 'lpy)
+ (setq lispy-python-proc
+ (format "Python:ob-ipython-%s"
+ (ob-ipython--normalize-session
+ (cdr (assoc :session (nth 2 info)))))
+ lispy--python-middleware-loaded-p nil)
+ (lispy--python-middleware-load)))
+
+
+;;
+;; * retina
+
+(defun +org--ob-ipython-mac-2x-image-file-name (filename &optional scale)
+ "Return the name of high-resolution image file for FILENAME.
+The optional arg SCALE is scale factor, and defaults to 2."
+ (let ((pos (or (string-match "\\.[^./]*\\'" filename) (length filename))))
+ (format "%s@%dx%s"
+ (substring filename 0 pos)
+ (or scale 2)
+ (substring filename pos))))
+
+;;;###autoload
+(defun +org*ob-ipython--write-base64-string (oldfunc &rest args)
+ (let ((file (car args))
+ (b64-string (cdr args)))
+ (let ((file2x (+org--ob-ipython-mac-2x-image-file-name file)))
+ (apply oldfunc file2x b64-string)
+ (shell-command (concat "convert " file2x " -resize 50% " file)))))
diff --git a/modules/lang/org/autoload/org-attach.el b/modules/lang/org/autoload/org-attach.el
index 554549575..c99715ef7 100644
--- a/modules/lang/org/autoload/org-attach.el
+++ b/modules/lang/org/autoload/org-attach.el
@@ -14,35 +14,60 @@
((or "zip" "gz" "tar" "7z" "rar") ?)
(_ ?))))
-;; (defun +org-attach-cleanup ()
-;; ;; "Deletes any attachments that are no longer present in the org-mode buffer."
-;; (let* ((attachments-local (+org-attachments))
-;; (attachments (directory-files org-attach-directory t "^[^.]" t))
-;; (to-delete (cl-set-difference attachments-local attachments)))
-;; ;; TODO
-;; to-delete))
-;; (defun +org-attachments ()
-;; "List all attachments in the current buffer."
-;; (unless (eq major-mode 'org-mode)
-;; (user-error "Not an org buffer"))
-;; (org-save-outline-visibility nil
-;; (let ((attachments '())
-;; element)
-;; (when (and (file-directory-p org-attach-directory)
-;; (> (length (file-expand-wildcards (expand-file-name "*" org-attach-directory))) 0))
-;; (save-excursion
-;; (goto-char (point-min))
-;; (while (progn (org-next-link) (not org-link-search-failed))
-;; (setq element (org-element-context))
-;; (when-let* (file (and (eq (org-element-type element) 'link)
-;; (expand-file-name (org-element-property :path element))))
-;; (when (and (string= (org-element-property :type element) "file")
-;; (string= (concat (file-name-base (directory-file-name (file-name-directory file))) "/")
-;; org-attach-directory)
-;; (file-exists-p file))
-;; (push file attachments))))))
-;; (cl-remove-duplicates attachments))))
+;;
+(defvar +org-attachments nil
+ "A list of all indexed attachments in `org-directory'.")
+
+(defvar +org-attachments-files nil
+ "A list of all attachments in `org-attach-directory'.")
+
+(defun +org-attachments--list (&optional beg end)
+ "Return a list of all attachment file names in the current buffer between BEG
+and END (defaults to `point-min' and `point-max')."
+ (let ((case-fold-search t)
+ attachments)
+ (or end (setq end (point-max)))
+ (org-save-outline-visibility nil
+ (org-with-wide-buffer
+ (goto-char (or beg (point-min)))
+ (while (search-forward "[[attach:" end t)
+ (let* ((context (save-match-data (org-element-context)))
+ (link (expand-file-name (org-link-unescape (org-element-property :path context))
+ org-attach-directory)))
+ (when (and (equal "file" (org-element-property :type context))
+ (file-in-directory-p link org-attach-directory))
+ (push (file-name-nondirectory link) attachments))))))
+ (cl-delete-duplicates attachments :test #'string=)))
+
+;;;###autoload
+(defun +org-attach/sync (arg)
+ "Reindex all attachments in `org-directory' and delete orphaned attachments in
+`org-attach-directory'. If ARG (universal arg), conduct a dry run."
+ (declare (interactive-only t))
+ (interactive "P")
+ (message "Reloading")
+ (setq +org-attachments-files (directory-files org-attach-directory nil "^[^.]" t))
+ (with-temp-buffer
+ (delay-mode-hooks (org-mode))
+ (dolist (org-file (directory-files-recursively org-directory "\\.org$"))
+ (insert-file-contents-literally org-file))
+ (setq +org-attachments (+org-attachments--list)))
+ ;; clean up
+ (let ((deleted 0))
+ (dolist (file (cl-set-difference +org-attachments-files +org-attachments
+ :test #'string=))
+ (message "Deleting orphaned attachment: %s" file)
+ (cl-incf deleted)
+ (unless arg
+ (delete-file (expand-file-name file org-attach-directory))))
+ (message "Buffer's attachments synced (%d deleted)" deleted)))
+
+;;;###autoload
+(defun +org-attach/find-file ()
+ "Open a file from `org-attach-directory'."
+ (interactive)
+ (doom-project-browse org-attach-directory))
;;;###autoload
(defun +org-attach/file (path)
@@ -58,21 +83,22 @@ the cursor."
(unless (eq major-mode 'org-mode)
(user-error "Not in an org buffer"))
(require 'org-download)
- (condition-case ex
- (cond ((string-match-p "^data:image/png;base64," uri)
- (org-download-dnd-base64 uri nil))
- ((image-type-from-file-name uri)
- (org-download-image uri))
- (t
- (let ((new-path (expand-file-name (org-download--fullname uri))))
- ;; Download the file
- (if (string-match-p (concat "^" (regexp-opt '("http" "https" "nfs" "ftp" "file")) ":/") uri)
- (url-copy-file uri new-path)
- (copy-file uri new-path))
- ;; insert the link
- (org-download-insert-link uri new-path))))
- (error
- (user-error "Failed to attach file: %s" (error-message-string ex)))))
+ (let ((raw-uri (url-unhex-string uri)))
+ (condition-case ex
+ (cond ((string-match-p "^data:image/png;base64," uri)
+ (org-download-dnd-base64 uri nil))
+ ((image-type-from-file-name raw-uri)
+ (org-download-image raw-uri))
+ (t
+ (let ((new-path (expand-file-name (org-download--fullname raw-uri))))
+ ;; Download the file
+ (if (string-match-p (concat "^" (regexp-opt '("http" "https" "nfs" "ftp" "file")) ":/") uri)
+ (url-copy-file raw-uri new-path)
+ (copy-file uri new-path))
+ ;; insert the link
+ (org-download-insert-link raw-uri new-path))))
+ (error
+ (user-error "Failed to attach file: %s" (error-message-string ex))))))
;;;###autoload
(defun +org-attach-download-dnd (uri action)
@@ -84,48 +110,36 @@ the cursor."
(copy-alist dnd-protocol-alist))))
(dnd-handle-one-url nil action uri))))
-;;;###autoload
-(defun +org-attach*link-format (filename &optional ext)
- (format "%s%s.%s"
- (file-name-sans-extension filename)
- (format-time-string org-download-timestamp)
- (or ext (file-name-extension filename))))
+
+;;
+;; Advice
;;;###autoload
(defun +org-attach*insert-link (_link filename)
- "TODO"
+ "Produces and inserts a link to FILENAME into the document.
+
+If FILENAME is an image, produce an attach:%s path, otherwise use file:%s (with
+an file icon produced by `+org-attach--icon')."
(if (looking-back "^[ \t]+" (line-beginning-position))
(delete-region (match-beginning 0) (match-end 0))
(newline))
(cond ((image-type-from-file-name filename)
- (when (file-in-directory-p filename org-attach-directory)
- (setq filename (file-relative-name filename +org-dir)))
(insert
- (concat (if (= org-download-image-html-width 0)
- ""
+ (concat (if (= org-download-image-html-width 0) ""
(format "#+attr_html: :width %dpx\n" org-download-image-html-width))
- (if (= org-download-image-latex-width 0)
- ""
+ (if (= org-download-image-latex-width 0) ""
(format "#+attr_latex: :width %dcm\n" org-download-image-latex-width))
- (format org-download-link-format
- (file-relative-name filename (file-name-directory buffer-file-name)))))
+ (cond ((file-in-directory-p filename org-attach-directory)
+ (format "[[attach:%s]]" (file-relative-name filename org-attach-directory)))
+ ((file-in-directory-p filename org-directory)
+ (format org-download-link-format (file-relative-name filename org-directory)))
+ (t
+ (format org-download-link-format filename)))))
(org-display-inline-images))
(t
(insert
(format "%s [[./%s][%s]] "
(+org-attach--icon filename)
- (file-relative-name filename buffer-file-name)
+ (file-relative-name filename (file-name-directory buffer-file-name))
(file-name-nondirectory (directory-file-name filename)))))))
-;;;###autoload
-(defun +org-attach*relative-to-attach-dir (orig-fn &rest args)
- "TODO"
- (if (file-in-directory-p buffer-file-name +org-dir)
- (let* ((context (save-match-data (org-element-context)))
- (file (org-link-unescape (org-element-property :path context)))
- (default-directory
- (if (file-in-directory-p file org-attach-directory)
- +org-dir
- default-directory)))
- (apply orig-fn args))
- (apply orig-fn args)))
diff --git a/modules/lang/org/autoload/org-babel.el b/modules/lang/org/autoload/org-babel.el
deleted file mode 100644
index 8bf723f4e..000000000
--- a/modules/lang/org/autoload/org-babel.el
+++ /dev/null
@@ -1,13 +0,0 @@
-;;; lang/org/autoload/org-babel.el -*- lexical-binding: t; -*-
-;;;###if (featurep! +babel)
-
-;;;###autoload
-(defun +org-babel/edit (arg)
- "Edit the source block at point in a popup.
-
-If ARG is non-nil (universal argument), use the current window."
- (interactive "P")
- (if arg
- (call-interactively #'org-edit-special)
- (with-popup-rules! (("^\\*Org Src" :regexp t :select t :noesc t :same t))
- (call-interactively #'org-edit-special))))
diff --git a/modules/lang/org/autoload/org-capture.el b/modules/lang/org/autoload/org-capture.el
index bd7dce4d4..e11d46d97 100644
--- a/modules/lang/org/autoload/org-capture.el
+++ b/modules/lang/org/autoload/org-capture.el
@@ -1,42 +1,17 @@
;;; lang/org/autoload/org-capture.el -*- lexical-binding: t; -*-
;;;###if (featurep! +capture)
-(when (featurep! :feature evil)
-;;;###autoload (autoload '+org-capture:open "lang/org/autoload/org-capture" nil t)
- (evil-define-operator +org-capture:open (&optional beg end)
- "Evil ex interface to `+org-capture/dwim'."
- :move-point nil :type inclusive
- (interactive "")
- (+org-capture/open
- (unless (or (evil-normal-state-p) (evil-insert-state-p))
- (buffer-substring beg end)))))
+(defvar org-capture-initial)
+
+;;
+;; External frame
;;;###autoload
-(defun +org-capture/open (&optional string key)
- "Sends STRING, the current selection or prompted input to `org-capture'.
-
-Uses the capture template specified by KEY. Otherwise, prompts you for one."
- (interactive)
- (let ((key (or key "n")))
- (if-let* ((string (cond ((not (equal string ""))
- string)
- ((region-active-p)
- (buffer-substring-no-properties
- (region-beginning)
- (region-end))))))
- (org-capture-string string key)
- (org-capture nil key))))
-
-
-;; --- External frame ---------------------
-
-(defvar +org-capture-window-params
+(defvar +org-capture-frame-parameters
`((name . "org-capture")
(width . 70)
(height . 25)
- (window-system . ,(cond (IS-MAC 'ns)
- (IS-LINUX 'x)
- (t 'w32)))
+ (transient . t)
,(if IS-LINUX '(display . ":0")))
"TODO")
@@ -50,33 +25,93 @@ Uses the capture template specified by KEY. Otherwise, prompts you for one."
(defun +org-capture-frame-p (&rest _)
"Return t if the current frame is an org-capture frame opened by
`+org-capture/open-frame'."
- (equal "org-capture" (frame-parameter nil 'name)))
+ (and (equal "org-capture" (frame-parameter nil 'name))
+ (frame-parameter nil 'transient)))
;;;###autoload
(defun +org-capture/open-frame (&optional string key)
"Opens the org-capture window in a floating frame that cleans itself up once
you're done. This can be called from an external shell script."
(interactive)
- (require 'org)
- (let (after-make-frame-functions before-make-frame-hook)
- (let ((frame (if (+org-capture-frame-p)
- (selected-frame)
- (make-frame +org-capture-window-params))))
- (with-selected-frame frame
- (condition-case ex
- (cl-letf (((symbol-function #'pop-to-buffer)
- (symbol-function #'switch-to-buffer)))
- (if (and (stringp string)
- (not (string-empty-p string)))
- (org-capture-string string key)
- (org-capture nil key))
- (when (featurep 'solaire-mode)
- (solaire-mode +1)))
- ('error
- (message "org-capture: %s" (error-message-string ex))
- (delete-frame frame)))))))
+ (when (and string (string-empty-p string))
+ (setq string nil))
+ (when (and key (string-empty-p key))
+ (setq key nil))
+ (let* ((frame-title-format "")
+ (frame (if (+org-capture-frame-p)
+ (selected-frame)
+ (let (before-make-frame-hook after-make-frame-functions)
+ (make-frame +org-capture-frame-parameters)))))
+ (with-selected-frame frame
+ (require 'org-capture)
+ (condition-case ex
+ (cl-letf (((symbol-function #'pop-to-buffer)
+ (symbol-function #'switch-to-buffer)))
+ (switch-to-buffer (doom-fallback-buffer))
+ (let ((org-capture-initial string)
+ org-capture-entry)
+ (when (and key (not (string-empty-p key)))
+ (setq org-capture-entry (org-capture-select-template key)))
+ (if (or org-capture-entry
+ (not (fboundp 'counsel-org-capture)))
+ (org-capture)
+ (unwind-protect
+ (counsel-org-capture)
+ (if-let* ((buf (cl-loop for buf in (buffer-list)
+ if (buffer-local-value 'org-capture-mode buf)
+ return buf)))
+ (with-current-buffer buf
+ (add-hook 'kill-buffer-hook #'+org-capture|cleanup-frame nil t))
+ (delete-frame frame))))))
+ ('error
+ (message "org-capture: %s" (error-message-string ex))
+ (delete-frame frame))))))
;;;###autoload
(defun +org-capture-available-keys ()
"TODO"
(string-join (mapcar #'car org-capture-templates) ""))
+
+
+;;
+;; Capture targets
+
+(defun +org--capture-root (path)
+ (let ((filename (file-name-nondirectory path)))
+ (expand-file-name
+ filename
+ (or (locate-dominating-file (file-truename default-directory)
+ filename)
+ (doom-project-root)
+ (user-error "Couldn't detect a project")))))
+
+;;;###autoload
+(defun +org-capture-todo-file ()
+ "Expand `+org-capture-todo-file' from `org-directory'.
+If it is an absolute path return `+org-capture-todo-file' verbatim."
+ (expand-file-name +org-capture-todo-file org-directory))
+
+;;;###autoload
+(defun +org-capture-notes-file ()
+ "Expand `+org-capture-notes-file' from `org-directory'.
+If it is an absolute path return `+org-capture-todo-file' verbatim."
+ (expand-file-name +org-capture-notes-file org-directory))
+
+;;;###autoload
+(defun +org-capture-project-todo-file ()
+ "Find the nearest `+org-capture-todo-file' in a parent directory, otherwise,
+opens a blank one at the project root. Throws an error if not in a project."
+ (+org--capture-root +org-capture-todo-file))
+
+;;;###autoload
+(defun +org-capture-project-notes-file ()
+ "Find the nearest `+org-capture-notes-file' in a parent directory, otherwise,
+opens a blank one at the project root. Throws an error if not in a project."
+ (+org--capture-root +org-capture-notes-file))
+
+;;;###autoload
+(defun +org-capture-project-changelog-file ()
+ "Find the nearest `+org-capture-changelog-file' in a parent directory,
+otherwise, opens a blank one at the project root. Throws an error if not in a
+project."
+ (+org--capture-root +org-capture-changelog-file))
diff --git a/modules/lang/org/autoload/org-export.el b/modules/lang/org/autoload/org-export.el
new file mode 100644
index 000000000..73ff7ea6f
--- /dev/null
+++ b/modules/lang/org/autoload/org-export.el
@@ -0,0 +1,49 @@
+;;; lang/org/autoload/org-export.el -*- lexical-binding: t; -*-
+;;;###if (featurep! +export)
+
+(defun +org--yank-html-buffer (buffer)
+ (with-current-buffer buffer
+ (require 'ox-clip)
+ (cond ((or IS-WINDOWS IS-MAC)
+ (shell-command-on-region
+ (point-min)
+ (point-max)
+ (cond (IS-WINDOWS ox-clip-w32-cmd)
+ (IS-MAC ox-clip-osx-cmd))))
+ (IS-LINUX
+ (let ((html (buffer-string)))
+ (with-temp-file "/tmp/ox-clip-md.html"
+ (insert html))
+ (apply
+ 'start-process "ox-clip" "*ox-clip*"
+ (split-string ox-clip-linux-cmd " ")))))))
+
+
+;;
+;;; Commands
+
+;;;###autoload
+(defun +org/export-to-clipboard (backend)
+ "Exports the current buffer/selection to the clipboard.
+
+Prompts for what BACKEND to use. See `org-export-backends' for options."
+ (interactive
+ (list (intern (completing-read "Export to: " org-export-backends))))
+ (let ((buffer (org-export-to-buffer backend "*Formatted Copy*" nil nil t t)))
+ (unwind-protect
+ (with-current-buffer buffer
+ (kill-new (buffer-string)))
+ (kill-buffer (current-buffer)))))
+
+;;;###autoload
+(defun +org/export-to-clipboard-as-rich-text (beg end)
+ "Export the current buffer to HTML then copies it to clipboard as rich text.
+
+Supports org-mode, markdown-mode, and gfm-mode buffers. In any other mode,
+htmlize is used (takes what you see in Emacs and converts it to html, text
+properties and font-locking et all)."
+ (interactive "r")
+ (pcase major-mode
+ ((or `markdown-mode `gfm-mode)
+ (+org--yank-html-buffer (markdown)))
+ (_ (ox-clip-formatted-copy beg end))))
diff --git a/modules/lang/org/autoload/org-link.el b/modules/lang/org/autoload/org-link.el
index 8d55ef8d0..62e37f7c0 100644
--- a/modules/lang/org/autoload/org-link.el
+++ b/modules/lang/org/autoload/org-link.el
@@ -13,3 +13,20 @@
(format "%s:%s"
key
(file-relative-name file dir))))
+
+;;;###autoload
+(defun +org-inline-data-image (_protocol link _description)
+ "Interpret LINK as base64-encoded image data."
+ (base64-decode-string link))
+
+;;;###autoload
+(defun +org-image-link (protocol link _description)
+ "Interpret LINK as base64-encoded image data."
+ (when (image-type-from-file-name link)
+ (if-let* ((buf (url-retrieve-synchronously (concat protocol ":" link))))
+ (with-current-buffer buf
+ (goto-char (point-min))
+ (re-search-forward "\r?\n\r?\n" nil t)
+ (buffer-substring-no-properties (point) (point-max)))
+ (message "Download of image \"%s\" failed" link)
+ nil)))
diff --git a/modules/lang/org/autoload/org-present.el b/modules/lang/org/autoload/org-present.el
index 61ca7216d..598291052 100644
--- a/modules/lang/org/autoload/org-present.el
+++ b/modules/lang/org/autoload/org-present.el
@@ -44,7 +44,7 @@
;;;###autoload
(defun +org-present|init-org-tree-window ()
"Set up the org window for presentation."
- (doom/window-zoom)
+ (doom/window-maximize-buffer)
(let ((cwm-use-vertical-padding t)
(cwm-frame-internal-border 110)
(cwm-left-fringe-ratio -10)
@@ -53,7 +53,7 @@
(when (fboundp 'centered-window-mode)
(centered-window-mode arg))
(window-divider-mode (* arg -1))
- (doom-hide-modeline-mode arg)
+ (hide-mode-line-mode arg)
(+org-pretty-mode arg)
(cond (org-tree-slide-mode
(org-indent-mode -1)
diff --git a/modules/lang/org/autoload/org.el b/modules/lang/org/autoload/org.el
index eb351f8cc..e6b74a67b 100644
--- a/modules/lang/org/autoload/org.el
+++ b/modules/lang/org/autoload/org.el
@@ -1,29 +1,56 @@
;;; org/org/autoload/org.el -*- lexical-binding: t; -*-
+(defun +org--get-property (name &optional bound)
+ (save-excursion
+ (let ((re (format "^#\\+%s:[ \t]*\\([^\n]+\\)" (upcase name))))
+ (goto-char (point-min))
+ (when (re-search-forward re bound t)
+ (buffer-substring-no-properties (match-beginning 1) (match-end 1))))))
+
+;;;###autoload
+(defun +org-get-property (name &optional file bound)
+ "Get a document property named NAME (string) from an org FILE (defaults to
+current file). Only scans first 2048 bytes of the document."
+ (unless bound
+ (setq bound 2048))
+ (if file
+ (with-temp-buffer
+ (insert-file-contents-literally file nil 0 bound)
+ (+org--get-property name))
+ (+org--get-property name bound)))
+
+;;;###autoload
+(defun +org-get-todo-keywords-for (keyword)
+ "TODO"
+ (when keyword
+ (cl-loop for (type . keyword-spec) in org-todo-keywords
+ for keywords = (mapcar (lambda (x) (if (string-match "^\\([^(]+\\)(" x)
+ (match-string 1 x)
+ x))
+ keyword-spec)
+ if (eq type 'sequence)
+ if (member keyword keywords)
+ return keywords)))
+
+
+;;
+;;; Modes
+
;;;###autoload
(define-minor-mode +org-pretty-mode
- "TODO"
+ "Hides emphasis markers and toggles pretty entities."
:init-value nil
:lighter " *"
:group 'evil-org
(setq org-hide-emphasis-markers +org-pretty-mode)
(org-toggle-pretty-entities)
- (org-with-silent-modifications
+ (with-silent-modifications
;; In case the above un-align tables
(org-table-map-tables 'org-table-align t)))
-;;;###autoload
-(defun +org|realign-table-maybe ()
- "Auto-align table under cursor and re-calculate formulas."
- (when (org-at-table-p)
- (save-excursion
- (quiet! (org-table-recalculate)))))
-;;;###autoload
-(defun +org|update-cookies ()
- "Update counts in headlines (aka \"cookies\")."
- (when (and buffer-file-name (file-exists-p buffer-file-name))
- (org-update-statistics-cookies t)))
+;;
+;;; Commands
;;;###autoload
(defun +org/dwim-at-point ()
@@ -33,7 +60,8 @@ If on a:
- checkbox list item or todo heading: toggle it.
- clock: update its time.
- headline: toggle latex fragments and inline images underneath.
-- footnote definition: jump to the footnote
+- footnote reference: jump to the footnote's definition
+- footnote definition: jump to the first reference of this footnote
- table-row or a TBLFM: recalculate the table's formulas
- table-cell: clear it and go into insert mode. If this is a formula cell,
recaluclate it instead.
@@ -43,8 +71,7 @@ If on a:
- link: follow it
- otherwise, refresh all inline images in current tree."
(interactive)
- (let* ((scroll-pt (window-start))
- (context (org-element-context))
+ (let* ((context (org-element-context))
(type (org-element-type context)))
;; skip over unimportant contexts
(while (and context (memq type '(verbatim code bold italic underline strike-through subscript superscript)))
@@ -56,20 +83,30 @@ If on a:
(org-toggle-checkbox (if (equal match "[ ]") '(16)))))
(`headline
- (cond ((org-element-property :todo-type context)
+ (cond ((and (fboundp 'toc-org-insert-toc)
+ (member "TOC" (org-get-tags)))
+ (toc-org-insert-toc)
+ (message "Updating table of contents"))
+ ((org-element-property :todo-type context)
(org-todo
- (if (eq (org-element-property :todo-type context) 'done) 'todo 'done)))
+ (if (eq (org-element-property :todo-type context) 'done)
+ (or (car (+org-get-todo-keywords-for (org-element-property :todo-keyword context)))
+ 'todo)
+ 'done)))
((string= "ARCHIVE" (car-safe (org-get-tags)))
(org-force-cycle-archived))
(t
+ (+org/refresh-inline-images)
(org-remove-latex-fragment-image-overlays)
(org-toggle-latex-fragment '(4)))))
(`clock (org-clock-update-time-maybe))
+ (`footnote-reference
+ (org-footnote-goto-definition (org-element-property :label context)))
+
(`footnote-definition
- (goto-char (org-element-property :post-affiliated context))
- (call-interactively #'org-footnote-action))
+ (org-footnote-goto-previous-reference (org-element-property :label context)))
((or `planning `timestamp)
(org-follow-timestamp-link))
@@ -102,68 +139,14 @@ If on a:
(org-toggle-latex-fragment))
(`link
- (let ((path (org-element-property :path (org-element-lineage context '(link) t))))
- (if (and path (image-type-from-file-name path))
+ (let* ((lineage (org-element-lineage context '(link) t))
+ (path (org-element-property :path lineage)))
+ (if (or (equal (org-element-property :type lineage) "img")
+ (and path (image-type-from-file-name path)))
(+org/refresh-inline-images)
(org-open-at-point))))
- (_ (+org/refresh-inline-images)))
- (set-window-start nil scroll-pt)))
-
-;;;###autoload
-(defun +org/indent ()
- "Indent the current item (header or item). Otherwise, forward to
-`self-insert-command'."
- (interactive)
- (cond ((org-at-item-p)
- (org-indent-item-tree))
- ((org-at-heading-p)
- (ignore-errors (org-demote)))
- ((org-in-src-block-p t)
- (doom/dumb-indent))
- (t
- (call-interactively #'self-insert-command))))
-
-;;;###autoload
-(defun +org/indent-or-next-field-or-yas-expand ()
- "Depending on the context either a) indent the current line, b) go the next
-table field or c) run `yas-expand'."
- (interactive)
- (call-interactively
- (cond ((and (bound-and-true-p yas-minor-mode)
- (yas--templates-for-key-at-point))
- #'yas-expand)
- ((org-at-table-p)
- #'org-table-next-field)
- (t
- #'+org/indent))))
-
-;;;###autoload
-(defun +org/dedent ()
- "Dedent the current item (header or item). Otherwise, forward to
-`self-insert-command'."
- (interactive)
- (cond ((org-at-item-p)
- (org-list-indent-item-generic
- -1 nil
- (save-excursion
- (when (org-region-active-p)
- (goto-char (region-beginning)))
- (org-list-struct))))
- ((org-at-heading-p)
- (ignore-errors (org-promote)))
- (t
- (call-interactively #'self-insert-command))))
-
-;;;###autoload
-(defun +org/dedent-or-prev-field ()
- "Depending on the context either dedent the current item or go the previous
-table field."
- (interactive)
- (call-interactively
- (if (org-at-table-p)
- #'org-table-previous-field
- #'+org/dedent)))
+ (_ (+org/refresh-inline-images)))))
;;;###autoload
(defun +org/insert-item (direction)
@@ -174,41 +157,59 @@ I use this instead of `org-insert-item' or `org-insert-heading' which are too
opinionated and perform this simple task incorrectly (e.g. whitespace in the
wrong places)."
(interactive)
- (let* ((context (org-element-lineage
- (org-element-context)
- '(table table-row headline inlinetask item plain-list)
- t))
+ (let* ((context
+ (save-excursion
+ (when (bolp)
+ (back-to-indentation)
+ (forward-char))
+ (org-element-lineage
+ (org-element-context)
+ '(table table-row headline inlinetask item plain-list)
+ t)))
(type (org-element-type context)))
(cond ((memq type '(item plain-list))
(let ((marker (org-element-property :bullet context))
(pad (save-excursion
+ (org-beginning-of-item)
(back-to-indentation)
(- (point) (line-beginning-position)))))
- (pcase direction
- ('below
- (org-end-of-item)
- (goto-char (line-beginning-position))
- (insert (make-string pad 32) (or marker ""))
- (save-excursion (insert "\n")))
- ('above
- (goto-char (line-beginning-position))
- (insert (make-string pad 32) (or marker ""))
- (save-excursion (insert "\n")))))
+ (save-match-data
+ (pcase direction
+ (`below
+ (org-end-of-item)
+ (backward-char)
+ (end-of-line)
+ (if (and marker (string-match "\\([0-9]+\\)\\([).] *\\)" marker))
+ (let ((l (line-number-at-pos)))
+ (org-insert-item)
+ (when (= l (line-number-at-pos))
+ (org-next-item)
+ (org-end-of-line)))
+ (insert "\n" (make-string pad 32) (or marker ""))))
+ (`above
+ (org-beginning-of-item)
+ (if (and marker (string-match-p "[0-9]+[).]" marker))
+ (org-insert-item)
+ (insert (make-string pad 32) (or marker ""))
+ (save-excursion (insert "\n")))))))
(when (org-element-property :checkbox context)
(insert "[ ] ")))
((memq type '(table table-row))
(pcase direction
- ('below (org-table-insert-row t))
- ('above (org-shiftmetadown))))
+ ('below (save-excursion (org-table-insert-row t))
+ (org-table-next-row))
+ ('above (save-excursion (org-shiftmetadown))
+ (+org/table-previous-row))))
((memq type '(headline inlinetask))
(let ((level (if (eq (org-element-type context) 'headline)
(org-element-property :level context)
1)))
(pcase direction
- ('below
- (let ((at-eol (= (point) (1- (line-end-position)))))
+ (`below
+ (let ((at-eol (>= (point) (1- (line-end-position))))
+ org-insert-heading-respect-content)
(goto-char (line-end-position))
(org-end-of-subtree)
(insert (concat "\n"
@@ -218,26 +219,37 @@ wrong places)."
"\n"))
(make-string level ?*)
" "))))
- ('above
+ (`above
(org-back-to-heading)
- (org-insert-heading)
- (when (= level 1)
- (save-excursion (insert "\n")))))
- (when (org-element-property :todo-type context)
- (org-todo 'todo))))
+ (insert (make-string level ?*) " ")
+ (save-excursion
+ (insert "\n")
+ (if (= level 1) (insert "\n")))))
+ (when-let* ((todo-keyword (org-element-property :todo-keyword context)))
+ (org-todo (or (car (+org-get-todo-keywords-for todo-keyword))
+ 'todo)))))
(t (user-error "Not a valid list, heading or table")))
+ (when (org-invisible-p)
+ (org-show-hidden-entry))
(when (bound-and-true-p evil-mode)
- (evil-append-line 1))))
+ (evil-insert 1))))
;;;###autoload
-(defun +org-get-property (name &optional _file) ; TODO Add FILE
- "Get a propery from an org file."
- (save-excursion
- (goto-char 1)
- (re-search-forward (format "^#\\+%s:[ \t]*\\([^\n]+\\)" (upcase name)) nil t)
- (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
+(defun +org/dedent ()
+ "TODO"
+ (interactive)
+ (cond ((org-at-item-p)
+ (org-list-indent-item-generic
+ -1 nil
+ (save-excursion
+ (when (org-region-active-p)
+ (goto-char (region-beginning)))
+ (org-list-struct))))
+ ((org-at-heading-p)
+ (ignore-errors (org-promote)))
+ ((call-interactively #'self-insert-command))))
;;;###autoload
(defun +org/refresh-inline-images ()
@@ -254,33 +266,6 @@ wrong places)."
(line-end-position)
(save-excursion (org-end-of-subtree) (point))))))
-;;;###autoload
-(defun +org/toggle-checkbox ()
- "Toggle the presence of a checkbox in the current item."
- (interactive)
- (org-toggle-checkbox '(4)))
-
-;;;###autoload
-(defun +org/toggle-fold ()
- "Toggle the local fold at the point (as opposed to cycling through all levels
-with `org-cycle'). Also:
-
- + If in a babel block, removes result blocks.
- + If in a table, realign it, if necessary."
- (interactive)
- (save-excursion
- (org-beginning-of-line)
- (cond ((org-at-table-p)
- (org-table-align))
- ((org-in-src-block-p)
- (org-babel-remove-result))
- ((org-at-heading-p)
- (outline-toggle-children))
- ((org-at-item-p)
- (let ((window-beg (window-start)))
- (org-cycle)
- (set-window-start nil window-beg))))))
-
;;;###autoload
(defun +org/remove-link ()
"Unlink the text at point."
@@ -295,3 +280,226 @@ with `org-cycle'). Also:
(apply #'delete-region remove)
(insert description))))
+;;;###autoload
+(defun +org/toggle-checkbox ()
+ "Toggle the presence of a checkbox in the current item."
+ (interactive)
+ (org-toggle-checkbox '(4)))
+
+;;;###autoload
+(defalias #'+org/toggle-fold #'+org|cycle-only-current-subtree)
+
+;;;###autoload
+(defun +org/open-fold ()
+ "Open the current fold (not but its children)."
+ (interactive)
+ (+org/toggle-fold t))
+
+;;;###autoload
+(defalias #'+org/close-fold #'outline-hide-subtree)
+
+(defun +org--get-foldlevel ()
+ (let ((max 1))
+ (save-restriction
+ (narrow-to-region (window-start) (window-end))
+ (save-excursion
+ (goto-char (point-min))
+ (while (not (eobp))
+ (org-next-visible-heading 1)
+ (when (outline-invisible-p (line-end-position))
+ (let ((level (org-outline-level)))
+ (when (> level max)
+ (setq max level))))))
+ max)))
+
+;;;###autoload
+(defun +org/show-next-fold-level ()
+ "Decrease the fold-level of the visible area of the buffer. This unfolds
+another level of headings on each invocation."
+ (interactive)
+ (let* ((current-level (+org--get-foldlevel))
+ (new-level (1+ current-level)))
+ (outline-hide-sublevels new-level)
+ (message "Folded to level %s" new-level)))
+
+;;;###autoload
+(defun +org/hide-next-fold-level ()
+ "Increase the global fold-level of the visible area of the buffer. This folds
+another level of headings on each invocation."
+ (interactive)
+ (let* ((current-level (+org--get-foldlevel))
+ (new-level (max 1 (1- current-level))))
+ (outline-hide-sublevels new-level)
+ (message "Folded to level %s" new-level)))
+
+
+;;
+;;; Hooks
+
+;;;###autoload
+(defun +org|delete-backward-char-and-realign-table-maybe ()
+ "TODO"
+ (when (eq major-mode 'org-mode)
+ (org-check-before-invisible-edit 'delete-backward)
+ (save-match-data
+ (when (and (org-at-table-p)
+ (not (org-region-active-p))
+ (string-match-p "|" (buffer-substring (point-at-bol) (point)))
+ (looking-at-p ".*?|"))
+ (let ((pos (point))
+ (noalign (looking-at-p "[^|\n\r]* |"))
+ (c org-table-may-need-update))
+ (delete-char -1)
+ (unless overwrite-mode
+ (skip-chars-forward "^|")
+ (insert " ")
+ (goto-char (1- pos)))
+ ;; noalign: if there were two spaces at the end, this field
+ ;; does not determine the width of the column.
+ (when noalign (setq org-table-may-need-update c)))
+ t))))
+
+;;;###autoload
+(defun +org|indent-maybe ()
+ "Indent the current item (header or item), if possible.
+Made for `org-tab-first-hook' in evil-mode."
+ (interactive)
+ (cond ((or (not (bound-and-true-p evil-mode))
+ (not (eq evil-state 'insert)))
+ nil)
+ ((org-at-item-p)
+ (if (eq this-command 'org-shifttab)
+ (org-outdent-item-tree)
+ (org-indent-item-tree))
+ t)
+ ((org-at-heading-p)
+ (ignore-errors
+ (if (eq this-command 'org-shifttab)
+ (org-promote)
+ (org-demote)))
+ t)
+ ((org-in-src-block-p t)
+ (org-babel-do-in-edit-buffer
+ (call-interactively #'indent-for-tab-command))
+ t)))
+
+;;;###autoload
+(defun +org|realign-table-maybe ()
+ "Auto-align table under cursor and re-calculate formulas."
+ (when (and (org-at-table-p) org-table-may-need-update)
+ (let ((pt (point))
+ (inhibit-message t))
+ (org-table-recalculate)
+ (if org-table-may-need-update (org-table-align))
+ (goto-char pt))))
+
+;;;###autoload
+(defun +org|update-cookies ()
+ "Update counts in headlines (aka \"cookies\")."
+ (when (and buffer-file-name (file-exists-p buffer-file-name))
+ (let (org-hierarchical-todo-statistics)
+ (org-update-parent-todo-statistics))))
+
+;;;###autoload
+(defun +org|yas-expand-maybe ()
+ "Tries to expand a yasnippet snippet, if one is available. Made for
+`org-tab-first-hook'."
+ (when (bound-and-true-p yas-minor-mode)
+ (cond ((and (or (not (bound-and-true-p evil-mode))
+ (eq evil-state 'insert))
+ (yas--templates-for-key-at-point))
+ (call-interactively #'yas-expand)
+ t)
+ ((use-region-p)
+ (call-interactively #'yas-insert-snippet)
+ t))))
+
+;;;###autoload
+(defun +org|cycle-only-current-subtree (&optional arg)
+ "Toggle the local fold at the point (as opposed to cycling through all levels
+with `org-cycle')."
+ (interactive "P")
+ (unless (eq this-command 'org-shifttab)
+ (save-excursion
+ (org-beginning-of-line)
+ (let (invisible-p)
+ (when (and (org-at-heading-p)
+ (or org-cycle-open-archived-trees
+ (not (member org-archive-tag (org-get-tags))))
+ (or (not arg)
+ (setq invisible-p (outline-invisible-p (line-end-position)))))
+ (unless invisible-p
+ (setq org-cycle-subtree-status 'subtree))
+ (org-cycle-internal-local)
+ t)))))
+
+;;;###autoload
+(defun +org|remove-occur-highlights ()
+ "Remove org occur highlights on ESC in normal mode."
+ (when org-occur-highlights
+ (org-remove-occur-highlights)
+ t))
+
+;;;###autoload
+(defun +org|unfold-to-2nd-level-or-point ()
+ "My version of the 'overview' #+STARTUP option: expand first-level headings.
+Expands the first level, but no further. If point was left somewhere deeper,
+unfold to point on startup."
+ (unless org-agenda-inhibit-startup
+ (when (eq org-startup-folded t)
+ (outline-hide-sublevels 2))
+ (when (outline-invisible-p)
+ (ignore-errors
+ (save-excursion
+ (outline-previous-visible-heading 1)
+ (org-show-subtree))))))
+
+;;;###autoload
+(defun +org|enable-auto-reformat-tables ()
+ "Realign tables & update formulas when exiting insert mode (`evil-mode')."
+ (when (featurep 'evil)
+ (add-hook 'evil-insert-state-exit-hook #'+org|realign-table-maybe nil t)
+ (add-hook 'evil-replace-state-exit-hook #'+org|realign-table-maybe nil t)
+ (advice-add #'evil-replace :after #'+org*realign-table-maybe)))
+
+;;;###autoload
+(defun +org|enable-auto-update-cookies ()
+ "Update statistics cookies when saving or exiting insert mode (`evil-mode')."
+ (when (featurep 'evil)
+ (add-hook 'evil-insert-state-exit-hook #'+org|update-cookies nil t))
+ (add-hook 'before-save-hook #'+org|update-cookies nil t))
+
+
+;;
+;;; Advice
+
+;;;###autoload
+(defun +org*fix-newline-and-indent-in-src-blocks ()
+ "Try to mimic `newline-and-indent' with correct indentation in src blocks."
+ (when (org-in-src-block-p t)
+ (org-babel-do-in-edit-buffer
+ (call-interactively #'indent-for-tab-command))))
+
+;;;###autoload
+(defun +org*realign-table-maybe (&rest _)
+ "Auto-align table under cursor and re-calculate formulas."
+ (when (eq major-mode 'org-mode)
+ (+org|realign-table-maybe)))
+
+;;;###autoload
+(defun +org*evil-org-open-below (orig-fn count)
+ "Fix o/O creating new list items in the middle of nested plain lists. Only has
+an effect when `evil-org-special-o/O' has `item' in it (not the default)."
+ (cl-letf (((symbol-function 'end-of-visible-line)
+ (lambda ()
+ (org-end-of-item)
+ (backward-char 1)
+ (evil-append nil))))
+ (funcall orig-fn count)))
+
+;;;###autoload
+(defun +org*display-link-in-eldoc (orig-fn &rest args)
+ "Display the link at point in eldoc."
+ (or (when-let* ((link (org-element-property :raw-link (org-element-context))))
+ (format "Link: %s" link))
+ (apply orig-fn args)))
diff --git a/modules/lang/org/autoload/tables.el b/modules/lang/org/autoload/tables.el
index 7248f4625..ffe162e51 100644
--- a/modules/lang/org/autoload/tables.el
+++ b/modules/lang/org/autoload/tables.el
@@ -1,12 +1,7 @@
;;; org/org/autoload/tables.el -*- lexical-binding: t; -*-
-;;;###autoload
-(defun +org/table-next-row ()
- "Go to the next row (same column) in the current table."
- (interactive)
- (if (org-at-table-p)
- (org-table-next-row)
- (org-down-element)))
+;;
+;;; Row/Column traversal
;;;###autoload
(defun +org/table-previous-row ()
@@ -14,40 +9,33 @@
re-align the table if necessary. (Necessary because org-mode has a
`org-table-next-row', but not `org-table-previous-row')"
(interactive)
- (if (org-at-table-p)
- (progn
- (org-table-maybe-eval-formula)
- (org-table-maybe-recalculate-line)
- (if (and org-table-automatic-realign
- org-table-may-need-update)
- (org-table-align))
- (let ((col (org-table-current-column)))
- (beginning-of-line 0)
- (when (or (not (org-at-table-p)) (org-at-table-hline-p))
- (beginning-of-line))
- (org-table-goto-column col)
- (skip-chars-backward "^|\n\r")
- (when (org-looking-at-p " ") (forward-char))))
- (org-up-element)))
+ (org-table-maybe-eval-formula)
+ (org-table-maybe-recalculate-line)
+ (if (and org-table-automatic-realign
+ org-table-may-need-update)
+ (org-table-align))
+ (let ((col (org-table-current-column)))
+ (beginning-of-line 0)
+ (when (or (not (org-at-table-p)) (org-at-table-hline-p))
+ (beginning-of-line))
+ (org-table-goto-column col)
+ (skip-chars-backward "^|\n\r")
+ (when (org-looking-at-p " ")
+ (forward-char))))
+
+
+;;
+;;; Row/Column insertion
;;;###autoload
-(defun +org/table-next-field ()
+(defun +org/table-insert-column-left ()
+ "Insert a new column right of the current column."
(interactive)
- (if (org-at-table-p) (org-table-next-field) (org-end-of-line)))
+ (org-table-insert-column)
+ (org-table-move-column-left))
;;;###autoload
-(defun +org/table-previous-field ()
+(defun +org/table-insert-row-below ()
+ "Insert a new row below the current row."
(interactive)
- (if (org-at-table-p) (org-table-previous-field) (org-beginning-of-line)))
-
-;;;###autoload
-(defun +org/table-append-field-or-shift-right ()
- (interactive)
- (org-shiftmetaright)
- (when (org-at-table-p) (org-metaright)))
-
-;;;###autoload
-(defun +org/table-prepend-field-or-shift-left ()
- (interactive)
- (if (org-at-table-p) (org-shiftmetaright) (org-shiftmetaleft)))
-
+ (org-table-insert-row 'below))
diff --git a/modules/lang/org/config.el b/modules/lang/org/config.el
index 1f0096f43..18ee476a3 100644
--- a/modules/lang/org/config.el
+++ b/modules/lang/org/config.el
@@ -1,133 +1,153 @@
;;; lang/org/config.el -*- lexical-binding: t; -*-
-(defvar +org-dir (expand-file-name "~/work/org/")
- "The directory where org files are kept.")
-
-;; Ensure ELPA org is prioritized above built-in org.
-(when-let* ((path (locate-library "org" nil doom--package-load-path)))
- (setq load-path (delete path load-path))
- (push (file-name-directory path) load-path))
-
-;; Sub-modules
-(if (featurep! +attach) (load! +attach))
-(if (featurep! +babel) (load! +babel))
-(if (featurep! +capture) (load! +capture))
-(if (featurep! +export) (load! +export))
-(if (featurep! +present) (load! +present))
-;; TODO (if (featurep! +publish) (load! +publish))
+;; Changed org defaults (should be set before org loads)
+(defvar org-directory "~/org/")
+(defvar org-modules
+ '(org-w3m
+ ;; org-bbdb
+ org-bibtex
+ org-docview
+ ;; org-gnus
+ org-info
+ ;; org-irc
+ ;; org-mhe
+ ;; org-rmail
+ ))
;;
-;; Plugins
-;;
+;;; Packages
-(def-package! toc-org
- :commands toc-org-enable)
+;; `toc-org'
+(setq toc-org-hrefify-default "gh")
+(defun +org*unfold-toc (&rest _)
+ (save-excursion
+ (when (re-search-forward toc-org-toc-org-regexp (point-max) t)
+ (+org/open-fold))))
+(advice-add #'toc-org-insert-toc :before #'+org*unfold-toc)
+
+(def-package! evil-org
+ :when (featurep! :feature evil +everywhere)
+ :hook (org-mode . evil-org-mode)
+ :init
+ (defvar evil-org-key-theme '(navigation insert textobjects))
+ (defvar evil-org-special-o/O '(table-row))
+ (add-hook 'org-load-hook #'+org|setup-evil-keybinds)
+ (add-hook 'evil-org-mode-hook #'evil-normalize-keymaps)
+ :config
+ (add-hook 'org-open-at-point-functions #'evil-set-jump)
+ ;; change `evil-org-key-theme' instead
+ (advice-add #'evil-org-set-key-theme :override #'ignore)
+ (def-package! evil-org-agenda
+ :after org-agenda
+ :config (evil-org-agenda-set-keys)))
+
+(def-package! org-pdfview
+ :when (featurep! :tools pdf)
+ :commands (org-pdfview-open)
+ :init
+ (after! org
+ (delete '("\\.pdf\\'" . default) org-file-apps)
+ ;; org links to pdf files are opened in pdf-view-mode
+ (add-to-list 'org-file-apps '("\\.pdf\\'" . (lambda (_file link) (org-pdfview-open link))))
+ ;; support for links to specific pages
+ (add-to-list 'org-file-apps '("\\.pdf::\\([[:digit:]]+\\)\\'" . (lambda (_file link) (org-pdfview-open link))))))
(def-package! org-crypt ; built-in
- :commands org-crypt-use-before-save-magic
+ :commands org-encrypt-entries
+ :hook (org-reveal-start . org-decrypt-entry)
+ :init
+ (add-hook! 'org-mode-hook
+ (add-hook 'before-save-hook 'org-encrypt-entries nil t))
:config
(setq org-tags-exclude-from-inheritance '("crypt")
org-crypt-key user-mail-address))
-(def-package! org-bullets
- :commands org-bullets-mode)
+(def-package! org-clock ; built-in
+ :commands org-clock-save
+ :hook (org-mode . org-clock-load)
+ :init
+ (setq org-clock-persist 'history
+ org-clock-persist-file (concat doom-etc-dir "org-clock-save.el"))
+ :config
+ (add-hook 'kill-emacs-hook #'org-clock-save))
;;
-;; Bootstrap
-;;
+;;; `org-load' hooks
-(after! org
- ;; Occasionally, Emacs encounters an error loading the built-in org, aborting
- ;; the load. This results in a broken, partially loaded state. This require
- ;; tries to set it straight.
- (require 'org)
-
- (defvaralias 'org-directory '+org-dir)
-
- (org-crypt-use-before-save-magic)
- (+org-init-ui)
- (+org-init-keybinds)
- (+org-hacks))
-
-(add-hook! org-mode
- #'(doom|disable-line-numbers ; no line numbers
- org-bullets-mode ; "prettier" bullets
- org-indent-mode ; margin-based indentation
- toc-org-enable ; auto-table of contents
- visual-line-mode ; line wrapping
-
- +org|enable-auto-reformat-tables
- +org|enable-auto-update-cookies
- +org|smartparens-compatibility-config
- +org|unfold-to-2nd-level-or-point
- +org|show-paren-mode-compatibility
- ))
+(defun +org|setup-agenda ()
+ (unless org-agenda-files
+ (setq org-agenda-files (list org-directory)))
+ (setq-default
+ org-agenda-dim-blocked-tasks nil
+ org-agenda-inhibit-startup t
+ org-agenda-skip-unavailable-files t
+ ;; Move the agenda to show the previous 3 days and the next 7 days for a bit
+ ;; better context instead of just the current week which is a bit confusing
+ ;; on, for example, a sunday
+ org-agenda-span 10
+ org-agenda-start-on-weekday nil
+ org-agenda-start-day "-3d"))
-;;
-;; Config hooks
-;;
+(defun +org|setup-custom-links ()
+ "Set up custom org links."
+ (setq org-link-abbrev-alist
+ '(("github" . "https://github.com/%s")
+ ("youtube" . "https://youtube.com/watch?v=%s")
+ ("google" . "https://google.com/search?q=")
+ ("gimages" . "https://google.com/images?q=%s")
+ ("gmap" . "https://maps.google.com/maps?q=%s")
+ ("duckduckgo" . "https://duckduckgo.com/?q=%s")
+ ("wolfram" . "https://wolframalpha.com/input/?i=%s")
+ ("doom-repo" . "https://github.com/hlissner/doom-emacs/%s")))
-(defun +org|unfold-to-2nd-level-or-point ()
- "My version of the 'overview' #+STARTUP option: expand first-level headings.
-Expands the first level, but no further. If point was left somewhere deeper,
-unfold to point on startup."
- (unless org-agenda-inhibit-startup
- (when (eq org-startup-folded t)
- (outline-hide-sublevels 2))
- (when (outline-invisible-p)
- (ignore-errors
- (save-excursion
- (outline-previous-visible-heading 1)
- (org-show-subtree))))))
+ (defun +org--relpath (path root)
+ (if (and buffer-file-name (file-in-directory-p buffer-file-name root))
+ (file-relative-name path)
+ path))
-(defun +org|smartparens-compatibility-config ()
- "Instruct `smartparens' not to impose itself in org-mode."
- (defun +org-sp-point-in-checkbox-p (_id action _context)
- (when (eq action 'insert)
- (sp--looking-at-p "\\s-*]")))
+ ;; highlight broken links
+ (org-link-set-parameters
+ "file"
+ :face (lambda (path)
+ (if (or (file-remote-p path)
+ (file-exists-p path))
+ 'org-link
+ 'error)))
- ;; make delimiter auto-closing a little more conservative
- (after! smartparens
- (sp-with-modes 'org-mode
- (sp-local-pair "*" nil :unless '(sp-point-after-word-p sp-point-before-word-p sp-point-at-bol-p))
- (sp-local-pair "_" nil :unless '(sp-point-after-word-p sp-point-before-word-p))
- (sp-local-pair "/" nil :unless '(sp-point-after-word-p sp-point-before-word-p +org-sp-point-in-checkbox-p))
- (sp-local-pair "~" nil :unless '(sp-point-after-word-p sp-point-before-word-p))
- (sp-local-pair "=" nil :unless '(sp-point-after-word-p sp-point-before-word-p)))))
+ (defun +org-def-link (key dir)
+ (org-link-set-parameters
+ key
+ :complete (lambda () (+org--relpath (+org-link-read-file key dir) dir))
+ :follow (lambda (link) (find-file (expand-file-name link dir)))
+ :face (lambda (link)
+ (if (file-exists-p (expand-file-name link dir))
+ 'org-link
+ 'error))))
-(defun +org|enable-auto-reformat-tables ()
- "Realign tables exiting insert mode (`evil-mode')."
- (when (featurep 'evil)
- (add-hook 'evil-insert-state-exit-hook #'+org|realign-table-maybe nil t)))
+ (+org-def-link "org" org-directory)
+ (+org-def-link "doom" doom-emacs-dir)
+ (+org-def-link "doom-docs" doom-docs-dir)
+ (+org-def-link "doom-modules" doom-modules-dir)
-(defun +org|enable-auto-update-cookies ()
- "Update statistics cookies when saving or exiting insert mode (`evil-mode')."
- (when (featurep 'evil)
- (add-hook 'evil-insert-state-exit-hook #'+org|update-cookies nil t))
- (add-hook 'before-save-hook #'+org|update-cookies nil t))
-
-(defun +org|show-paren-mode-compatibility ()
- "`show-paren-mode' causes flickering with indentation margins made by
-`org-indent-mode', so we simply turn off show-paren-mode altogether."
- (set (make-local-variable 'show-paren-mode) nil))
+ (def-package! org-yt
+ :config
+ (org-link-set-parameters "http" :image-data-fun #'+org-image-link)
+ (org-link-set-parameters "https" :image-data-fun #'+org-image-link)
+ (org-link-set-parameters "img" :image-data-fun #'+org-inline-data-image)))
-;;
-(defun +org-init-ui ()
+(defun +org|setup-ui ()
"Configures the UI for `org-mode'."
(setq-default
org-adapt-indentation nil
- org-agenda-dim-blocked-tasks nil
- org-agenda-files (directory-files +org-dir t "\\.org$" t)
- org-agenda-inhibit-startup t
- org-agenda-skip-unavailable-files nil
org-cycle-include-plain-lists t
- org-cycle-separator-lines 1
- org-entities-user '(("flat" "\\flat" nil "" "" "266D" "♭") ("sharp" "\\sharp" nil "" "" "266F" "♯"))
- ;; org-ellipsis " ... "
+ org-eldoc-breadcrumb-separator " → "
+ org-entities-user
+ '(("flat" "\\flat" nil "" "" "266D" "♭")
+ ("sharp" "\\sharp" nil "" "" "266F" "♯"))
org-fontify-done-headline t
org-fontify-quote-and-verse-blocks t
org-fontify-whole-heading-line t
@@ -139,127 +159,361 @@ unfold to point on startup."
org-image-actual-width nil
org-indent-indentation-per-level 2
org-indent-mode-turns-on-hiding-stars t
+ org-list-description-max-indent 4
org-pretty-entities nil
org-pretty-entities-include-sub-superscripts t
org-priority-faces
- `((?a . ,(face-foreground 'error))
- (?b . ,(face-foreground 'warning))
- (?c . ,(face-foreground 'success)))
+ '((?a . error)
+ (?b . warning)
+ (?c . success))
+ org-refile-targets
+ '((nil :maxlevel . 3)
+ (org-agenda-files :maxlevel . 3))
org-startup-folded t
org-startup-indented t
org-startup-with-inline-images nil
org-tags-column 0
org-todo-keywords
- '((sequence "[ ](t)" "[-](p)" "[?](m)" "|" "[X](d)")
- (sequence "TODO(T)" "|" "DONE(D)")
- (sequence "NEXT(n)" "ACTIVE(a)" "WAITING(w)" "LATER(l)" "|" "CANCELLED(c)"))
+ '((sequence "TODO(t)" "|" "DONE(d)")
+ (sequence "[ ](T)" "[-](p)" "[?](m)" "|" "[X](D)")
+ (sequence "NEXT(n)" "WAITING(w)" "LATER(l)" "|" "CANCELLED(c)"))
+ org-todo-keyword-faces
+ '(("[-]" :inherit (font-lock-constant-face bold))
+ ("[?]" :inherit (warning bold))
+ ("WAITING" :inherit bold)
+ ("LATER" :inherit (warning bold)))
org-use-sub-superscripts '{}
- outline-blank-line t
- ;; LaTeX previews are too small and usually render to light backgrounds, so
- ;; this enlargens them and ensures their background (and foreground) match the
- ;; current theme.
+ ;; Scale up LaTeX previews a bit (default is too small)
org-preview-latex-image-directory (concat doom-cache-dir "org-latex/")
- org-format-latex-options (plist-put org-format-latex-options :scale 1.5)
- org-format-latex-options
- (plist-put org-format-latex-options
- :background (face-attribute (or (cadr (assq 'default face-remapping-alist))
- 'default)
- :background nil t)))
+ org-format-latex-options (plist-put org-format-latex-options :scale 1.5))
- ;; Custom links
- (org-link-set-parameters
- "org"
- :complete (lambda () (+org-link-read-file "org" +org-dir))
- :follow (lambda (link) (find-file (expand-file-name link +org-dir)))
- :face (lambda (link)
- (if (file-exists-p (expand-file-name link +org-dir))
- 'org-link
- 'error))))
+ (advice-add #'org-eldoc-documentation-function :around #'+org*display-link-in-eldoc)
-(defun +org-init-keybinds ()
+ ;; Don't do automatic indent detection in org files
+ (add-to-list 'doom-detect-indentation-excluded-modes 'org-mode nil #'eq)
+
+ ;; Previews are usually rendered with light backgrounds, so ensure their
+ ;; background (and foreground) match the current theme.
+ (defun +org|update-latex-preview-background-color ()
+ (setq-default
+ org-format-latex-options
+ (plist-put org-format-latex-options
+ :background
+ (face-attribute (or (cadr (assq 'default face-remapping-alist))
+ 'default)
+ :background nil t))))
+ (add-hook 'doom-load-theme-hook #'+org|update-latex-preview-background-color))
+
+
+(defun +org|setup-keybinds ()
"Sets up org-mode and evil keybindings. Tries to fix the idiosyncrasies
between the two."
+ (add-hook 'doom-escape-hook #'+org|remove-occur-highlights)
+
+ ;; C-a & C-e act like `doom/backward-to-bol-or-indent' and
+ ;; `doom/forward-to-last-non-comment-or-eol', but with more org awareness.
+ (setq org-special-ctrl-a/e t)
+
+ (setq org-M-RET-may-split-line nil
+ ;; insert new headings after current subtree rather than inside it
+ org-insert-heading-respect-content t)
+
+ (add-hook! 'org-tab-first-hook #'(+org|indent-maybe +org|yas-expand-maybe))
+ (add-hook 'doom-delete-backward-functions #'+org|delete-backward-char-and-realign-table-maybe)
+
(map! :map org-mode-map
- "RET" #'org-return-indent
+ ;; textmate-esque newline insertion
+ [C-return] (λ! (+org/insert-item 'below))
+ [C-S-return] (λ! (+org/insert-item 'above))
+ (:when IS-MAC
+ [s-return] (λ! (+org/insert-item 'below))
+ [s-S-return] (λ! (+org/insert-item 'above)))
"C-c C-S-l" #'+org/remove-link
- :n "C-c C-i" #'org-toggle-inline-images
+ "C-c C-i" #'org-toggle-inline-images
+ [remap doom/backward-to-bol-or-indent] #'org-beginning-of-line
+ [remap doom/forward-to-last-non-comment-or-eol] #'org-end-of-line
- :n "RET" #'+org/dwim-at-point
+ :localleader
+ "," #'org-switchb
+ "." #'org-goto
+ (:when (featurep! :completion ivy)
+ "." #'counsel-org-goto
+ "/" #'counsel-org-goto-all)
+ (:when (featurep! :completion helm)
+ "." #'helm-org-in-buffer-headings
+ "/" #'helm-org-agenda-files-headings)
+ "d" #'org-deadline
+ "f" #'org-footnote-new
+ "t" #'org-todo
+ "T" #'org-todo-list
+ "l" #'org-insert-link
+ "L" #'org-store-link
+ "r" #'org-refile
+ "s" #'org-schedule
+ "'" #'org-edit-special
+ (:prefix ("c" . "clock")
+ "c" #'org-clock-in
+ "C" #'org-clock-out
+ "d" #'org-clock-mark-default-task
+ "e" #'org-clock-modify-effort-estimate
+ "l" #'org-clock-in-last
+ "g" #'org-clock-goto
+ "G" (λ! (org-clock-goto 'select))
+ "x" #'org-clock-cancel
+ "=" #'org-clock-timestamps-up
+ "-" #'org-clock-timestamps-down)
+ (:prefix ("e" . "export")
+ :desc "to markdown" "m" #'org-md-export-to-markdown
+ :desc "to markdown & open" "M" #'org-md-export-as-markdown
+ :desc "to reveal.js" "r" #'org-reveal-export-to-html
+ :desc "to reveal.js & open" "R" #'org-reveal-export-to-html-and-browse
+ (:prefix ("b" . "from beamer")
+ :desc "to latex" "l" #'org-beamer-export-to-latex
+ :desc "to latex & open" "L" #'org-beamer-export-as-latex
+ :desc "as pdf" "p" #'org-beamer-export-to-pdf))
+ (:prefix ("g" . "goto")
+ "g" #'org-goto
+ (:when (featurep! :completion ivy)
+ "g" #'counsel-org-goto
+ "G" #'counsel-org-goto-all)
+ "a" #'org-agenda-goto
+ "A" #'org-agenda-clock-goto
+ "c" #'org-clock-goto
+ "C" (λ! (org-clock-goto 'select))
+ "i" #'org-id-goto
+ "r" #'org-refile-goto-last-stored
+ "x" #'org-capture-goto-last-stored)
+ (:prefix ("b" . "tables")
+ "a" #'org-table-align
+ "e" #'org-table-edit-field
+ "h" #'org-table-field-info
+ (:prefix ("i" . "insert")
+ "-" #'org-table-insert-hline
+ "h" #'+org/table-insert-column-left
+ "j" #'+org/table-insert-row-below
+ "k" #'org-table-insert-row
+ "l" #'+org/table-insert-column-right)
+ (:prefix ("m" . "move")
+ "h" #'org-table-move-column-left
+ "j" #'org-table-move-row-down
+ "k" #'org-table-move-row-up
+ "l" #'org-table-move-column-right)
+ (:prefix ("f" . "formula")
+ "c" #'org-table-create
+ "r" #'org-table-recalculate
+ "e" #'org-table-edit-formulas
+ "=" #'org-table-eval-formulas))))
- ;; Navigate table cells (from insert-mode)
- :i "C-l" #'+org/table-next-field
- :i "C-h" #'+org/table-previous-field
- :i "C-k" #'+org/table-previous-row
- :i "C-j" #'+org/table-next-row
- ;; Expand tables (or shiftmeta move)
- :ni "C-S-l" #'+org/table-append-field-or-shift-right
- :ni "C-S-h" #'+org/table-prepend-field-or-shift-left
- :ni "C-S-k" #'org-metaup
- :ni "C-S-j" #'org-metadown
- :n [tab] #'+org/toggle-fold
- :i [tab] #'+org/indent-or-next-field-or-yas-expand
- :i [backtab] #'+org/dedent-or-prev-field
+(defun +org|setup-evil-keybinds (&rest args)
+ (unless args ; lookout for recursive requires
+ (require 'evil-org))
- :ni [M-return] (λ! (+org/insert-item 'below))
- :ni [S-M-return] (λ! (+org/insert-item 'above))
+ ;; Only fold the current tree, rather than recursively
+ (add-hook 'org-tab-first-hook #'+org|cycle-only-current-subtree t)
- :m "]]" (λ! (org-forward-heading-same-level nil) (org-beginning-of-line))
- :m "[[" (λ! (org-backward-heading-same-level nil) (org-beginning-of-line))
- :m "]l" #'org-next-link
- :m "[l" #'org-previous-link
- :m "$" #'org-end-of-line
- :m "^" #'org-beginning-of-line
- :n "gQ" #'org-fill-paragraph
- :n "<" #'org-metaleft
- :n ">" #'org-metaright
- :v "<" (λ! (org-metaleft) (evil-visual-restore))
- :v ">" (λ! (org-metaright) (evil-visual-restore))
+ ;; Fix o/O creating new list items in the middle of nested plain lists. Only
+ ;; has an effect when `evil-org-special-o/O' has `item' in it (not the
+ ;; default).
+ (advice-add #'evil-org-open-below :around #'+org*evil-org-open-below)
- ;; Fix code-folding keybindings
- :n "za" #'+org/toggle-fold
- :n "zA" #'org-shifttab
- :n "zc" #'outline-hide-subtree
- :n "zC" (λ! (outline-hide-sublevels 1))
- :n "zd" (lambda (&optional arg) (interactive "p") (outline-hide-sublevels (or arg 3)))
- :n "zm" (λ! (outline-hide-sublevels 1))
- :n "zo" #'outline-show-subtree
- :n "zO" #'outline-show-all
- :n "zr" #'outline-show-all
+ ;; Undo keybinds in `evil-collection-outline'
+ (map! :map outline-mode-map
+ :n "^" nil
+ :n [backtab] nil
+ :n "M-j" nil
+ :n "M-k" nil
+ :n "C-j" nil
+ :n "C-k" nil
+ :n "]" nil
+ :n "[" nil
- (:after org-agenda
- (:map org-agenda-mode-map
- :e "" #'org-agenda-Quit
- :e "m" #'org-agenda-month-view
- :e "C-j" #'org-agenda-next-item
- :e "C-k" #'org-agenda-previous-item
- :e "C-n" #'org-agenda-next-item
- :e "C-p" #'org-agenda-previous-item))))
+ :map evil-org-mode-map
+ :ni [C-return] (λ! (+org/insert-item 'below))
+ :ni [C-S-return] (λ! (+org/insert-item 'above))
+ (:when IS-MAC
+ :ni [s-return] (λ! (+org/insert-item 'below))
+ :ni [s-S-return] (λ! (+org/insert-item 'above)))
+ ;; navigate table cells (from insert-mode)
+ :i "C-l" (general-predicate-dispatch 'org-end-of-line
+ (org-at-table-p) 'org-table-next-field)
+ :i "C-h" (general-predicate-dispatch 'org-beginning-of-line
+ (org-at-table-p) 'org-table-previous-field)
+ :i "C-k" (general-predicate-dispatch 'org-up-element
+ (org-at-table-p) '+org/table-previous-row)
+ :i "C-j" (general-predicate-dispatch 'org-down-element
+ (org-at-table-p) 'org-table-next-row)
+ ;; expanding tables (prepend/append columns/rows)
+ :ni "C-S-l" (general-predicate-dispatch 'org-shiftmetaright
+ (org-at-table-p) 'org-table-insert-column)
+ :ni "C-S-h" (general-predicate-dispatch 'org-shiftmetaleft
+ (org-at-table-p) '+org/table-insert-column-left)
+ :ni "C-S-k" (general-predicate-dispatch 'org-shiftmetaup
+ (org-at-table-p) 'org-table-insert-row)
+ :ni "C-S-j" (general-predicate-dispatch 'org-shiftmetadown
+ (org-at-table-p) '+org/table-insert-row-below)
+ ;; shifting table rows/columns
+ :ni "C-M-S-l" (general-predicate-dispatch 'org-metaright
+ (org-at-table-p) 'org-table-move-column-right)
+ :ni "C-M-S-h" (general-predicate-dispatch 'org-metaleft
+ (org-at-table-p) 'org-table-move-column-left)
+ :ni "C-M-S-k" (general-predicate-dispatch 'org-metaup
+ (org-at-table-p) 'org-table-move-row-up)
+ :ni "C-M-S-j" (general-predicate-dispatch 'org-metadown
+ (org-at-table-p) 'org-table-move-row-down)
+ ;; more intuitive RET keybinds
+ :i [return] #'org-return-indent
+ :i "RET" #'org-return-indent
+ :n [return] #'+org/dwim-at-point
+ :n "RET" #'+org/dwim-at-point
+ ;; more vim-esque org motion keys (not covered by evil-org-mode)
+ :m "]]" (λ! (org-forward-heading-same-level nil) (org-beginning-of-line))
+ :m "[[" (λ! (org-backward-heading-same-level nil) (org-beginning-of-line))
+ :m "]h" #'org-next-visible-heading
+ :m "[h" #'org-previous-visible-heading
+ :m "]l" #'org-next-link
+ :m "[l" #'org-previous-link
+ :m "]c" #'org-babel-next-src-block
+ :m "[c" #'org-babel-previous-src-block
+ :m "^" #'evil-org-beginning-of-line
+ :m "0" (λ! (let (visual-line-mode) (org-beginning-of-line)))
+ :n "gQ" #'org-fill-paragraph
+ ;; sensible vim-esque folding keybinds
+ :n "za" #'+org/toggle-fold
+ :n "zA" #'org-shifttab
+ :n "zc" #'+org/close-fold
+ :n "zC" #'outline-hide-subtree
+ :n "zm" #'+org/hide-next-fold-level
+ :n "zo" #'+org/open-fold
+ :n "zO" #'outline-show-subtree
+ :n "zr" #'+org/show-next-fold-level
+ :n "zR" #'outline-show-all
+ :n "zi" #'org-toggle-inline-images
-;;
-(defun +org-hacks ()
+ :map org-read-date-minibuffer-local-map
+ "C-h" (λ! (org-eval-in-calendar '(calendar-backward-day 1)))
+ "C-l" (λ! (org-eval-in-calendar '(calendar-forward-day 1)))
+ "C-k" (λ! (org-eval-in-calendar '(calendar-backward-week 1)))
+ "C-j" (λ! (org-eval-in-calendar '(calendar-forward-week 1)))
+ "C-S-h" (λ! (org-eval-in-calendar '(calendar-backward-month 1)))
+ "C-S-l" (λ! (org-eval-in-calendar '(calendar-forward-month 1)))
+ "C-S-k" (λ! (org-eval-in-calendar '(calendar-backward-year 1)))
+ "C-S-j" (λ! (org-eval-in-calendar '(calendar-forward-year 1)))))
+
+
+(defun +org|setup-hacks ()
"Getting org to behave."
;; Don't open separate windows
- (push '(file . find-file) org-link-frame-setup)
+ (setf (alist-get 'file org-link-frame-setup) #'find-file)
+ ;; Open directory links in Emacs
+ (add-to-list 'org-file-apps '(directory . emacs))
- ;; Let OS decide what to do with files when opened
- (setq org-file-apps
- `(("\\.org$" . emacs)
- (t . ,(cond (IS-MAC "open -R \"%s\"")
- (IS-LINUX "xdg-open \"%s\"")))))
+ (defun +org|delayed-recenter ()
+ "`recenter', but after a tiny delay. Necessary to prevent certain race
+conditions where a window's buffer hasn't changed at the time this hook is run."
+ (run-at-time 0.1 nil #'recenter))
+ (add-hook 'org-follow-link-hook #'+org|delayed-recenter)
- (defun +org|remove-occur-highlights ()
- "Remove org occur highlights on ESC in normal mode."
- (when (and (derived-mode-p 'org-mode)
- org-occur-highlights)
- (org-remove-occur-highlights)))
- (add-hook '+evil-esc-hook #'+org|remove-occur-highlights)
+ ;; Fix variable height text (e.g. org headings) in the eldoc string
+ (defun +org*strip-properties-from-outline (orig-fn path &optional width prefix separator)
+ (let ((result (funcall orig-fn path width prefix separator))
+ (separator (or separator "/")))
+ (string-join
+ (cl-loop for part
+ in (split-string (substring-no-properties result) separator)
+ for n from 0
+ for face = (nth (% n org-n-level-faces) org-level-faces)
+ collect (org-add-props part nil 'face `(:foreground ,(face-foreground face nil t) :weight bold)))
+ separator)))
+ (advice-add #'org-format-outline-path :around #'+org*strip-properties-from-outline)
- (after! recentf
- ;; Don't clobber recentf with agenda files
- (defun +org-is-agenda-file (filename)
- (cl-find (file-truename filename) org-agenda-files
- :key #'file-truename
- :test #'equal))
- (add-to-list 'recentf-exclude #'+org-is-agenda-file)))
+ ;; Prevent from temporarily-opened agenda buffers from being associated with
+ ;; the current workspace, or being added to recentf. They haven't been opened
+ ;; interactively, so shouldn't be treated as if they were.
+ (defun +org|exclude-agenda-buffers-from-workspace ()
+ (when (and org-agenda-new-buffers (bound-and-true-p persp-mode))
+ (let (persp-autokill-buffer-on-remove)
+ (persp-remove-buffer org-agenda-new-buffers
+ (get-current-persp)
+ nil))))
+ (add-hook 'org-agenda-finalize-hook #'+org|exclude-agenda-buffers-from-workspace)
+
+ (defun +org*exclude-agenda-buffers-from-recentf (orig-fn file)
+ (let ((recentf-exclude (list (lambda (_file) t))))
+ (funcall orig-fn file)))
+ (advice-add #'org-get-agenda-file-buffer
+ :around #'+org*exclude-agenda-buffers-from-recentf))
+
+
+;;
+;;; Bootstrap
+
+(def-package! org
+ :defer-incrementally
+ (calendar find-func format-spec org-macs org-compat org-faces org-entities
+ org-list org-pcomplete org-src org-footnote org-macro ob org org-agenda
+ org-capture)
+ :init
+ (add-hook! 'org-mode-hook
+ #'(org-bullets-mode ; "prettier" bullets
+ org-indent-mode ; margin-based indentation
+ toc-org-enable ; auto-table of contents
+ auto-fill-mode ; line wrapping
+ ;; `show-paren-mode' causes flickering with indentation margins made by
+ ;; `org-indent-mode', so we simply turn off show-paren-mode altogether."
+ doom|disable-show-paren-mode
+
+ +org|enable-auto-reformat-tables
+ +org|enable-auto-update-cookies
+ +org|unfold-to-2nd-level-or-point))
+
+ :config
+ (+org|setup-ui)
+ (+org|setup-agenda)
+ (+org|setup-keybinds)
+ (+org|setup-hacks)
+ (+org|setup-custom-links)
+
+
+ ;; Cross-module configuration
+ (set-popup-rules!
+ '(("^\\*Org Links" :slot -1 :vslot -1 :size 2 :ttl 0)
+ ("^\\*\\(?:Agenda Com\\|Calendar\\|Org \\(?:Export Dispatcher\\|Select\\)\\)"
+ :slot -1 :vslot -1 :size #'+popup-shrink-to-fit :ttl 0)
+ ("^\\*Org Agenda" :size 0.35 :select t :ttl nil)
+ ("^\\*Org Src" :size 0.3 :quit nil :select t :autosave t :ttl nil)
+ ("^CAPTURE.*\\.org$" :size 0.2 :quit nil :select t :autosave t)))
+
+ (set-pretty-symbols! 'org-mode
+ :name "#+NAME:"
+ :src_block "#+BEGIN_SRC"
+ :src_block_end "#+END_SRC")
+
+ (after! smartparens
+ (defun +org-sp-point-in-checkbox-p (_id action _context)
+ (and (eq action 'insert)
+ (sp--looking-at-p "\\s-*]")))
+
+ (defun +org-sp-point-at-bol-p (_id action _context)
+ (and (eq action 'insert)
+ (eq (char-before) ?*)
+ (sp--looking-back-p "^\\**" (line-beginning-position))))
+
+ ;; make delimiter auto-closing a little more conservative
+ (sp-with-modes 'org-mode
+ (sp-local-pair "*" "*" :unless '(:add sp-point-before-word-p +org-sp-point-at-bol-p))
+ (sp-local-pair "_" "_" :unless '(:add sp-point-before-word-p))
+ (sp-local-pair "/" "/" :unless '(:add sp-point-before-word-p +org-sp-point-in-checkbox-p))
+ (sp-local-pair "~" "~" :unless '(:add sp-point-before-word-p))
+ (sp-local-pair "=" "=" :unless '(:add sp-point-before-word-p))))
+
+
+ ;; Sub-modules
+ (if (featurep! +attach) (load! "+attach"))
+ (if (featurep! +babel) (load! "+babel"))
+ (if (featurep! +capture) (load! "+capture"))
+ (if (featurep! +export) (load! "+export"))
+ (if (featurep! +habit) (load! "+habit"))
+ (if (featurep! +present) (load! "+present"))
+ (if (featurep! +protocol) (load! "+protocol")))
diff --git a/modules/lang/org/packages.el b/modules/lang/org/packages.el
index 37f729a8f..8652ea769 100644
--- a/modules/lang/org/packages.el
+++ b/modules/lang/org/packages.el
@@ -1,33 +1,59 @@
;; -*- no-byte-compile: t; -*-
;;; lang/org/packages.el
-(when (version< emacs-version "26.1")
- ;; We want org 9.1.x, but the org packaged with Emacs 25.x and under is 8.x.
- ;; The only secure (and reasonably trustworthy) source for this is via
- ;; emacsmirror. Emacs 26+ comes with Org 9.1.4.
- (package! org-plus-contrib
- :recipe (:fetcher github :repo "emacsmirror/org" :files (:defaults "contrib/lisp/*.el"))))
+;; Installs a cutting-edge version of org-mode
+(package! org-plus-contrib)
-(package! org-bullets :recipe (:fetcher github :repo "hlissner/org-bullets"))
+;; Prevent built-in Org from playing into the byte-compilation of
+;; `org-plus-contrib'.
+(when-let* ((orglib (locate-library "org" nil doom-site-load-path)))
+ (setq load-path (delete (substring (file-name-directory orglib) 0 -1)
+ load-path)))
+;; Ignore org on ELPA, if possible
+(package! org :ignore t)
+
+(package! org-bullets :recipe (:fetcher github :repo "Kaligule/org-bullets"))
+(package! org-yt :recipe (:fetcher github :repo "TobiasZawada/org-yt"))
(package! toc-org)
+(when (featurep! :feature evil)
+ (package! evil-org))
+
+(when (featurep! :tools pdf)
+ (package! org-pdfview))
+
(when (featurep! +attach)
(package! org-download))
(when (featurep! +babel)
- (package! ob-go)
+ (package! ob-async)
(package! ob-mongo)
- (package! ob-redis)
- (package! ob-restclient)
- (package! ob-rust)
(package! ob-sql-mode)
- (package! ob-translate))
+ (package! ob-translate)
+
+ (when (featurep! +ipython)
+ (package! ob-ipython))
+
+ (when (featurep! :lang crystal)
+ (package! ob-crystal))
+ (when (featurep! :lang go)
+ (package! ob-go))
+ (when (featurep! :lang nim)
+ (package! ob-nim))
+ (when (featurep! :lang racket)
+ (package! ob-racket :recipe (:fetcher github :repo "DEADB17/ob-racket")))
+ (when (featurep! :lang rest)
+ (package! ob-restclient))
+ (when (featurep! :lang rust)
+ (package! ob-rust)))
(when (featurep! +export)
- (package! ox-pandoc))
+ (package! ox-clip)
+ (package! ox-pandoc)
+ (package! htmlize))
(when (featurep! +present)
- (package! centered-window-mode)
+ (package! centered-window :recipe (:fetcher github :repo "anler/centered-window-mode"))
(package! org-tree-slide)
(package! ox-reveal))
diff --git a/modules/lang/org/test/autoload-org.el b/modules/lang/org/test/autoload-org.el
deleted file mode 100644
index 567f1abcf..000000000
--- a/modules/lang/org/test/autoload-org.el
+++ /dev/null
@@ -1,42 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; lang/org/test/autoload-org.el
-
-(defmacro should-org-buffer!! (source expected &rest body)
- `(should-buffer!! ,source ,expected
- (org-mode)
- ,@body))
-
-;;
-;; `+org/insert-item'
-(def-test! insert-item-h1
- "Should append/prepend new first-level headers with an extra newline."
- (should-org-buffer!! ("* {0}Header") ("* Header\n\n* {|}")
- (+org/insert-item 'below))
- (should-org-buffer!! ("* {0}Header") ("* {|}\n\n* Header")
- (+org/insert-item 'above)))
-
-(def-test! insert-item-h2
- "Should append/prepend new second-level (and higher) headers without an extra
-newline."
- (should-org-buffer!! ("** {0}Header") ("** Header\n** {|}")
- (+org/insert-item 'below))
- (should-org-buffer!! ("** {0}Header") ("** {|}\n** Header")
- (+org/insert-item 'above)))
-
-(def-test! insert-item-plain-list
- "Should append/prepend new second-level (and higher) headers without an extra
-newline."
- (should-org-buffer!! ("+ {0}List item") ("+ List item\n+ {|}")
- (+org/insert-item 'below))
- (should-org-buffer!! ("+ {0}List item"
- " + Sub item")
- ("+ List item"
- " + Sub item"
- "+ {|}")
- (+org/insert-item 'below))
- (should-org-buffer!! ("+ {0}List item"
- "+ Next item")
- ("+ List item"
- "+ {|}"
- "+ Next item")
- (+org/insert-item 'below)))
diff --git a/modules/lang/org/test/org.el b/modules/lang/org/test/org.el
deleted file mode 100644
index 9b77e67db..000000000
--- a/modules/lang/org/test/org.el
+++ /dev/null
@@ -1,9 +0,0 @@
-;;; lang/org/test/org.el -*- lexical-binding: t; -*-
-
-(when (featurep 'org)
- (unload-feature 'org t))
-(require! :lang org)
-
-(require 'org (locate-library "org" nil doom--package-load-path))
-
-;;
diff --git a/modules/lang/org/test/test-org.el b/modules/lang/org/test/test-org.el
new file mode 100644
index 000000000..40f78bd1e
--- /dev/null
+++ b/modules/lang/org/test/test-org.el
@@ -0,0 +1,145 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/org/test/test-autoload-org.el
+
+(describe "lang/org"
+ ;; `+org/insert-item'
+ (describe "insert-item"
+ (before-all
+ (require 'org)
+ (load! "../autoload/org.el"))
+ (after-all
+ (unload-feature 'org t))
+
+ (before-each
+ (set-buffer (get-buffer-create "org"))
+ (erase-buffer)
+ (delay-mode-hooks (org-mode)))
+ (after-each
+ (kill-buffer (get-buffer "org")))
+
+ (describe "headlines"
+ (it "appends first-level headlines with an extra newline"
+ (insert! "* {0}Header")
+ (+org/insert-item 'below)
+ (expect (eobp))
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal "* Header\n\n* "))
+ (it "prepends first-level headlines with an extra newline"
+ (insert! "* {0}Header")
+ (+org/insert-item 'above)
+ (expect (eolp))
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal "* \n\n* Header"))
+
+ (it "appends second-level headlines with an no extra newline"
+ (insert! "** {0}Header")
+ (+org/insert-item 'below)
+ (expect (eobp))
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal "** Header\n** "))
+ (it "prepends second-level headlines with an no extra newline"
+ (insert! "** {0}Header")
+ (+org/insert-item 'above)
+ (expect (eolp))
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal "** \n** Header"))
+
+ (it "appends headlines, skipping subtrees"
+ (insert! "** {0}First\n"
+ "*** sub 1\n"
+ "*** sub 2\n"
+ "**** subsub 1\n"
+ "** Header")
+ (+org/insert-item 'below)
+ (expect (eolp))
+ (expect (line-number-at-pos) :to-be 5)
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal
+ (string-join '("** First"
+ "*** sub 1"
+ "*** sub 2"
+ "**** subsub 1"
+ "** "
+ "** Header")
+ "\n")))
+ (it "prepends headlines, skipping subtrees"
+ (insert! "** First\n"
+ "*** sub 1\n"
+ "*** sub 2\n"
+ "**** {0}subsub 1\n"
+ "** Header")
+ (+org/insert-item 'above)
+ (expect (eolp))
+ (expect (line-number-at-pos) :to-be 4)
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal
+ (string-join '("** First"
+ "*** sub 1"
+ "*** sub 2"
+ "**** "
+ "**** subsub 1"
+ "** Header")
+ "\n"))))
+
+ (describe "plain lists"
+ (it "appends items"
+ (insert! "+ {0}List item")
+ (+org/insert-item 'below)
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal "+ List item\n+ "))
+ (it "prepends items"
+ (insert! "+ {0}List item")
+ (+org/insert-item 'above)
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal "+ \n+ List item"))
+
+ (it "appends items, but skips over child items"
+ (insert! "+ {0}List item\n"
+ " + Sub item\n"
+ "+ List item")
+ (+org/insert-item 'below)
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal
+ (string-join '("+ List item"
+ " + Sub item"
+ "+ "
+ "+ List item")
+ "\n")))
+ (it "prepends items, but skips over child items"
+ (insert! "+ List item\n"
+ " + Sub item\n"
+ "+ {0}List item")
+ (+org/insert-item 'above)
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal
+ (string-join '("+ List item"
+ " + Sub item"
+ "+ "
+ "+ List item")
+ "\n"))))
+
+ (describe "numbered lists"
+ (it "appends items and updates numbers"
+ (insert! "1. {0}List item\n"
+ "2. Sub item\n"
+ "3. List item")
+ (+org/insert-item 'below)
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal
+ (string-join '("1. List item"
+ "2. "
+ "3. Sub item"
+ "4. List item")
+ "\n")))
+ (it "prepends items and updates numbers"
+ (insert! "1. List item\n"
+ "2. Sub item\n"
+ "3. {0}List item")
+ (+org/insert-item 'above)
+ (expect (buffer-substring-no-properties (point-min) (point-max))
+ :to-equal
+ (string-join '("1. List item"
+ "2. Sub item"
+ "3. "
+ "4. List item")
+ "\n"))))))
diff --git a/modules/lang/perl/config.el b/modules/lang/perl/config.el
index b0b7dec3c..e0768b554 100644
--- a/modules/lang/perl/config.el
+++ b/modules/lang/perl/config.el
@@ -1,14 +1,10 @@
;;; lang/perl/config.el -*- lexical-binding: t; -*-
;; There's also `perl-mode' for perl < 6, which is already set up.
-(when (featurep! :feature syntax-checker)
- (add-hook 'perl-mode-hook #'flycheck-mode))
-
(def-package! perl6-detect)
(def-package! flycheck-perl6
- :when (featurep! :feature syntax-checker)
- :after perl6-mode
- :config (add-hook 'perl6-mode-hook #'flycheck-mode))
+ :when (featurep! :tools flycheck)
+ :after perl6-mode)
diff --git a/modules/lang/perl/packages.el b/modules/lang/perl/packages.el
index a87fd4b8e..e00de4f19 100644
--- a/modules/lang/perl/packages.el
+++ b/modules/lang/perl/packages.el
@@ -3,5 +3,5 @@
(package! perl6-mode)
-(when (featurep! :feature syntax-checker)
+(when (featurep! :tools flycheck)
(package! flycheck-perl6))
diff --git a/modules/lang/php/autoload.el b/modules/lang/php/autoload.el
index fc2fd8ca9..012d7e0b3 100644
--- a/modules/lang/php/autoload.el
+++ b/modules/lang/php/autoload.el
@@ -2,6 +2,17 @@
(defvar +php-composer-conf (make-hash-table :test 'equal))
+;;;###autoload
+(defun +php-company-backend (command &optional arg &rest _ignored)
+ "A delegating company-backend that uses `company-phpactor' if phpactor is
+available and installed, or `php-extras-company' otherwise."
+ (cond ((and (require 'company-phpactor nil t)
+ (ignore-errors (phpactor-find-executable)))
+ (company-phpactor command arg))
+ ((and (require 'php-extras nil t)
+ (file-exists-p (concat php-extras-eldoc-functions-file ".el")))
+ (php-extras-company command arg))))
+
;;;###autoload
(defun +php-composer-conf (&optional project-root refresh-p)
"Retrieve the contents of composer.json as an alist. If REFRESH-P is non-nil
@@ -10,5 +21,17 @@ ignore the cache."
(or (and (not refresh-p) (gethash project-root +php-composer-conf))
(let ((package-file (expand-file-name "composer.json" project-root)))
(when-let* ((data (and (file-exists-p package-file)
+ (require 'json)
(json-read-file package-file))))
(puthash project-root data +php-composer-conf))))))
+
+;;;###autoload
+(defun +php|init-ac-php-core-eldoc ()
+ "Initialize eldoc support for `php-mode' with `ac-php-core'. Fails gracefully
+if phpctags isn't installed."
+ (require 'ac-php-core)
+ (cond ((not ac-php-ctags-executable))
+ ((not (file-exists-p ac-php-ctags-executable))
+ (message "Could not find phpctags executable, eldoc support is disabled")
+ (message "To disable these warnings, set ac-php-ctags-executable to nil"))
+ ((ac-php-core-eldoc-setup))))
diff --git a/modules/lang/php/config.el b/modules/lang/php/config.el
index 3dfc1aa4e..bb09c887e 100644
--- a/modules/lang/php/config.el
+++ b/modules/lang/php/config.el
@@ -1,63 +1,95 @@
;;; lang/php/config.el -*- lexical-binding: t; -*-
-;; (def-package! hack-mode
-;; :mode "\\.hh$"
-;; :config
-;; (set! :company-backend 'hack-mode '(company-capf)))
-
-
(def-package! php-mode
- :mode "\\.php[s345]?$"
- :mode "\\.inc$"
- :interpreter "php"
+ :mode "\\.inc\\'"
:config
- (add-hook! php-mode #'(ac-php-core-eldoc-setup flycheck-mode))
-
+ ;; Disable HTML compatibility in php-mode. `web-mode' has superior support for
+ ;; php+html. Use the .phtml
(setq php-template-compatibility nil)
- (set! :repl 'php-mode #'php-boris)
+ (set-docsets! 'php-mode "PHP" "PHPUnit" "Laravel" "CakePHP" "CodeIgniter" "Doctrine_ORM")
+ (set-repl-handler! 'php-mode #'php-boris)
+ (set-lookup-handlers! 'php-mode :documentation #'php-search-documentation)
+ (set-formatter! 'php-mode #'php-cs-fixer-fix)
- ;; ac-php provides custom autocompletion, php-extras provides autocompletion
- ;; for built-in libraries
- (set! :company-backend 'php-mode '(company-ac-php-backend php-extras-company))
+ (if (featurep! +lsp)
+ (add-hook 'php-mode-hook #'lsp!)
+ ;; `+php-company-backend' uses `company-phpactor', `php-extras-company' or
+ ;; `company-dabbrev-code', in that order.
+ (set-company-backend! 'php-mode '+php-company-backend 'company-dabbrev-code))
- ;; default is 10; this optimizes `smartparens' performance, but limits sp
- ;; pairs to 6 characters.
- (add-hook! php-mode (setq-local sp-max-pair-length 6))
+ ;; Use the smallest `sp-max-pair-length' for optimum `smartparens' performance
+ (setq-hook! 'php-mode-hook sp-max-pair-length 5)
(sp-with-modes '(php-mode)
- (sp-local-pair "/* " "*/" :post-handlers '(("||\n[i] " "RET") ("| " "SPC")))
- (sp-local-pair " " " ?>")
- (sp-local-pair "")
- (sp-local-pair "=" " ?>")
- (sp-local-pair "" "?>" :when '(("RET")) :post-handlers '("||\n[i]"))
- (sp-local-pair "" :when '(("RET")) :post-handlers '("||\n[i]")))
+ (sp-local-pair "" "?>" :post-handlers '(("| " "SPC" "=") ("||\n[i]" "RET") ("[d2]" "p")))
+ (sp-local-pair "" :post-handlers '(("| " "SPC") ("||\n[i]" "RET"))))
- (map! :map php-mode-map
- :localleader
- (:prefix "r"
- :n "cv" #'php-refactor--convert-local-to-instance-variable
- :n "u" #'php-refactor--optimize-use
- :v "xm" #'php-refactor--extract-method
- :n "rv" #'php-refactor--rename-local-variable)
- (:prefix "t"
- :n "r" #'phpunit-current-project
- :n "a" #'phpunit-current-class
- :n "s" #'phpunit-current-test)))
+ (map! :localleader
+ :map php-mode-map
+ :prefix "t"
+ "r" #'phpunit-current-project
+ "a" #'phpunit-current-class
+ "s" #'phpunit-current-test))
+
+
+(def-package! phpactor
+ :unless (featurep! +lsp)
+ :after php-mode
+ :config
+ (set-lookup-handlers! 'php-mode
+ :definition #'phpactor-goto-definition)
+
+ ;; TODO PR these for phpactor.el?
+ ;; company-phpactor breaks company if executable doesn't exist
+ (defun +php*company-phpactor-fail-silently (orig-fn &rest args)
+ (when (phpactor-find-executable)
+ (apply orig-fn args)))
+ (advice-add #'company-phpactor :around #'+php*company-phpactor-fail-silently)
+
+ ;; `phpactor-get-working-dir' throws stringp errors if not in a project.
+ (defun +php*project-root (&rest _)
+ (setq phpactor-working-dir
+ (or phpactor-working-dir
+ (php-project-get-root-dir)
+ (doom-project-root)
+ default-directory)))
+ (advice-add #'phpactor-get-working-dir :before #'+php*project-root)
+
+ (map! :localleader
+ :map php-mode-map
+ :prefix "r"
+ "cc" #'phpactor-copy-class
+ "mc" #'phpactor-move-class
+ "oi" #'phpactor-offset-info
+ "t" #'phpactor-transform
+ "ic" #'phpactor-import-class))
+
+
+(def-package! php-refactor-mode
+ :hook php-mode
+ :config
+ (map! :localleader
+ :map php-refactor-mode-map
+ :prefix "r"
+ "cv" #'php-refactor--convert-local-to-instance-variable
+ "u" #'php-refactor--optimize-use
+ "xm" #'php-refactor--extract-method
+ "rv" #'php-refactor--rename-local-variable))
(def-package! php-extras
:after php-mode
- :init
- ;; company will set up itself
+ :preface
+ ;; We'll set up company support ourselves
(advice-add #'php-extras-company-setup :override #'ignore)
:config
(setq php-extras-eldoc-functions-file
(concat doom-etc-dir "php-extras-eldoc-functions"))
-
;; Make expensive php-extras generation async
(unless (file-exists-p (concat php-extras-eldoc-functions-file ".el"))
(message "Generating PHP eldoc files...")
+ (require 'async)
(async-start `(lambda ()
,(async-inject-variables "\\`\\(load-path\\|php-extras-eldoc-functions-file\\)$")
(require 'php-extras-gen-eldoc)
@@ -67,29 +99,13 @@
(message "PHP eldoc updated!")))))
-(def-package! php-refactor-mode
- :hook php-mode)
-
-
-(def-package! phpunit
- :commands (phpunit-current-test phpunit-current-class phpunit-current-project))
-
-
-(def-package! php-boris :commands php-boris)
-
-
-(def-package! company-php
- :when (featurep! :completion company)
- :commands (company-ac-php-backend ac-php-remake-tags ac-php-remake-tags-all ac-php-core-eldoc-setup)
- :config
- (unless (executable-find "phpctags")
- (warn "php-mode: phpctags isn't installed, auto-completion will be gimped"))
- (setq ac-php-tags-path (concat doom-cache-dir "ac-php/")))
+(def-package! hack-mode
+ :when (featurep! +hack)
+ :mode "\\.hh$")
;;
;; Projects
-;;
(def-project-mode! +php-laravel-mode
:modes (php-mode yaml-mode web-mode nxml-mode js2-mode scss-mode)
@@ -97,5 +113,5 @@
(def-project-mode! +php-composer-mode
:modes (web-mode php-mode)
- :files "composer.json")
+ :files ("composer.json"))
diff --git a/modules/lang/php/packages.el b/modules/lang/php/packages.el
index 5db18500f..61f3ce303 100644
--- a/modules/lang/php/packages.el
+++ b/modules/lang/php/packages.el
@@ -7,10 +7,8 @@
(package! php-refactor-mode)
(package! phpunit)
-(when (featurep! :completion company)
- (package! company-php))
-
-;; (package! hack-mode
-;; :recipe
-;; (:fetcher url :url "https://raw.githubusercontent.com/facebook/hhvm/master/hphp/hack/editor-plugins/emacs/hack-mode.el"))
+(when (featurep! +hack)
+ (package! hack-mode :recipe (:fetcher github :repo "hhvm/hack-mode")))
+(unless (featurep! +lsp)
+ (package! phpactor :recipe (:fetcher github :repo "emacs-php/phpactor.el" :files ("*"))))
diff --git a/modules/lang/plantuml/autoload.el b/modules/lang/plantuml/autoload.el
index 1fbd15a96..171febc8e 100644
--- a/modules/lang/plantuml/autoload.el
+++ b/modules/lang/plantuml/autoload.el
@@ -4,7 +4,7 @@
(defun +plantuml/install ()
"Install plantuml.jar."
(interactive)
- (unless (file-exists-p plantuml-jar-path)
- (user-error "plantuml.jar already installed"))
- (url-copy-file "https://kent.dl.sourceforge.net/project/plantuml/plantuml.jar"
- plantuml-jar-path))
+ (if (file-exists-p plantuml-jar-path)
+ (user-error "plantuml.jar already installed")
+ (url-copy-file "https://kent.dl.sourceforge.net/project/plantuml/plantuml.jar"
+ plantuml-jar-path)))
diff --git a/modules/lang/plantuml/config.el b/modules/lang/plantuml/config.el
index 13353d1ab..f32ba72b1 100644
--- a/modules/lang/plantuml/config.el
+++ b/modules/lang/plantuml/config.el
@@ -1,18 +1,15 @@
;;; lang/plantuml/config.el -*- lexical-binding: t; -*-
(def-package! plantuml-mode
- :mode "\\.p\\(lant\\)?uml$"
+ :defer t
+ :init
+ (setq plantuml-jar-path (concat doom-etc-dir "plantuml.jar")
+ org-plantuml-jar-path plantuml-jar-path)
:config
- (setq plantuml-jar-path (concat doom-etc-dir "plantuml.jar"))
- (set! :popup "*PLANTUML Preview*" :size 25 :noselect t :autokill t)
-
- (unless (executable-find "java")
- (warn "plantuml-mode: can't find java, preview disabled."))
- (unless (file-exists-p plantuml-jar-path)
- (warn "plantuml-mode: can't find plantuml.jar; run M-x +plantuml/install.")))
+ (set-popup-rule! "^\\*PLANTUML" :size 0.4 :select nil :ttl 0))
(def-package! flycheck-plantuml
- :when (featurep! :feature syntax-checker)
+ :when (featurep! :tools flycheck)
:after plantuml-mode
:config (flycheck-plantuml-setup))
diff --git a/modules/lang/plantuml/doctor.el b/modules/lang/plantuml/doctor.el
new file mode 100644
index 000000000..3d3174e25
--- /dev/null
+++ b/modules/lang/plantuml/doctor.el
@@ -0,0 +1,10 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/plantuml/doctor.el
+
+(when (require 'plantuml-mode nil t)
+ ;; java
+ (unless (executable-find "java")
+ (warn! "Couldn't find java. PlantUML preview or syntax checking won't work"))
+ ;; plantuml.jar
+ (unless (file-exists-p plantuml-jar-path)
+ (warn! "Couldn't find plantuml.jar. Install it with-x +plantuml/install")))
diff --git a/modules/lang/plantuml/packages.el b/modules/lang/plantuml/packages.el
index f83696e6c..0335841a7 100644
--- a/modules/lang/plantuml/packages.el
+++ b/modules/lang/plantuml/packages.el
@@ -2,5 +2,5 @@
;;; lang/plantuml/packages.el
(package! plantuml-mode)
-(when (featurep! :feature syntax-checker)
+(when (featurep! :tools flycheck)
(package! flycheck-plantuml))
diff --git a/modules/lang/purescript/config.el b/modules/lang/purescript/config.el
index e4cb6ea54..2cf6fb038 100644
--- a/modules/lang/purescript/config.el
+++ b/modules/lang/purescript/config.el
@@ -1,18 +1,22 @@
;;; lang/purescript/config.el -*- lexical-binding: t; -*-
-(def-package! purescript-mode
- :mode "\\.purs$"
- :config
+(after! purescript-mode
(add-hook! 'purescript-mode-hook
- #'(flycheck-mode purescript-indentation-mode rainbow-delimiters-mode))
+ #'(purescript-indentation-mode
+ rainbow-delimiters-mode))
+ (set-lookup-handlers! 'purescript-mode
+ :definition #'psc-ide-goto-definition
+ :documentation #'purescript-pursuit))
- (load "purescript-mode-autoloads" nil t))
;; (def-package! flycheck-purescript
;; :after purescript-mode
;; :config
;; (add-hook 'flycheck-mode-hook #'flycheck-purescript-setup))
-(def-package! psc-ide
- :hook (purescript-mode . psc-ide-mode))
+(def-package! psc-ide
+ :hook (purescript-mode . psc-ide-mode)
+ :config
+ (remove-hook 'company-backends 'company-psc-ide-backend)
+ (set-company-backend! 'purescript-mode 'company-psc-ide-backend))
diff --git a/modules/lang/python/autoload.el b/modules/lang/python/autoload.el
deleted file mode 100644
index 122ef045a..000000000
--- a/modules/lang/python/autoload.el
+++ /dev/null
@@ -1,7 +0,0 @@
-;;; lang/python/autoload.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun +python/repl ()
- "Open the Python REPL."
- (interactive)
- (process-buffer (run-python nil t t)))
diff --git a/modules/lang/python/autoload/conda.el b/modules/lang/python/autoload/conda.el
new file mode 100644
index 000000000..f0b78b5c2
--- /dev/null
+++ b/modules/lang/python/autoload/conda.el
@@ -0,0 +1,17 @@
+;;; lang/python/autoload/conda.el -*- lexical-binding: t; -*-
+;;;###if (featurep! +conda)
+
+;;;###autoload
+(defun +python/set-conda-home ()
+ "Set `conda-anaconda-home' (ANACONDA_HOME).
+
+Usually it's `~/.anaconda3' on local machine, but it can be set to a remote
+directory using TRAMP syntax, e.g. `/ssh:host:/usr/bin/anaconda3'. This way, you
+can use a remote conda environment, including the corresponding remote python
+executable and packages."
+ (interactive)
+ (require 'conda)
+ (when-let* ((home (read-directory-name "Set conda home: " "~" nil nil conda-anaconda-home)))
+ (setq conda-anaconda-home home)
+ (message "Successfully changed conda home to: %s" (abbreviate-file-name home))))
+
diff --git a/modules/lang/python/autoload/python.el b/modules/lang/python/autoload/python.el
new file mode 100644
index 000000000..84fcbcecd
--- /dev/null
+++ b/modules/lang/python/autoload/python.el
@@ -0,0 +1,54 @@
+;;; lang/python/autoload/python.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +python/open-repl ()
+ "Open the Python REPL."
+ (interactive)
+ (unless python-shell-interpreter
+ (user-error "`python-shell-interpreter' isn't set"))
+ (pop-to-buffer
+ (process-buffer
+ (if-let* ((pipenv (+python-executable-find "pipenv"))
+ (pipenv-project (pipenv-project-p)))
+ (let ((default-directory pipenv-project)
+ (python-shell-interpreter-args
+ (format "run %s %s"
+ python-shell-interpreter
+ python-shell-interpreter-args))
+ (python-shell-interpreter pipenv))
+ (run-python nil t t))
+ (run-python nil t t)))))
+
+;;;###autoload
+(defun +python/open-ipython-repl ()
+ "Open an IPython REPL."
+ (interactive)
+ (let ((python-shell-interpreter (or (+python-executable-find "ipython") "ipython"))
+ (python-shell-interpreter-args (string-join +python-ipython-repl-args " ")))
+ (+python/open-repl)))
+
+;;;###autoload
+(defun +python/open-jupyter-repl ()
+ "Open a Jupyter console."
+ (interactive)
+ (add-to-list 'python-shell-completion-native-disabled-interpreters "jupyter")
+ (let ((python-shell-interpreter (or (+python-executable-find "jupyter") "jupyter"))
+ (python-shell-interpreter-args (format "console %s" (string-join +python-jupyter-repl-args " "))))
+ (+python/open-repl)))
+
+;;;###autoload
+(defun +python-executable-find (exe)
+ "TODO"
+ (if (file-name-absolute-p exe)
+ (file-executable-p exe)
+ (let ((exe-root (format "bin/%s" exe)))
+ (cond ((when python-shell-virtualenv-root
+ (let ((bin (expand-file-name exe-root python-shell-virtualenv-root)))
+ (if (file-exists-p bin) bin))))
+ ((when (require 'conda nil t)
+ (let ((bin (expand-file-name (concat conda-env-current-name "/" exe-root)
+ (conda-env-location))))
+ (if (file-executable-p bin) bin))))
+ ((when-let* ((bin (projectile-locate-dominating-file default-directory "bin/python")))
+ (setq-local doom-modeline-python-executable (expand-file-name "bin/python" bin))))
+ ((executable-find exe))))))
diff --git a/modules/lang/python/config.el b/modules/lang/python/config.el
index 1b2ff7983..403071e64 100644
--- a/modules/lang/python/config.el
+++ b/modules/lang/python/config.el
@@ -1,128 +1,205 @@
;;; lang/python/config.el -*- lexical-binding: t; -*-
-(defvar +python-pyenv-root nil
- "The path to pyenv's root directory. This is automatically set when `python'
-is loaded.")
+(defvar +python-ipython-repl-args '("-i" "--simple-prompt" "--no-color-info")
+ "CLI arguments to initialize ipython with when `+python/open-ipython-repl' is
+called.")
-(defvar +python-pyenv-versions nil
- "Available versions of python in pyenv.")
-
-(defvar-local +python-current-version nil
- "The currently active pyenv version.")
+(defvar +python-jupyter-repl-args '("--simple-prompt")
+ "CLI arguments to initialize 'jupiter console %s' with when
+`+python/open-ipython-repl' is called.")
;;
-;; Plugins
-;;
+;; Packages
(def-package! python
- :commands python-mode
+ :defer t
:init
(setq python-environment-directory doom-cache-dir
- python-indent-guess-indent-offset-verbose nil
- python-shell-interpreter "python")
+ python-indent-guess-indent-offset-verbose nil)
:config
- (add-hook! 'python-mode-hook #'(flycheck-mode highlight-numbers-mode))
+ (set-electric! 'python-mode :chars '(?:))
+ (set-repl-handler! 'python-mode #'+python/open-repl)
- (set! :repl 'python-mode #'+python/repl)
- (set! :electric 'python-mode :chars '(?:))
+ (set-pretty-symbols! 'python-mode
+ ;; Functional
+ :def "def"
+ :lambda "lambda"
+ ;; Types
+ :null "None"
+ :true "True" :false "False"
+ :int "int" :str "str"
+ :float "float"
+ :bool "bool"
+ :tuple "tuple"
+ ;; Flow
+ :not "not"
+ :in "in" :not-in "not in"
+ :and "and" :or "or"
+ :for "for"
+ :return "return" :yield "yield")
- (when (executable-find "ipython")
- (setq python-shell-interpreter "ipython"
- python-shell-interpreter-args "-i --simple-prompt --no-color-info"
- python-shell-prompt-regexp "In \\[[0-9]+\\]: "
- python-shell-prompt-block-regexp "\\.\\.\\.\\.: "
- python-shell-prompt-output-regexp "Out\\[[0-9]+\\]: "
- python-shell-completion-setup-code
- "from IPython.core.completerlib import module_completion"
- python-shell-completion-string-code
- "';'.join(get_ipython().Completer.all_completions('''%s'''))\n"))
-
- ;; Version management with pyenv
- (defun +python|add-version-to-modeline ()
- "Add version string to the major mode in the modeline."
- (setq mode-name
- (if +python-current-version
- (format "Python %s" +python-current-version)
- "Python")))
- (add-hook 'python-mode-hook #'+python|add-version-to-modeline)
-
- (if (not (executable-find "pyenv"))
- (setq +python-current-version (string-trim (shell-command-to-string "python --version 2>&1 | cut -d' ' -f2")))
- (setq +python-pyenv-root (string-trim (shell-command-to-string "pyenv root"))
- +python-pyenv-versions (split-string (shell-command-to-string "pyenv versions --bare") "\n" t))
-
- (defun +python|detect-pyenv-version ()
- "Detect the pyenv version for the current project and set the relevant
-environment variables."
- (when-let* ((version-str (shell-command-to-string "python --version 2>&1 | cut -d' ' -f2")))
- (setq version-str (string-trim version-str)
- +python-current-version version-str)
- (let ((pyenv-current-path (concat +python-pyenv-root "/versions/" version-str)))
- (when (file-directory-p pyenv-current-path)
- (setq pythonic-environment pyenv-current-path)))
- (when (member version-str +python-pyenv-versions)
- (setenv "PYENV_VERSION" version-str))))
- (add-hook 'python-mode-hook #'+python|detect-pyenv-version))
+ (when (featurep! +lsp)
+ (add-hook 'python-mode-local-vars-hook #'lsp!))
(define-key python-mode-map (kbd "DEL") nil) ; interferes with smartparens
- (sp-with-modes 'python-mode
- (sp-local-pair "'" nil :unless '(sp-point-before-word-p sp-point-after-word-p sp-point-before-same-p))))
+ (sp-local-pair 'python-mode "'" nil
+ :unless '(sp-point-before-word-p
+ sp-point-after-word-p
+ sp-point-before-same-p))
+
+ ;; Affects pyenv and conda
+ (advice-add #'pythonic-activate :after-while #'+modeline|update-env-in-all-windows)
+ (advice-add #'pythonic-deactivate :after #'+modeline|clear-env-in-all-windows)
+
+ (setq-hook! 'python-mode-hook tab-width python-indent-offset))
(def-package! anaconda-mode
- :after python
- :hook python-mode
+ :unless (featurep! +lsp)
+ :hook python-mode-local-vars
:init
(setq anaconda-mode-installation-directory (concat doom-etc-dir "anaconda/")
anaconda-mode-eldoc-as-single-line t)
:config
(add-hook 'anaconda-mode-hook #'anaconda-eldoc-mode)
- (set! :popup "*anaconda-mode*" :size 10 :noselect t :autoclose t :autokill t)
- (map! :map anaconda-mode-map :m "gd" #'anaconda-mode-find-definitions)
- (advice-add #'anaconda-mode-doc-buffer :after #'doom*anaconda-mode-doc-buffer))
-
-
-(def-package! company-anaconda
- :when (featurep! :completion company)
- :after anaconda-mode
- :config
- (set! :company-backend 'python-mode '(company-anaconda))
- (set! :jump 'python-mode
+ (set-company-backend! 'anaconda-mode '(company-anaconda))
+ (set-lookup-handlers! 'anaconda-mode
:definition #'anaconda-mode-find-definitions
- :references #'anaconda-mode-find-referenences
+ :references #'anaconda-mode-find-references
:documentation #'anaconda-mode-show-doc)
- (map! :map python-mode-map
- :localleader
+ (set-popup-rule! "^\\*anaconda-mode" :select nil)
+
+ (defun +python|auto-kill-anaconda-processes ()
+ "Kill anaconda processes if this buffer is the last python buffer."
+ (when (and (eq major-mode 'python-mode)
+ (not (delq (current-buffer)
+ (doom-buffers-in-mode 'python-mode (buffer-list)))))
+ (anaconda-mode-stop)))
+ (add-hook! 'python-mode-hook
+ (add-hook 'kill-buffer-hook #'+python|auto-kill-anaconda-processes nil t))
+
+ (when (featurep 'evil)
+ (add-hook 'anaconda-mode-hook #'evil-normalize-keymaps))
+ (map! :localleader
+ :map anaconda-mode-map
:prefix "f"
- :nv "d" #'anaconda-mode-find-definitions
- :nv "h" #'anaconda-mode-show-doc
- :nv "a" #'anaconda-mode-find-assignments
- :nv "f" #'anaconda-mode-find-file
- :nv "u" #'anaconda-mode-find-references))
-
-
-(def-package! pip-requirements
- :mode ("/requirements.txt$" . pip-requirements-mode))
+ "d" #'anaconda-mode-find-definitions
+ "h" #'anaconda-mode-show-doc
+ "a" #'anaconda-mode-find-assignments
+ "f" #'anaconda-mode-find-file
+ "u" #'anaconda-mode-find-references))
(def-package! nose
:commands nose-mode
- :preface
- (defvar nose-mode-map (make-sparse-keymap))
- :init
- (associate! nose-mode :match "/test_.+\\.py$" :modes (python-mode))
+ :preface (defvar nose-mode-map (make-sparse-keymap))
+ :init (associate! nose-mode :match "/test_.+\\.py$" :modes (python-mode))
:config
- (set! :popup "*nosetests*" :size 0.4 :noselect t)
- (set! :yas-minor-mode 'nose-mode)
- (map! :map nose-mode-map
- :localleader
- :prefix "t"
- :n "r" #'nosetests-again
- :n "a" #'nosetests-all
- :n "s" #'nosetests-one
- :n "v" #'nosetests-module
- :n "A" #'nosetests-pdb-all
- :n "O" #'nosetests-pdb-one
- :n "V" #'nosetests-pdb-module))
+ (set-popup-rule! "^\\*nosetests" :size 0.4 :select nil)
+ (set-yas-minor-mode! 'nose-mode)
+ (when (featurep 'evil)
+ (add-hook 'nose-mode-hook #'evil-normalize-keymaps))
+ (map! :localleader
+ :map nose-mode-map
+ :prefix "t"
+ "r" #'nosetests-again
+ "a" #'nosetests-all
+ "s" #'nosetests-one
+ "v" #'nosetests-module
+ "A" #'nosetests-pdb-all
+ "O" #'nosetests-pdb-one
+ "V" #'nosetests-pdb-module))
+
+
+(def-package! python-pytest
+ :defer t
+ :init
+ (map! :after python
+ :localleader
+ :map python-mode-map
+ :prefix "t"
+ "f" #'python-pytest-file
+ "k" #'python-pytest-file-dwim
+ "m" #'python-pytest-repeat
+ "p" #'python-pytest-popup))
+
+
+;;
+;; Environment management
+
+(def-package! pipenv
+ :commands pipenv-project-p
+ :hook (python-mode . pipenv-mode)
+ :init (setq pipenv-with-projectile nil)
+ :config
+ (set-eval-handler! 'python-mode
+ '((:command . (lambda () python-shell-interpreter))
+ (:exec (lambda ()
+ (if-let* ((bin (executable-find "pipenv"))
+ (_ (pipenv-project-p)))
+ (format "PIPENV_MAX_DEPTH=9999 %s run %%c %%o %%s %%a" bin)
+ "%c %o %s %a")))
+ (:description . "Run Python script"))))
+
+
+(def-package! pyvenv
+ :after python
+ :init
+ (when (featurep! :ui modeline)
+ (add-hook 'pyvenv-post-activate-hooks #'+modeline|update-env-in-all-windows)
+ (add-hook 'pyvenv-pre-deactivate-hooks #'+modeline|clear-env-in-all-windows))
+ :config
+ (add-hook 'hack-local-variables-hook #'pyvenv-track-virtualenv)
+ (add-to-list 'global-mode-string
+ '(pyvenv-virtual-env-name (" venv:" pyvenv-virtual-env-name " "))
+ 'append))
+
+
+(def-package! pyenv-mode
+ :when (featurep! +pyenv)
+ :after python
+ :config
+ (pyenv-mode +1)
+ (when (executable-find "pyenv")
+ (add-to-list 'exec-path (expand-file-name "shims" (or (getenv "PYENV_ROOT") "~/.pyenv")))))
+
+
+(def-package! conda
+ :when (featurep! +conda)
+ :after python
+ :config
+ ;; The location of your anaconda home will be guessed from the following:
+ ;;
+ ;; + `conda-anaconda-home's default value:
+ ;; + ANACONDA_HOME
+ ;; + ~/.anaconda3
+ ;; + ~/.anaconda
+ ;; + ~/.miniconda
+ ;; + ~/usr/bin/anaconda3
+ ;; + ~/usr/local/anaconda3
+ ;; + ~/usr/local/miniconda3
+ ;;
+ ;; If none of these work for you, you must set `conda-anaconda-home'
+ ;; explicitly. Once set, run M-x `conda-env-activate' to switch between
+ ;; environments
+ (unless (cl-loop for dir in (list conda-anaconda-home
+ "~/.anaconda"
+ "~/.miniconda"
+ "~/.miniconda3"
+ "/usr/bin/anaconda3"
+ "/usr/local/anaconda3"
+ "/usr/local/miniconda3")
+ if (file-directory-p dir)
+ return (setq conda-anaconda-home dir
+ conda-env-home-directory dir))
+ (message "Cannot find Anaconda installation"))
+
+ ;; integration with term/eshell
+ (conda-env-initialize-interactive-shells)
+ (after! eshell (conda-env-initialize-eshell))
+
+ (add-to-list 'global-mode-string
+ '(conda-env-current-name (" conda:" conda-env-current-name " "))
+ 'append))
diff --git a/modules/lang/python/doctor.el b/modules/lang/python/doctor.el
new file mode 100644
index 000000000..9dc7c4e12
--- /dev/null
+++ b/modules/lang/python/doctor.el
@@ -0,0 +1,18 @@
+;;; lang/python/doctor.el -*- lexical-binding: t; -*-
+
+(unless (executable-find "python")
+ (warn! "Python isn't installed."))
+
+(when (featurep! +pyenv)
+ (if (not (executable-find "pyenv"))
+ (warn! "Couldn't find pyenv in your PATH")
+ (unless (split-string (shell-command-to-string "pyenv versions --bare") "\n" t)
+ (warn! "No versions of python are available via pyenv, did you forget to install one?"))))
+
+(when (featurep! +conda)
+ (unless (executable-find "conda")
+ (warn! "Couldn't find conda in your PATH")))
+
+(when (featurep! +ipython)
+ (unless (executable-find "ipython")
+ (warn! "Couldn't find ipython in your PATH")))
diff --git a/modules/lang/python/packages.el b/modules/lang/python/packages.el
index 8ed17820e..193f5d4bd 100644
--- a/modules/lang/python/packages.el
+++ b/modules/lang/python/packages.el
@@ -1,11 +1,22 @@
;; -*- no-byte-compile: t; -*-
;;; lang/python/packages.el
-;; requires: python jedi setuptools
+;; requires: python setuptools
-(package! anaconda-mode)
(package! nose)
+(package! python-pytest)
(package! pip-requirements)
-(when (featurep! :completion company)
- (package! company-anaconda))
+;; Environmet management
+(package! pipenv)
+(package! pyvenv)
+(when (featurep! +pyenv)
+ (package! pyenv-mode))
+(when (featurep! +conda)
+ (package! conda))
+
+;; Programming environment
+(unless (featurep! +lsp)
+ (package! anaconda-mode)
+ (when (featurep! :completion company)
+ (package! company-anaconda)))
diff --git a/modules/lang/qt/autoload.el b/modules/lang/qt/autoload.el
new file mode 100644
index 000000000..ba5db146c
--- /dev/null
+++ b/modules/lang/qt/autoload.el
@@ -0,0 +1,4 @@
+;;; lang/qt/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(add-to-list 'auto-mode-alist '("\\.pr[io]\\'" . qt-pro-mode))
diff --git a/modules/lang/qt/packages.el b/modules/lang/qt/packages.el
new file mode 100644
index 000000000..a41b28abb
--- /dev/null
+++ b/modules/lang/qt/packages.el
@@ -0,0 +1,5 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/qt/packages.el
+
+(package! qml-mode)
+(package! qt-pro-mode)
diff --git a/modules/lang/racket/autoload.el b/modules/lang/racket/autoload.el
new file mode 100644
index 000000000..c1d071aa3
--- /dev/null
+++ b/modules/lang/racket/autoload.el
@@ -0,0 +1,12 @@
+;;; lang/racket/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +racket/repl ()
+ "Open the Racket REPL."
+ (interactive)
+ (pop-to-buffer
+ (or (get-buffer "*Racket REPL*")
+ (progn (racket-run-and-switch-to-repl)
+ (let ((buf (get-buffer "*Racket REPL*")))
+ (bury-buffer buf)
+ buf)))))
diff --git a/modules/lang/racket/config.el b/modules/lang/racket/config.el
new file mode 100644
index 000000000..7359baebb
--- /dev/null
+++ b/modules/lang/racket/config.el
@@ -0,0 +1,52 @@
+;;; lang/racket/config.el -*- lexical-binding: t; -*-
+
+(def-package! racket-mode
+ :hook (racket-repl-mode . racket-unicode-input-method-enable)
+ :config
+ (set-popup-rule! "^\\*Racket REPL" :size 10 :select t)
+ (set-repl-handler! 'racket-mode #'+racket/repl)
+ (set-lookup-handlers! 'racket-mode
+ :definition #'racket-visit-definition
+ :documentation #'racket-describe)
+ (set-docsets! 'racket-mode "Racket")
+ (set-pretty-symbols! 'racket-mode
+ :lambda "lambda"
+ :map "map"
+ :dot ".")
+ (set-rotate-patterns! 'racket-mode
+ :symbols '(("#true" "#false")))
+
+ (setq racket-smart-open-bracket-enable t)
+
+ (add-hook! racket-mode #'(rainbow-delimiters-mode highlight-quoted-mode))
+
+ (map! :localleader
+ :map racket-mode-map
+ "a" #'racket-align
+ "A" #'racket-unalign
+ "f" #'racket-fold-all-tests
+ "F" #'racket-unfold-all-tests
+ "h" #'racket-doc
+ "i" #'racket-unicode-input-method-enable
+ "l" #'racket-logger
+ "o" #'racket-profile
+ "p" #'racket-cycle-paren-shapes
+ "r" #'racket-run
+ "R" #'racket-run-and-switch-to-repl
+ "t" #'racket-test
+ "u" #'racket-backward-up-list
+ "y" #'racket-insert-lambda
+ (:prefix "e"
+ "d" #'racket-expand-definition
+ "l" #'racket-expand-last-sexp
+ "r" #'racket-expand-region
+ "a" #'racket-expand-again)
+ (:prefix "g"
+ "d" #'racket-visit-definition
+ "m" #'racket-visit-module
+ "r" #'racket-open-require-path
+ "b" #'racket-unvisit)
+ (:prefix "s"
+ "d" #'racket-send-definition
+ "l" #'racket-send-last-sexp
+ "r" #'racket-send-region)))
diff --git a/modules/lang/racket/doctor.el b/modules/lang/racket/doctor.el
new file mode 100644
index 000000000..cabeb0d55
--- /dev/null
+++ b/modules/lang/racket/doctor.el
@@ -0,0 +1,11 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/racket/doctor.el
+
+(unless (executable-find "drracket")
+ (warn! "Could not find drracket executable; code-completion and syntax checking will not work."))
+
+(unless (executable-find "racket")
+ (warn! "Could not find racket executable; REPL will not work."))
+
+(unless (executable-find "raco")
+ (warn! "Could not find raco executable; commands for install packages and build libraries will not work."))
diff --git a/modules/lang/racket/packages.el b/modules/lang/racket/packages.el
new file mode 100644
index 000000000..bfab32f65
--- /dev/null
+++ b/modules/lang/racket/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/racket/packages.el
+
+(package! racket-mode)
diff --git a/modules/lang/rest/autoload.el b/modules/lang/rest/autoload.el
new file mode 100644
index 000000000..0562764b0
--- /dev/null
+++ b/modules/lang/rest/autoload.el
@@ -0,0 +1,28 @@
+;;; lang/rest/autoload.el -*- lexical-binding: t; -*-
+
+(defun +rest-request-at-point-p (&optional pos)
+ (save-excursion
+ (if pos (goto-char pos))
+ (beginning-of-line)
+ (and (re-search-forward restclient-method-url-regexp
+ (line-end-position) t)
+ (not (nth 4 (syntax-ppss))))))
+
+;;;###autoload
+(defun +rest/dwim-at-point ()
+ "TODO"
+ (interactive)
+ (when (+rest-request-at-point-p)
+ (restclient-http-send-current-stay-in-window)))
+
+;;;###autoload
+(defun +rest/fold-all ()
+ "TODO"
+ (interactive)
+ (save-excursion
+ (goto-char (point-min))
+ (let ((last (point)))
+ (while (and (restclient-jump-next)
+ (not (= last (setq last (point)))))
+ (unless (overlays-at (line-end-position))
+ (restclient-toggle-body-visibility))))))
diff --git a/modules/lang/rest/config.el b/modules/lang/rest/config.el
index 4af59d251..a9b3d67e2 100644
--- a/modules/lang/rest/config.el
+++ b/modules/lang/rest/config.el
@@ -1,19 +1,33 @@
;;; lang/rest/config.el -*- lexical-binding: t; -*-
(def-package! restclient
- :commands restclient-mode
- :mode ("\\.http$" . restclient-mode)
+ :mode ("\\.http\\'" . restclient-mode)
:config
- (set! :popup "*HTTP Response*" :size 30 :select t :noesc t :autokill t)
- (map! :mode restclient-mode
- :n [M-return] 'restclient-http-send-current
+ (set-popup-rule! "^\\*HTTP Response" :size 0.4 :quit 'other)
+
+ ;; line numbers aren't enabled by default in fundamental-mode-derived modes
+ (add-hook 'restclient-mode-hook #'display-line-numbers-mode)
+
+ ;; Forces underlying SSL verification to prompt for self-signed or invalid
+ ;; certs, rather than silently reject them.
+ (defun +rest*permit-self-signed-ssl (orig-fn &rest args)
+ (let (gnutls-verify-error tls-checktrust)
+ (apply orig-fn args)))
+ (advice-add #'restclient-http-do :around #'+rest*permit-self-signed-ssl)
+
+ (map! :map restclient-mode-map
+ :n [return] #'+rest/dwim-at-point
+ :n "za" #'restclient-toggle-body-visibility
+ :n "zm" #'+rest/fold-all
+ :n "zr" #'outline-show-all
+
:localleader
- :desc "Execute HTTP request" :n "e" 'restclient-http-send-current
- :desc "Execute raw HTTP request" :n "E" 'restclient-http-send-current-raw
- :desc "Copy curl command" :n "c" 'restclient-copy-curl-command))
+ "e" #'restclient-http-send-current
+ "E" #'restclient-http-send-current-raw
+ "c" #'restclient-copy-curl-command))
(def-package! company-restclient
:when (featurep! :completion company)
:after restclient
- :config (set! :company-backend 'restclient-mode '(company-restclient)))
+ :config (set-company-backend! 'restclient-mode 'company-restclient))
diff --git a/modules/lang/ruby/autoload.el b/modules/lang/ruby/autoload.el
new file mode 100644
index 000000000..8f9519283
--- /dev/null
+++ b/modules/lang/ruby/autoload.el
@@ -0,0 +1,15 @@
+;;; lang/ruby/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +ruby|cleanup-robe-servers ()
+ "Clean up dangling inf robe processes if there are no more `enh-ruby-mode'
+buffers open."
+ ;; FIXME This should wait X seconds before cleaning up
+ (unless (or (not robe-mode) (doom-buffers-in-mode 'enh-ruby-mode))
+ (let (inf-buffer kill-buffer-query-functions)
+ (while (setq inf-buffer (robe-inf-buffer))
+ (let ((process (get-buffer-process inf-buffer))
+ confirm-kill-processes)
+ (when (processp process)
+ (kill-process (get-buffer-process inf-buffer))
+ (kill-buffer inf-buffer)))))))
diff --git a/modules/lang/ruby/config.el b/modules/lang/ruby/config.el
index 38de8171e..b2cba8e06 100644
--- a/modules/lang/ruby/config.el
+++ b/modules/lang/ruby/config.el
@@ -1,84 +1,118 @@
;;; lang/ruby/config.el -*- lexical-binding: t; -*-
-(defvar +ruby-rbenv-versions nil
- "Available versions of ruby in rbenv.")
-
-(defvar-local +ruby-current-version nil
- "The currently active ruby version.")
-
-
-;;
-;; Plugins
;;
+;; Packages
-(def-package! ruby-mode
- :mode "\\.rb$"
- :mode "\\.rake$"
- :mode "\\.gemspec$"
- :mode "\\.\\(pry\\|irb\\)rc$"
- :mode "/\\(Gem\\|Cap\\|Vagrant\\|Rake\\|Pod\\|Puppet\\|Berks\\)file$"
- :interpreter "ruby"
+(def-package! enh-ruby-mode
+ :mode ("\\.\\(?:pry\\|irb\\)rc\\'" . +ruby|init)
+ :mode ("\\.\\(?:rb\\|rake\\|rabl\\|ru\\|builder\\|gemspec\\|jbuilder\\|thor\\)\\'" . +ruby|init)
+ :mode ("/\\(?:Berks\\|Cap\\|Gem\\|Guard\\|Pod\\|Puppet\\|Rake\\|Thor\\|Vagrant\\)file\\'" . +ruby|init)
+ :preface
+ (after! ruby-mode
+ (require 'enh-ruby-mode))
+ (defun +ruby|init ()
+ "Enable `enh-ruby-mode' if ruby is available, otherwise `ruby-mode'."
+ (if (executable-find "ruby")
+ (enh-ruby-mode)
+ (ruby-mode)))
:config
- (set! :company-backend 'ruby-mode '(company-dabbrev-code))
- (set! :electric 'ruby-mode :words '("else" "end" "elseif"))
- (setq ruby-deep-indent-paren t)
- ;; Don't interfere with my custom RET behavior
- (define-key ruby-mode-map [?\n] nil)
+ (set-electric! '(ruby-mode enh-ruby-mode) :words '("else" "end" "elsif"))
+ (set-repl-handler! '(ruby-mode enh-ruby-mode) #'inf-ruby)
- (add-hook 'ruby-mode-hook #'flycheck-mode)
+ (when (featurep! +lsp)
+ (add-hook 'enh-ruby-mode-hook #'lsp!))
- ;; Version management with rbenv
- (defun +ruby|add-version-to-modeline ()
- "Add version string to the major mode in the modeline."
- (setq mode-name
- (if +python-current-version
- (format "Ruby %s" +ruby-current-version)
- "Ruby")))
- (add-hook 'ruby-mode-hook #'+ruby|add-version-to-modeline)
+ (after! company-dabbrev-code
+ (add-to-list 'company-dabbrev-code-modes 'enh-ruby-mode nil #'eq)
+ (add-to-list 'company-dabbrev-code-modes 'ruby-mode nil #'eq))
- (if (not (executable-find "rbenv"))
- (setq +ruby-current-version (string-trim (shell-command-to-string "ruby --version 2>&1 | cut -d' ' -f2")))
- (setq +ruby-rbenv-versions (split-string (shell-command-to-string "rbenv versions --bare") "\n" t))
+ ;; so class and module pairs work
+ (setq-hook! (ruby-mode enh-ruby-mode) sp-max-pair-length 6))
- (defun +ruby|detect-rbenv-version ()
- "Detect the rbenv version for the current project and set the relevant
-environment variables."
- (when-let* ((version-str (shell-command-to-string "ruby --version 2>&1 | cut -d' ' -f2")))
- (setq version-str (string-trim version-str)
- +ruby-current-version version-str)
- (when (member version-str +ruby-rbenv-versions)
- (setenv "RBENV_VERSION" version-str))))
- (add-hook 'ruby-mode-hook #'+ruby|detect-rbenv-version))
- (map! :map ruby-mode-map
+(def-package! robe
+ :unless (featurep! +lsp)
+ :hook (enh-ruby-mode . robe-mode)
+ :config
+ (set-repl-handler! 'enh-ruby-mode #'robe-start)
+ (set-company-backend! 'enh-ruby-mode 'company-robe)
+ (set-lookup-handlers! 'enh-ruby-mode
+ :definition #'robe-jump
+ :documentation #'robe-doc)
+ (map! :localleader
+ :map robe-mode-map
+ "'" #'robe-start
+ ;; robe mode specific
+ "h" #'robe-doc
+ "rr" #'robe-rails-refresh
+ ;; inf-enh-ruby-mode
+ :prefix "s"
+ "f" #'ruby-send-definition
+ "F" #'ruby-send-definition-and-go
+ "r" #'ruby-send-region
+ "R" #'ruby-send-region-and-go
+ "i" #'ruby-switch-to-inf))
+
+
+;; NOTE Must be loaded before `robe-mode'
+(def-package! yard-mode
+ :hook (ruby-mode enh-ruby-mode))
+
+
+(def-package! rubocop
+ :hook (enh-ruby-mode . rubocop-mode)
+ :config
+ (map! :localleader
+ :map rubocop-mode-map
+ "f" #'rubocop-check-current-file
+ "F" #'rubocop-autocorrect-current-file
+ "p" #'rubocop-check-project
+ "P" #'rubocop-autocorrect-project))
+
+
+;;
+;; Package & Ruby version management
+
+(def-package! rake
+ :defer t
+ :init
+ (setq rake-cache-file (concat doom-cache-dir "rake.cache"))
+ (map! :after enh-ruby-mode
:localleader
- :prefix "r"
- :nv "b" #'ruby-toggle-block
- :nv "ec" #'ruby-refactor-extract-constant
- :nv "el" #'ruby-refactor-extract-to-let
- :nv "em" #'ruby-refactor-extract-to-method
- :nv "ev" #'ruby-refactor-extract-local-variable
- :nv "ad" #'ruby-refactor-add-parameter
- :nv "cc" #'ruby-refactor-convert-post-conditional))
+ :map enh-ruby-mode-map
+ :prefix "k"
+ "k" #'rake
+ "r" #'rake-rerun
+ "R" #'rake-regenerate-cache
+ "f" #'rake-find-task))
+
+(def-package! bundler
+ :defer t
+ :init
+ (map! :after enh-ruby-mode
+ :localleader
+ :map enh-ruby-mode-map
+ :prefix "b"
+ "c" #'bundle-check
+ "C" #'bundle-console
+ "i" #'bundle-install
+ "u" #'bundle-update
+ "e" #'bundle-exec
+ "o" #'bundle-open))
+
+;;;###package `rvm'
+(setq rspec-use-rvm t)
+
+(after! rbenv
+ (add-to-list 'exec-path (expand-file-name "shims" rbenv-installation-dir)))
-(def-package! ruby-refactor
- :commands
- (ruby-refactor-extract-to-method ruby-refactor-extract-local-variable
- ruby-refactor-extract-constant ruby-refactor-add-parameter
- ruby-refactor-extract-to-let ruby-refactor-convert-post-conditional))
-
-
-;; Highlight doc comments
-(def-package! yard-mode :hook ruby-mode)
-
+;;
+;; Testing frameworks
(def-package! rspec-mode
- :mode ("/\\.rspec$" . text-mode)
+ :mode ("/\\.rspec\\'" . text-mode)
:init
- (associate! rspec-mode :match "/\\.rspec$")
- (associate! rspec-mode :modes (ruby-mode yaml-mode) :files ("/spec/"))
- (defvar rspec-mode-verifiable-map (make-sparse-keymap))
(defvar evilmi-ruby-match-tags
'((("unless" "if") ("elsif" "else") "end")
("begin" ("rescue" "ensure") "end")
@@ -86,28 +120,41 @@ environment variables."
(("class" "def" "while" "do" "module" "for" "until") () "end")
;; Rake
(("task" "namespace") () "end")))
+
+ (when (featurep! :feature evil)
+ (add-hook 'rspec-mode-hook #'evil-normalize-keymaps))
:config
- (map! :map rspec-mode-map
- :localleader
+ (map! :localleader
:prefix "t"
- :n "r" #'rspec-rerun
- :n "a" #'rspec-verify-all
- :n "s" #'rspec-verify-single
- :n "v" #'rspec-verify))
+ :map (rspec-verifiable-mode-map rspec-dired-mode-map rspec-mode-map)
+ "a" #'rspec-verify-all
+ "r" #'rspec-rerun
+ :map (rspec-verifiable-mode-map rspec-mode-map)
+ "v" #'rspec-verify
+ "c" #'rspec-verify-continue
+ "l" #'rspec-run-last-failed
+ "T" #'rspec-toggle-spec-and-target
+ "t" #'rspec-toggle-spec-and-target-find-example
+ :map rspec-verifiable-mode-map
+ "f" #'rspec-verify-method
+ "m" #'rspec-verify-matching
+ :map rspec-mode-map
+ "s" #'rspec-verify-single
+ "e" #'rspec-toggle-example-pendingness
+ :map rspec-dired-mode-map
+ "v" #'rspec-dired-verify
+ "s" #'rspec-dired-verify-single))
-(def-package! inf-ruby
- :commands (inf-ruby inf-ruby-console-auto)
- :init (set! :repl 'ruby-mode 'inf-ruby))
-
-
-(def-package! company-inf-ruby
- :when (featurep! :completion company)
- :after inf-ruby
- :config (set! :company-backend 'inf-ruby-mode '(company-inf-ruby)))
-
-
-(def-package! rake
- :commands (rake rake-find-task rake-rerun)
- :config (setq rake-completion-system 'default))
-
+(def-package! minitest
+ :defer t
+ :config
+ (when (featurep! :feature evil)
+ (add-hook 'minitest-mode-hook #'evil-normalize-keymaps))
+ (map! :localleader
+ :map minitest-mode-map
+ :prefix "t"
+ "r" #'minitest-rerun
+ "a" #'minitest-verify-all
+ "s" #'minitest-verify-single
+ "v" #'minitest-verify))
diff --git a/modules/lang/ruby/doctor.el b/modules/lang/ruby/doctor.el
new file mode 100644
index 000000000..7050b4f28
--- /dev/null
+++ b/modules/lang/ruby/doctor.el
@@ -0,0 +1,8 @@
+;;; lang/ruby/doctor.el -*- lexical-binding: t; -*-
+
+(unless (executable-find "ruby")
+ (warn! "Ruby isn't installed."))
+
+(when (executable-find "rbenv")
+ (unless (split-string (shell-command-to-string "rbenv versions --bare") "\n" t)
+ (warn! "No versions of ruby are available via rbenv, did you forget to install one?")))
diff --git a/modules/lang/ruby/packages.el b/modules/lang/ruby/packages.el
index 5328f19fa..3c010a6e6 100644
--- a/modules/lang/ruby/packages.el
+++ b/modules/lang/ruby/packages.el
@@ -3,12 +3,26 @@
;; requires ruby ruby-lint
-(package! inf-ruby)
-(package! rspec-mode)
-(package! ruby-refactor)
+(package! enh-ruby-mode)
(package! yard-mode)
-(package! rake)
+(package! inf-ruby)
+(unless (featurep! +lsp)
+ (package! robe))
(when (featurep! :completion company)
(package! company-inf-ruby))
+;; Project tools
+(package! bundler)
+(package! rake)
+(package! rubocop)
+
+;; Version management
+(when (featurep! +rbenv)
+ (package! rbenv))
+(when (featurep! +rvm)
+ (package! rvm))
+
+;; Testing frameworks
+(package! rspec-mode)
+(package! minitest)
diff --git a/modules/lang/rust/autoload.el b/modules/lang/rust/autoload.el
index 2b94b95de..b52ddae2b 100644
--- a/modules/lang/rust/autoload.el
+++ b/modules/lang/rust/autoload.el
@@ -5,4 +5,10 @@
;;;###autoload
(defun +rust-cargo-project-p ()
"Return t if this is a cargo project."
- (doom-project-has! "Cargo.toml"))
+ (locate-dominating-file buffer-file-name "Cargo.toml"))
+
+;;;###autoload
+(defun +rust-cargo-compile (command)
+ "TODO"
+ (let ((default-directory (+rust-cargo-project-p)))
+ (compile command)))
diff --git a/modules/lang/rust/config.el b/modules/lang/rust/config.el
index bde7c4602..1b8fceb9a 100644
--- a/modules/lang/rust/config.el
+++ b/modules/lang/rust/config.el
@@ -1,49 +1,32 @@
;;; lang/rust/config.el -*- lexical-binding: t; -*-
-(defvar +rust-src-dir (concat doom-etc-dir "rust/")
- "The path to Rust source library. Required by racer.")
+(after! rust-mode
+ (set-docsets! 'rust-mode "Rust")
+ (setq rust-indent-method-chain t)
+ (when (featurep! +lsp)
+ (add-hook 'rust-mode-hook #'lsp!))
-;;
-;; Plugins
-;;
-
-(def-package! rust-mode
- :mode "\\.rs$"
- :config
- (def-menu! +rust/build-menu
- "TODO"
- '(("run" :exec "cargo run" :cwd t :when (+rust-cargo-project-p))
- ("build" :exec "cargo build" :cwd t :when (+rust-cargo-project-p)))
- :prompt "Cargo: "))
+ (map! :map rust-mode-map
+ :localleader
+ :prefix "b"
+ :desc "cargo build" "b" (λ! (+rust-cargo-compile "cargo build --color always"))
+ :desc "cargo check" "c" (λ! (+rust-cargo-compile "cargo check --color always"))
+ :desc "cargo run" "r" (λ! (+rust-cargo-compile "cargo run --color always"))
+ :desc "cargo test" "t" (λ! (+rust-cargo-compile "cargo test --color always"))))
(def-package! racer
+ :unless (featurep! +lsp)
:after rust-mode
- :hook (rust-mode . racer-mode)
:config
- (add-hook 'rust-mode-hook #'eldoc-mode)
-
- (setq racer-cmd (or (executable-find "racer")
- (expand-file-name "racer/target/release/racer" +rust-src-dir))
- racer-rust-src-path (or (getenv "RUST_SRC_PATH")
- (expand-file-name "rust/src/" +rust-src-dir)))
-
- (unless (file-exists-p racer-cmd)
- (warn "rust-mode: racer binary can't be found; auto-completion is disabled"))
-
- (set! :jump 'rust-mode :definition #'racer-find-definition))
-
-
-(def-package! company-racer
- :when (featurep! :completion company)
- :after racer
- :config (set! :company-backend 'rust-mode '(company-racer)))
+ (add-hook 'rust-mode-hook #'racer-mode)
+ (set-lookup-handlers! 'rust-mode :async t
+ :definition #'racer-find-definition
+ :documentation #'racer-describe))
(def-package! flycheck-rust
- :when (featurep! :feature syntax-checker)
+ :when (featurep! :tools flycheck)
:after rust-mode
- :hook (flycheck-mode . flycheck-rust-setup)
- :config (add-hook 'rust-mode-hook #'flycheck-mode))
-
+ :config (add-hook 'rust-mode-hook #'flycheck-rust-setup))
diff --git a/modules/lang/rust/doctor.el b/modules/lang/rust/doctor.el
new file mode 100644
index 000000000..79ef3dadb
--- /dev/null
+++ b/modules/lang/rust/doctor.el
@@ -0,0 +1,10 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/rust/doctor.el
+
+(when (require 'racer nil t)
+ ;; racer
+ (unless (file-exists-p racer-cmd)
+ (warn! "Couldn't find the racer binary at `racer-cmd'"))
+ ;; rust source code (rustup component add rust-src)
+ (unless (file-directory-p racer-rust-src-path)
+ (warn! "Couldn't find Rust's source code at RUST_SRC_PATH or `racer-rust-src-path'.")))
diff --git a/modules/lang/rust/packages.el b/modules/lang/rust/packages.el
index cf4244060..96421d159 100644
--- a/modules/lang/rust/packages.el
+++ b/modules/lang/rust/packages.el
@@ -3,11 +3,10 @@
;; requires rust cargo racer
-(package! racer)
(package! rust-mode)
-(when (featurep! :feature syntax-checker)
+(when (featurep! :tools flycheck)
(package! flycheck-rust))
-(when (featurep! :completion company)
- (package! company-racer))
+(unless (featurep! +lsp)
+ (package! racer))
diff --git a/modules/lang/scala/config.el b/modules/lang/scala/config.el
index b656ebbf6..ff3b9cb63 100644
--- a/modules/lang/scala/config.el
+++ b/modules/lang/scala/config.el
@@ -1,28 +1,34 @@
;;; lang/scala/config.el -*- lexical-binding: t; -*-
-(def-package! scala-mode
- :mode "\\.s\\(cala\\|bt\\)$"
- :config (setq scala-indent:align-parameters t))
-
-
-(def-package! sbt-mode :after scala-mode)
+(after! scala-mode
+ (setq scala-indent:align-parameters t)
+ (after! dtrt-indent
+ (add-to-list 'dtrt-indent-hook-mapping-list '(scala-mode c/c++/java scala-indent:step))))
(def-package! ensime
- :commands (ensime ensime-scala-mode-hook)
- :hook (scala-mode . ensime-mode)
+ :unless (featurep! +lsp)
+ :defer t
:config
- (add-hook 'ensime-mode-hook #'eldoc-mode)
-
- (set! :company-backend 'scala-mode '(ensime-company company-yasnippet))
-
(setq ensime-startup-snapshot-notification nil
ensime-startup-notification nil
ensime-eldoc-hints 'all
;; let DOOM handle company setup
ensime-completion-style nil)
+ (set-company-backend! 'scala-mode '(ensime-company company-yasnippet))
+
;; Fix void-variable imenu-auto-rescan error caused by `ensime--setup-imenu'
;; trying to make imenu variables buffer local before imenu is loaded.
(require 'imenu))
+
+(def-package! sbt-mode
+ :after scala-mode
+ :config (set-repl-handler! 'scala-mode #'run-scala))
+
+
+(def-package! lsp-scala
+ :when (featurep! +lsp)
+ :after scala-mode
+ :init (add-hook 'scala-mode-hook #'lsp!))
diff --git a/modules/lang/scala/packages.el b/modules/lang/scala/packages.el
index c458f6851..83207fe79 100644
--- a/modules/lang/scala/packages.el
+++ b/modules/lang/scala/packages.el
@@ -1,6 +1,9 @@
;; -*- no-byte-compile: t; -*-
;;; lang/scala/packages.el
-(package! ensime)
(package! sbt-mode)
(package! scala-mode)
+
+(if (featurep! +lsp)
+ (package! lsp-scala)
+ (package! ensime))
diff --git a/modules/lang/sh/README.org b/modules/lang/sh/README.org
index 782344eb9..8f7b62a43 100644
--- a/modules/lang/sh/README.org
+++ b/modules/lang/sh/README.org
@@ -8,10 +8,10 @@ This module adds support for shell scripting languages.
+ REPL support
* Table of Contents :TOC:
-- [[#install][Install]]
- - [[#dependencies][Dependencies]]
-- [[#appendix][Appendix]]
- - [[#commands][Commands]]
+- [[Install][Install]]
+ - [[Dependencies][Dependencies]]
+- [[Appendix][Appendix]]
+ - [[Commands][Commands]]
* Install
** Dependencies
diff --git a/modules/lang/sh/autoload.el b/modules/lang/sh/autoload.el
index ad78ecc4b..886cfcad1 100644
--- a/modules/lang/sh/autoload.el
+++ b/modules/lang/sh/autoload.el
@@ -27,7 +27,7 @@
(defvar sh-shell-file)
;;;###autoload
-(defun +sh/repl ()
+(defun +sh/open-repl ()
"Open a shell REPL."
(let* ((dest-sh (symbol-name sh-shell))
(sh-shell-file dest-sh))
diff --git a/modules/lang/sh/config.el b/modules/lang/sh/config.el
index 6a78ed628..d71f81662 100644
--- a/modules/lang/sh/config.el
+++ b/modules/lang/sh/config.el
@@ -8,25 +8,27 @@
;;
-;; Plugins
-;;
+;; Packages
(def-package! sh-script ; built-in
- :mode ("\\.zsh$" . sh-mode)
- :mode ("\\.zunit$" . sh-mode)
- :mode ("/bspwmrc$" . sh-mode)
- :init
- (add-hook! sh-mode #'(flycheck-mode highlight-numbers-mode))
+ :mode ("\\.zunit\\'" . sh-mode)
+ :mode ("/bspwmrc\\'" . sh-mode)
:config
- (set! :electric 'sh-mode :words '("else" "elif" "fi" "done" "then" "do" "esac" ";;"))
- (set! :repl 'sh-mode #'+sh/repl)
+ (set-electric! 'sh-mode :words '("else" "elif" "fi" "done" "then" "do" "esac" ";;"))
+ (set-repl-handler! 'sh-mode #'+sh/open-repl)
(setq sh-indent-after-continuation 'always)
+ ;; [pedantry intensifies]
+ (setq-hook! 'sh-mode-hook mode-name "sh")
+
;; recognize function names with dashes in them
- (push '((sh . ((nil "^\\s-*function\\s-+\\([[:alpha:]_-][[:alnum:]_-]*\\)\\s-*\\(?:()\\)?" 1)
- (nil "^\\s-*\\([[:alpha:]_-][[:alnum:]_-]*\\)\\s-*()" 1))))
- sh-imenu-generic-expression)
+ (add-to-list 'sh-imenu-generic-expression
+ '(sh (nil "^\\s-*function\\s-+\\([[:alpha:]_-][[:alnum:]_-]*\\)\\s-*\\(?:()\\)?" 1)
+ (nil "^\\s-*\\([[:alpha:]_-][[:alnum:]_-]*\\)\\s-*()" 1)))
+
+ ;; `sh-set-shell' is chatty about setting up indentation rules
+ (advice-add #'sh-set-shell :around #'doom*shut-up)
;; 1. Fontifies variables in double quotes
;; 2. Fontify command substitution in double quotes
@@ -39,9 +41,11 @@
(1 'sh-quoted-exec prepend))
(,(regexp-opt +sh-builtin-keywords 'words)
(0 'font-lock-type-face append))))
+ ;; 4. Fontify delimiters by depth
+ (add-hook 'sh-mode-hook #'rainbow-delimiters-mode)
;; autoclose backticks
- (sp-local-pair 'sh-mode "`" nil :unless '(sp-point-before-word-p sp-point-before-same-p))
+ (sp-local-pair 'sh-mode "`" "`" :unless '(sp-point-before-word-p sp-point-before-same-p))
;; sh-mode has file extensions checks for other shells, but not zsh, so...
(defun +sh|detect-zsh ()
@@ -49,7 +53,7 @@
(string-match-p "\\.zsh\\'" buffer-file-name))
(save-excursion
(goto-char (point-min))
- (looking-at-p "^#!.+zsh[$\\s-]")))
+ (looking-at-p "^#!.+/zsh[$ ]")))
(sh-set-shell "zsh")))
(add-hook 'sh-mode-hook #'+sh|detect-zsh))
@@ -58,6 +62,11 @@
:when (featurep! :completion company)
:after sh-script
:config
- (set! :company-backend 'sh-mode '(company-shell company-files))
+ (set-company-backend! 'sh-mode '(company-shell company-files))
(setq company-shell-delete-duplicates t))
+
+(def-package! fish-mode
+ :when (featurep! +fish)
+ :defer t
+ :config (set-formatter! 'fish-mode #'fish_indent))
diff --git a/modules/lang/sh/packages.el b/modules/lang/sh/packages.el
index 11f456485..d3e3ad9e1 100644
--- a/modules/lang/sh/packages.el
+++ b/modules/lang/sh/packages.el
@@ -6,3 +6,6 @@
(when (featurep! :completion company)
(package! company-shell))
+
+(when (featurep! +fish)
+ (package! fish-mode))
diff --git a/modules/lang/solidity/README.org b/modules/lang/solidity/README.org
new file mode 100644
index 000000000..e0800b5a8
--- /dev/null
+++ b/modules/lang/solidity/README.org
@@ -0,0 +1,47 @@
+#+TITLE: :lang solidity
+
+This module adds [[https://github.com/ethereum/solidity][Solidity]] support through [[https://github.com/ethereum/emacs-solidity][solidity-mode]]
+
++ Syntax-checking (~flycheck~)
++ Code completion ([[https://github.com/ssmolkin1/company-solidity][company-solidity]])
++ Gas estimation (~C-c C-g~)
+
+* Table of Contents :TOC:
+- [[Module Flags][Module Flags]]
+- [[Prerequisites][Prerequisites]]
+ - [[Solc][Solc]]
+ - [[Solium][Solium]]
+- [[TODO][TODO]]
+
+* Module Flags
+This module provides no flags.
+
+* Prerequisites
+This module requires one or both linters for syntax checking:
+
++ [[https://github.com/ethereum/solc-js][Solc]]
++ [[http://solium.readthedocs.io/en/latest/user-guide.html#installation][Solium]]
+
+If both are enabled *Solc* is run first, then *Solium* if *Solc* catches no
+errors.
+
+** Solc
+#+BEGIN_SRC sh
+npm install -g solc
+#+END_SRC
+
+** Solium
+#+BEGIN_SRC sh
+npm install -g solium
+#+END_SRC
+
+By default *solium* looks for ~.soliumrc.json~ in the project directory, but you
+can set it to your own ~.soliumrc.json~ with this in your private doom
+~config.el~
+
+#+BEGIN_SRC emacs-lisp
+(setq flycheck-solidity-solium-soliumrcfile "~/.soliumrc.json")
+#+END_SRC
+
+* TODO
++ Snippets
diff --git a/modules/lang/solidity/config.el b/modules/lang/solidity/config.el
new file mode 100644
index 000000000..6a4cf3efb
--- /dev/null
+++ b/modules/lang/solidity/config.el
@@ -0,0 +1,26 @@
+;;; lang/solidity/config.el -*- lexical-binding: t; -*-
+
+;;
+;; Packages
+
+;; `solidity-mode'
+(setq solidity-comment-style 'slash)
+
+
+(def-package! solidity-flycheck ; included with solidity-mode
+ :when (featurep! :tools flycheck)
+ :after solidity-mode
+ :config
+ (setq flycheck-solidity-solc-addstd-contracts t)
+ (when (funcall flycheck-executable-find solidity-solc-path)
+ (add-to-list 'flycheck-checkers 'solidity-checker nil #'eq))
+ (when (funcall flycheck-executable-find solidity-solium-path)
+ (add-to-list 'flycheck-checkers 'solium-checker nil #'eq)))
+
+
+(def-package! company-solidity
+ :when (featurep! :completion company)
+ :after solidity-mode
+ :config
+ (setq company-backends (delq 'company-solidity company-backends))
+ (set-company-backend! 'solidity-mode 'company-solidity))
diff --git a/modules/lang/solidity/doctor.el b/modules/lang/solidity/doctor.el
new file mode 100644
index 000000000..146d640ee
--- /dev/null
+++ b/modules/lang/solidity/doctor.el
@@ -0,0 +1,8 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; lang/solidity/doctor.el
+
+(when (require 'solidity-common nil t)
+ (unless (executable-find solidity-solc-path)
+ (warn! "Solc isn't installed."))
+ (unless (executable-find solidity-solium-path)
+ (warn! "Solium isn't installed.")))
diff --git a/modules/lang/solidity/packages.el b/modules/lang/solidity/packages.el
new file mode 100644
index 000000000..7d6e78ff5
--- /dev/null
+++ b/modules/lang/solidity/packages.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/solidity/packages.el
+
+(package! solidity-mode)
+(package! company-solidity)
+(package! solidity-flycheck)
diff --git a/modules/lang/swift/config.el b/modules/lang/swift/config.el
index 9daae8f3a..0129d78ac 100644
--- a/modules/lang/swift/config.el
+++ b/modules/lang/swift/config.el
@@ -1,18 +1,25 @@
;;; lang/swift/config.el -*- lexical-binding: t; -*-
-;; TODO Set up emacs task runners for fruitstrap
+(after! swift-mode
+ (set-repl-handler! 'swift-mode #'run-swift))
-(def-package! swift-mode
- :mode "\\.swift$"
- :config
- (add-hook 'swift-mode-hook #'flycheck-mode)
- (set! :repl 'swift-mode #'swift-mode-run-repl) ; TODO test this
- (push 'swift flycheck-checkers))
+
+(def-package! flycheck-swift
+ :when (and (featurep! :tools flycheck)
+ (not (featurep! +lsp)))
+ :after swift-mode
+ :config (flycheck-swift-setup))
(def-package! company-sourcekit
- :when (featurep! :completion company)
+ :when (and (featurep! :completion company)
+ (not (featurep! +lsp)))
:after swift-mode
:config
- (set! :company-backend 'swift-mode '(company-sourcekit company-yasnippet)))
+ (set-company-backend! 'swift-mode '(company-sourcekit company-yasnippet)))
+
+(def-package! lsp-sourcekit
+ :when (featurep! +lsp)
+ :after swift-mode
+ :init (add-hook 'swift-mode-hook #'lsp!))
diff --git a/modules/lang/swift/packages.el b/modules/lang/swift/packages.el
index a9cc3a778..6dd3244f0 100644
--- a/modules/lang/swift/packages.el
+++ b/modules/lang/swift/packages.el
@@ -3,5 +3,9 @@
(package! swift-mode)
-(when (featurep! :completion company)
- (package! company-sourcekit))
+(if (featurep! +lsp)
+ (package! lsp-sourcekit)
+ (when (featurep! :completion company)
+ (package! company-sourcekit))
+ (when (featurep! :tools flycheck)
+ (package! flycheck-swift)))
diff --git a/modules/lang/terra/autoload.el b/modules/lang/terra/autoload.el
new file mode 100644
index 000000000..0920c3e08
--- /dev/null
+++ b/modules/lang/terra/autoload.el
@@ -0,0 +1,8 @@
+;;; lang/terra/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +terra/open-repl ()
+ "Open Terra REPL."
+ (interactive)
+ (terra-start-process "terra" "terra")
+ (pop-to-buffer terra-process-buffer))
diff --git a/modules/lang/terra/config.el b/modules/lang/terra/config.el
new file mode 100644
index 000000000..27515edb6
--- /dev/null
+++ b/modules/lang/terra/config.el
@@ -0,0 +1,12 @@
+;;; lang/lua/config.el -*- lexical-binding: t; -*-
+
+;;
+;; Major modes
+
+(def-package! terra-mode
+ :defer t
+ :config
+ (set-lookup-handlers! 'terra-mode :documentation 'terra-search-documentation)
+ (set-electric! 'terra-mode :words '("else" "end"))
+ (set-repl-handler! 'terra-mode #'+terra/open-repl)
+ (set-company-backend! 'terra-mode '(company-lua company-yasnippet)))
diff --git a/modules/lang/terra/packages.el b/modules/lang/terra/packages.el
new file mode 100644
index 000000000..90115f396
--- /dev/null
+++ b/modules/lang/terra/packages.el
@@ -0,0 +1,8 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/lua/packages.el
+
+(package! terra-mode :recipe (:fetcher github :repo "StanfordLegion/terra-mode"))
+
+(when (featurep! :completion company)
+ (package! company-lua))
+
diff --git a/modules/lang/typescript/config.el b/modules/lang/typescript/config.el
deleted file mode 100644
index 5c42d36e9..000000000
--- a/modules/lang/typescript/config.el
+++ /dev/null
@@ -1,45 +0,0 @@
-;;; lang/typescript/config.el -*- lexical-binding: t; -*-
-
-(def-package! typescript-mode
- :mode "\\.ts$"
- :config
- (add-hook 'typescript-mode-hook #'rainbow-delimiters-mode)
-
- (set! :electric 'typescript-mode :chars '(?\} ?\)) :words '("||" "&&"))
-
- ;; TODO tide-jump-back
- ;; TODO (tide-jump-to-definition t)
- ;; TODO convert into keybinds
- ;; (set! :emr 'typescript-mode
- ;; '(tide-find-references "find usages")
- ;; '(tide-rename-symbol "rename symbol")
- ;; '(tide-jump-to-definition "jump to definition")
- ;; '(tide-documentation-at-point "current type documentation")
- ;; '(tide-restart-server "restart tide server"))
- )
-
-
-(def-package! tide
- :after typescript-mode
- :config
- (set! :company-backend 'typescript-mode '(company-tide))
- (set! :jump 'typescript-mode
- :definition #'tide-jump-to-definition
- :references #'tide-references
- :documentation #'tide-documentation-at-point)
-
- (setq tide-format-options
- '(:insertSpaceAfterFunctionKeywordForAnonymousFunctions t
- :placeOpenBraceOnNewLineForFunctions nil))
-
- (defun +typescript|init-tide ()
- (when (or (eq major-mode 'typescript-mode)
- (and (eq major-mode 'web-mode)
- buffer-file-name
- (equal (file-name-extension buffer-file-name) "tsx")))
- (tide-setup)
- (flycheck-mode +1)
- (eldoc-mode +1)
- (setq tide-project-root (doom-project-root))))
- (add-hook! (typescript-mode web-mode) #'+typescript|init-tide))
-
diff --git a/modules/lang/typescript/packages.el b/modules/lang/typescript/packages.el
deleted file mode 100644
index 41af79318..000000000
--- a/modules/lang/typescript/packages.el
+++ /dev/null
@@ -1,6 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; lang/typescript/packages.el
-
-(package! tide)
-(package! typescript-mode)
-
diff --git a/modules/lang/vala/packages.el b/modules/lang/vala/packages.el
new file mode 100644
index 000000000..9bd41db12
--- /dev/null
+++ b/modules/lang/vala/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/vala/packages.el
+
+(package! vala-mode)
diff --git a/modules/lang/web/+css.el b/modules/lang/web/+css.el
index 8f4d51e07..ee987c085 100644
--- a/modules/lang/web/+css.el
+++ b/modules/lang/web/+css.el
@@ -1,57 +1,67 @@
;;; lang/web/+css.el -*- lexical-binding: t; -*-
-;; css-mode hooks apply to scss and less-css modes
-(add-hook 'css-mode-hook #'rainbow-delimiters-mode)
-(add-hook! (css-mode sass-mode)
- #'(yas-minor-mode-on flycheck-mode highlight-numbers-mode))
+;; An improved newline+continue comment function
+(setq-hook! css-mode comment-indent-function #'+css/comment-indent-new-line)
-(after! smartparens
- (sp-with-modes '(css-mode scss-mode less-css-mode stylus-mode)
- (sp-local-pair "/*" "*/" :post-handlers '(("[d-3]||\n[i]" "RET") ("| " "SPC")))))
+(map! :map (css-mode-map scss-mode-map less-css-mode-map)
+ :localleader
+ "rb" #'+css/toggle-inline-or-block)
-(map! :map* (css-mode-map scss-mode-map less-css-mode-map)
- :n "M-R" #'+css/web-refresh-browser
- (:localleader
- :n "rb" #'+css/toggle-inline-or-block))
+(after! (:any css-mode sass-mode)
+ (set-docsets! '(css-mode scss-mode sass-mode)
+ "CSS" "HTML" "Bourbon" "Compass"
+ ["Sass" (memq major-mode '(scss-mode sass-mode))]))
+
+(after! projectile
+ (pushnew! projectile-other-file-alist
+ '("css" "scss" "sass" "less" "styl")
+ '("scss" "css")
+ '("sass" "css")
+ '("less" "css")
+ '("styl" "css")))
;;
-;; Packages
-;;
+;; Major modes
-(def-package! counsel-css
- :when (featurep! :completion ivy)
- :commands (counsel-css counsel-css-imenu-setup)
- :hook (css-mode . counsel-css-imenu-setup)
- :init
- (map! :map* (css-mode-map scss-mode-map less-css-mode-map)
- :localleader :n ";" #'counsel-css))
+(add-hook! (css-mode sass-mode stylus-mode) #'rainbow-mode)
-
-(def-package! rainbow-mode
- :hook (css-mode sass-mode))
-
-
-(def-package! css-mode
- :mode "\\.css$"
- :mode ("\\.scss$" . scss-mode)
- :config
- (set! :company-backend '(css-mode scss-mode) '(company-css company-yasnippet))
+(after! css-mode ; built-in -- contains both css-mode & scss-mode
+ ;; css-mode hooks apply to scss and less-css modes
+ (add-hook 'css-mode-hook #'rainbow-delimiters-mode)
+ (unless EMACS26+
+ ;; css-mode's built in completion is superior in 26+
+ (set-company-backend! '(css-mode scss-mode) 'company-css))
(map! :map scss-mode-map :localleader "b" #'+css/scss-build))
-(def-package! sass-mode
- :mode "\\.sass$"
+(after! sass-mode
+ (set-company-backend! 'sass-mode 'company-css)
+ (map! :map sass-mode-map :localleader "b" #'+css/sass-build))
+
+
+;;
+;; Tools
+
+(when (featurep! +lsp)
+ (add-hook! (css-mode sass-mode less-css-mode) #'lsp!))
+
+
+(def-package! counsel-css
+ :when (featurep! :completion ivy)
+ :commands counsel-css
+ :hook (css-mode . counsel-css-imenu-setup)
+ :init
+ (map! :map (css-mode-map scss-mode-map less-css-mode-map)
+ :localleader ";" #'counsel-css))
+
+
+(def-package! helm-css-scss
+ :when (featurep! :completion helm)
+ :defer t
+ :init
+ (map! :map (css-mode-map scss-mode-map less-css-mode-map)
+ :localleader ";" #'helm-css-scss)
:config
- (set! :company-backend 'sass-mode '(company-css company-yasnippet))
- (map! :map scss-mode-map :localleader "b" #'+css/sass-build))
-
-
-(def-package! less-css-mode
- :mode "\\.less$")
-
-
-(def-package! stylus-mode
- :mode "\\.styl$"
- :init (add-hook! stylus-mode #'(yas-minor-mode-on flycheck-mode)))
-
+ (setq helm-css-scss-split-direction #'split-window-vertically
+ helm-css-scss-split-with-multiple-windows t))
diff --git a/modules/lang/web/+html.el b/modules/lang/web/+html.el
index 73ba50cf6..7b6bb02bb 100644
--- a/modules/lang/web/+html.el
+++ b/modules/lang/web/+html.el
@@ -2,23 +2,127 @@
(def-package! web-mode
:mode "\\.p?html?$"
- :mode "\\.\\(tpl\\|blade\\)\\(\\.php\\)?$"
+ :mode "\\.\\(?:tpl\\|blade\\)\\(\\.php\\)?$"
:mode "\\.erb$"
:mode "\\.jsp$"
:mode "\\.as[cp]x$"
+ :mode "\\.hbs$"
:mode "\\.mustache$"
:mode "\\.tsx$"
+ :mode "\\.vue$"
+ :mode "\\.twig$"
+ :mode "\\.jinja$"
:mode "wp-content/themes/.+/.+\\.php$"
+ :mode "templates/.+\\.php$"
:config
- (add-hook 'web-mode-hook #'turn-off-smartparens-mode)
- (set! :company-backend 'web-mode '(company-web-html company-yasnippet))
- (setq web-mode-enable-html-entities-fontification t)
+ (set-docsets! 'web-mode "HTML" "CSS" "Twig" "WordPress")
+
+ ;; tidy is already defined by the format-all package. We redefine it to add
+ ;; more sensible arguments to the tidy command.
+ (set-formatter! 'html-tidy
+ '("tidy" "-q" "-indent"
+ "--tidy-mark" "no"
+ "--drop-empty-elements" "no"
+ "--show-body-only" "auto" ; don't inject html/body tags
+ ("--indent-spaces" "%d" tab-width)
+ ("--indent-with-tabs" "%s" (if indent-tabs-mode "yes" "no"))
+ ("-xml" (memq major-mode '(nxml-mode xml-mode))))
+ :ok-statuses '(0 1))
+
+ (setq web-mode-enable-html-entities-fontification t
+ web-mode-auto-close-style 2)
+
+ (after! smartparens
+ (defun +web-is-auto-close-style-3 (_id action _context)
+ (and (eq action 'insert)
+ (eq web-mode-auto-close-style 3)))
+ (sp-local-pair 'web-mode "<" ">" :unless '(:add +web-is-auto-close-style-3))
+
+ ;; let smartparens handle these
+ (setq web-mode-enable-auto-quoting nil
+ web-mode-enable-auto-pairing t)
+
+ ;; 1. Remove web-mode auto pairs whose end pair starts with a latter
+ ;; (truncated autopairs like ). Smartparens handles these
+ ;; better.
+ ;; 2. Strips out extra closing pairs to prevent redundant characters
+ ;; inserted by smartparens.
+ (dolist (alist web-mode-engines-auto-pairs)
+ (setcdr alist
+ (cl-loop for pair in (cdr alist)
+ unless (string-match-p "^[a-z-]" (cdr pair))
+ collect (cons (car pair)
+ ;; TODO Replace with `string-trim-right' (Emacs 26+)
+ (let ((string (cdr pair)))
+ (if (string-match "\\(?:>\\|]\\|}\\)+\\'" string)
+ (replace-match "" t t string)
+ string))))))
+ (delq (assq nil web-mode-engines-auto-pairs) web-mode-engines-auto-pairs))
(map! :map web-mode-map
- (:localleader :n "rt" #'web-mode-element-rename)
- "M-/" #'web-mode-comment-or-uncomment
+ (:localleader
+ :desc "Rehighlight buffer" "h" #'web-mode-buffer-highlight
+ :desc "Indent buffer" "i" #'web-mode-buffer-indent
+
+ (:prefix "a"
+ "b" #'web-mode-attribute-beginning
+ "e" #'web-mode-attribute-end
+ "i" #'web-mode-attribute-insert
+ "n" #'web-mode-attribute-next
+ "s" #'web-mode-attribute-select
+ "k" #'web-mode-attribute-kill
+ "p" #'web-mode-attribute-previous
+ "p" #'web-mode-attribute-transpose)
+
+ (:prefix "b"
+ "b" #'web-mode-block-beginning
+ "c" #'web-mode-block-close
+ "e" #'web-mode-block-end
+ "k" #'web-mode-block-kill
+ "n" #'web-mode-block-next
+ "p" #'web-mode-block-previous
+ "s" #'web-mode-block-select)
+
+ (:prefix "d"
+ "a" #'web-mode-dom-apostrophes-replace
+ "d" #'web-mode-dom-errors-show
+ "e" #'web-mode-dom-entities-encode
+ "n" #'web-mode-dom-normalize
+ "q" #'web-mode-dom-quotes-replace
+ "t" #'web-mode-dom-traverse
+ "x" #'web-mode-dom-xpath)
+
+ (:prefix "e"
+ "/" #'web-mode-element-close
+ "a" #'web-mode-element-content-select
+ "b" #'web-mode-element-beginning
+ "c" #'web-mode-element-clone
+ "d" #'web-mode-element-child
+ "e" #'web-mode-element-end
+ "f" #'web-mode-element-children-fold-or-unfold
+ "i" #'web-mode-element-insert
+ "k" #'web-mode-element-kill
+ "m" #'web-mode-element-mute-blanks
+ "n" #'web-mode-element-next
+ "p" #'web-mode-element-previous
+ "r" #'web-mode-element-rename
+ "s" #'web-mode-element-select
+ "t" #'web-mode-element-transpose
+ "u" #'web-mode-element-parent
+ "v" #'web-mode-element-vanish
+ "w" #'web-mode-element-wrap)
+
+ (:prefix "t"
+ "a" #'web-mode-tag-attributes-sort
+ "b" #'web-mode-tag-beginning
+ "e" #'web-mode-tag-end
+ "m" #'web-mode-tag-match
+ "n" #'web-mode-tag-next
+ "p" #'web-mode-tag-previous
+ "s" #'web-mode-tag-select))
+
+ :g "M-/" #'web-mode-comment-or-uncomment
:i "SPC" #'self-insert-command
- :n "M-r" #'doom/web-refresh-browser
:n "za" #'web-mode-fold-or-unfold
:nv "]a" #'web-mode-attribute-next
:nv "[a" #'web-mode-attribute-previous
@@ -28,16 +132,14 @@
:nv "[T" #'web-mode-element-parent))
-(def-package! company-web
- :when (featurep! :completion company)
- :after web-mode)
+;;
+(after! pug-mode
+ (set-company-backend! 'pug-mode 'company-web-jade))
+(after! web-mode
+ (set-company-backend! 'web-mode 'company-web-html))
+(after! slim-mode
+ (set-company-backend! 'slim-mode 'company-web-slim))
-(def-package! haml-mode :mode "\\.haml$")
-
-
-(def-package! pug-mode
- :mode "\\.jade$"
- :mode "\\.pug$"
- :config
- (set! :company-backend 'pug-mode '(company-yasnippet)))
+(when (featurep! +lsp)
+ (add-hook! (html-mode web-mode) #'lsp!))
diff --git a/modules/lang/web/autoload/css.el b/modules/lang/web/autoload/css.el
index 608f6d2ec..93eacc2a0 100644
--- a/modules/lang/web/autoload/css.el
+++ b/modules/lang/web/autoload/css.el
@@ -1,33 +1,81 @@
;;; lang/web/autoload/css.el -*- lexical-binding: t; -*-
-;;;###autoload
+;; ;;;###autoload
;; TODO (defun +css/scss-build ())
-;;;###autoload
+;; ;;;###autoload
;; TODO (defun +css/sass-build ())
+(defun +css--toggle-inline-or-block (beg end)
+ (skip-chars-forward " \t")
+ (let ((orig (point-marker)))
+ (goto-char beg)
+ (if (= (line-number-at-pos beg) (line-number-at-pos end))
+ (progn
+ (forward-char)
+ (insert "\n")
+ (while (re-search-forward ";\\s-+" end t)
+ (replace-match ";\n" nil t))
+ (indent-region beg end))
+ (save-excursion
+ (while (re-search-forward "\n+" end t)
+ (replace-match " " nil t)))
+ (while (re-search-forward "\\([{;]\\) +" end t)
+ (replace-match (concat (match-string 1) " ") nil t)))
+ (if orig (goto-char orig))
+ (skip-chars-forward " \t")))
+
;;;###autoload
(defun +css/toggle-inline-or-block ()
"Toggles between a bracketed block and inline block."
(interactive)
- ;; TODO Remove evil dependency
- (save-excursion
- (cl-destructuring-bind (beg end)
- (or (ignore-errors (evil-a-curly))
- (user-error "No block found"))
- (if (= (line-number-at-pos beg) (line-number-at-pos end))
- (save-excursion
- (goto-char (1+ beg)) (insert "\n")
- (unless (string-match ";[\s\t]*}$" (buffer-substring-no-properties beg (1+ end)))
- (goto-char end) (insert "\n"))
- (while (re-search-forward ";[\s\t]*" (1+ end) t)
- (replace-match ";\n" t t))
- (setq end (cadr (evil-a-curly)))
- (evil-indent beg end)
- (delete-trailing-whitespace beg end))
- (goto-char beg)
- (evil-join beg end)
- (goto-char (1+ beg))
- (just-one-space)
- (goto-char (cadr (evil-inner-curly)))
- (just-one-space)))))
+ (let ((inhibit-modification-hooks t))
+ (cl-destructuring-bind (&key beg end op cl &allow-other-keys)
+ (save-excursion
+ (when (and (eq (char-after) ?\{)
+ (not (eq (char-before) ?\{)))
+ (forward-char))
+ (sp-get-sexp))
+ (when (or (not (and beg end op cl))
+ (string-empty-p op) (string-empty-p cl))
+ (user-error "No block found %s" (list beg end op cl)))
+ (unless (string= op "{")
+ (user-error "Incorrect block found"))
+ (+css--toggle-inline-or-block beg end))))
+
+;;;###autoload
+(defun +css/comment-indent-new-line ()
+ "Continues the comment in an indented new line in css-mode and scss-mode.
+Meant for `comment-line-break-function'."
+ (interactive)
+ (when (sp-point-in-comment)
+ (let ((at-end (looking-at-p ".+\\*/"))
+ type pre-indent post-indent)
+ (save-excursion
+ (let ((bol (line-beginning-position))
+ (eol (line-end-position)))
+ (if (not comment-use-syntax)
+ (progn
+ (goto-char bol)
+ (when (re-search-forward comment-start-skip eol t)
+ (goto-char (or (match-end 1) (match-beginning 0)))))
+ (goto-char (comment-beginning))))
+ (save-match-data
+ (looking-at "\\(//\\|/?\\*\\)")
+ (setq type (match-string 0)
+ pre-indent (- (match-beginning 0) (line-beginning-position))
+ post-indent
+ (progn
+ (goto-char (match-end 0))
+ (max 1 (skip-chars-forward " " (line-end-position)))))
+ (if (eolp) (setq post-indent 1))))
+ (insert "\n"
+ (make-string pre-indent 32)
+ (if (string= "/*" type)
+ " *"
+ type)
+ (make-string post-indent 32))
+ (when at-end
+ (save-excursion
+ (insert "\n" (make-string pre-indent 32))
+ (delete-char -1))))))
diff --git a/modules/lang/web/autoload/html.el b/modules/lang/web/autoload/html.el
index 4afdb1cb6..cd9093fb5 100644
--- a/modules/lang/web/autoload/html.el
+++ b/modules/lang/web/autoload/html.el
@@ -105,3 +105,18 @@ function @ http://ergoemacs.org/emacs/elisp_replace_html_entities_command.html"
"Decode HTML entities in region."
(interactive "r")
(+web--entities-region beg end t))
+
+;;;###autoload
+(defun +web/indent-or-yas-or-emmet-expand ()
+ "Invoke `indent-for-tab-command' if at or before text bol, `yas-expand' if on
+a snippet, or `emmet-expand-yas'/`emmet-expand-line', depending on whether
+`yas-minor-mode' is enabled or not."
+ (interactive)
+ (call-interactively
+ (cond ((<= (current-column) (current-indentation))
+ #'indent-for-tab-command)
+ ((bound-and-true-p yas-minor-mode)
+ (if (yas--templates-for-key-at-point)
+ #'yas-expand
+ #'emmet-expand-yas))
+ (#'emmet-expand-line))))
diff --git a/modules/lang/web/config.el b/modules/lang/web/config.el
index 391063ba1..973d8b424 100644
--- a/modules/lang/web/config.el
+++ b/modules/lang/web/config.el
@@ -1,41 +1,31 @@
;;; lang/web/config.el -*- lexical-binding: t; -*-
-(load! +html)
-(load! +css)
-
-
-(def-package! web-beautify
- :commands (web-beautify-html web-beautify-css)
- :init
- (map! (:map* (css-mode-map scss-mode-map less-css-mode-map)
- :n "gQ" #'web-beautify-css)
- (:map* web-mode-map
- :n "gQ" #'web-beautify-html)))
+(load! "+html")
+(load! "+css")
(def-package! emmet-mode
:commands emmet-mode
:preface (defvar emmet-mode-keymap (make-sparse-keymap))
- :hook (css-mode web-mode html-mode haml-mode nxml-mode rjsx-mode)
+ :hook (css-mode web-mode html-mode haml-mode nxml-mode rjsx-mode reason-mode)
:config
+ (when (require 'yasnippet nil t)
+ (add-hook 'emmet-mode-hook #'yas-minor-mode-on))
(setq emmet-move-cursor-between-quotes t)
+ (setq-hook! 'rjsx-mode-hook emmet-expand-jsx-className? t)
(map! :map emmet-mode-keymap
- :v "M-e" #'emmet-wrap-with-markup
- :i "M-e" #'emmet-expand-yas
+ :v [tab] #'emmet-wrap-with-markup
+ :i [tab] #'+web/indent-or-yas-or-emmet-expand
:i "M-E" #'emmet-expand-line))
;;
-;; Frameworks
-;;
-
-(def-project-mode! +web-angularjs-mode
- :modes (+javascript-npm-mode)
- :when (+javascript-npm-dep-p 'angular))
+;; Framework-based minor-modes
(def-project-mode! +web-jekyll-mode
:modes (web-mode js-mode coffee-mode css-mode haml-mode pug-mode)
- :files (and "config.yml" (or "_layouts/" "_posts/"))
+ :files (and (or "_config.yml" "_config.toml")
+ (or "_layouts/" "_posts/"))
:on-enter
(when (eq major-mode 'web-mode)
(web-mode-set-engine "django")))
@@ -44,10 +34,15 @@
:modes (php-mode web-mode css-mode haml-mode pug-mode)
:files (or "wp-config.php" "wp-config-sample.php"))
-(def-project-mode! +web-react-mode
- :modes (+javascript-npm-mode)
- :when (+javascript-npm-dep-p 'react))
+(when (featurep! :lang javascript)
+ (def-project-mode! +web-angularjs-mode
+ :modes (+javascript-npm-mode)
+ :when (+javascript-npm-dep-p 'angular))
-(def-project-mode! +web-phaser-mode
- :modes (+javascript-npm-mode)
- :when (+javascript-npm-dep-p '(or phaser phaser-ce)))
+ (def-project-mode! +web-react-mode
+ :modes (+javascript-npm-mode)
+ :when (+javascript-npm-dep-p 'react))
+
+ (def-project-mode! +web-phaser-mode
+ :modes (+javascript-npm-mode)
+ :when (+javascript-npm-dep-p '(or phaser phaser-ce))))
diff --git a/modules/lang/web/doctor.el b/modules/lang/web/doctor.el
new file mode 100644
index 000000000..c351f0c73
--- /dev/null
+++ b/modules/lang/web/doctor.el
@@ -0,0 +1,8 @@
+;;; lang/web/doctor.el -*- lexical-binding: t; -*-
+
+(unless (executable-find "js-beautify")
+ (warn! "Couldn't find js-beautify. Code formatting in JS/CSS/HTML modes will not work."))
+
+(unless (executable-find "stylelint")
+ (warn! "Couldn't find stylelint. Linting for CSS modes will not work."))
+
diff --git a/modules/lang/web/packages.el b/modules/lang/web/packages.el
index b8e748133..553bc6111 100644
--- a/modules/lang/web/packages.el
+++ b/modules/lang/web/packages.el
@@ -1,23 +1,21 @@
;; -*- no-byte-compile: t; -*-
;;; lang/web/packages.el
-;; requires js-beautify stylelint stylelint-scss
-
-(package! rainbow-mode)
-(package! web-beautify)
-(when (featurep! :completion ivy)
- (package! counsel-css :recipe (:fetcher github :repo "hlissner/emacs-counsel-css")))
-
;; +html.el
(package! emmet-mode)
(package! haml-mode)
(package! pug-mode)
-(package! web-mode)
-(when (featurep! :completion company)
- (package! company-web))
+(package! slim-mode)
+(when (package! web-mode)
+ (when (featurep! :completion company)
+ (package! company-web)))
;; +css.el
(package! less-css-mode)
(package! sass-mode)
(package! stylus-mode)
-
+(package! rainbow-mode)
+(when (featurep! :completion ivy)
+ (package! counsel-css))
+(when (featurep! :completion helm)
+ (package! helm-css-scss))
diff --git a/modules/lang/web/test/autoload-html.el b/modules/lang/web/test/autoload-html.el
deleted file mode 100644
index 95b6cc88f..000000000
--- a/modules/lang/web/test/autoload-html.el
+++ /dev/null
@@ -1,14 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; lang/web/test/autoload-html.el
-
-(def-test! encode-entities
- (should (equal (+web-encode-entities "Hello world")
- "Hello world"))
- (should (equal (+web-encode-entities "H€llø wørld")
- "H€llø wørld")))
-
-(def-test! decode-entities
- (should (equal (+web-decode-entities "Hello world")
- "Hello world"))
- (should (equal (+web-decode-entities "H€llø wørld")
- "H€llø wørld")))
diff --git a/modules/lang/web/test/test-web.el b/modules/lang/web/test/test-web.el
new file mode 100644
index 000000000..72fbc04cf
--- /dev/null
+++ b/modules/lang/web/test/test-web.el
@@ -0,0 +1,92 @@
+;; -*- no-byte-compile: t; -*-
+;;; lang/web/test/test-autoload-web.el
+
+(describe "lang/web"
+ (describe "+html"
+ (before-all (load! "../autoload/html.el"))
+
+ (describe "encode entities"
+ (it "encodes strings with html entities"
+ (expect (+web-encode-entities "H€llø wørld")
+ :to-equal "H€llø wørld"))
+ (it "does nothing when html entities are absent"
+ (expect (+web-encode-entities "Hello world")
+ :to-equal "Hello world")))
+
+ (describe "decode entities"
+ (it "decodes strings with html entities"
+ (expect (+web-decode-entities "H€llø wørld")
+ :to-equal "H€llø wørld"))
+ (it "does nothing when html entities are absent"
+ (expect (+web-decode-entities "Hello world")
+ :to-equal "Hello world"))))
+
+ (describe "+css"
+ :var (css-indent-offset)
+ (before-all
+ (load! "../autoload/css.el")
+ (require 'smartparens)
+ (smartparens-mode +1))
+ (after-all
+ (smartparens-mode -1)
+ (unload-feature 'smartparens t))
+
+ (before-each
+ (setq css-indent-offset 2)
+ (set-buffer (get-buffer-create "css"))
+ (delay-mode-hooks (css-mode)))
+ (after-each
+ (kill-buffer (get-buffer "css")))
+
+ (describe "toggle-inline-or-block"
+ (after-each
+ (quiet! (+css/toggle-inline-or-block))
+ (expect (string-trim (buffer-string)) :to-equal
+ (string-join
+ '("body {"
+ " color: red;"
+ " font-size: 2em;"
+ "}")
+ "\n")))
+
+ (describe "css-mode"
+ (it "converts inline statements into multiline blocks"
+ (insert! "body { color: red{0}; font-size: 2em; }"))
+ (it "works when cursor is on closing brace"
+ (insert! "body { color: red; font-size: 2em; {0}}"))
+ (it "works when cursor is on opening brace"
+ (insert! "body {{0} color: red; font-size: 2em; }"))
+ (it "works when cursor is on same line"
+ (insert! "{0}body { color: red; font-size: 2em; }"))))
+
+ (describe "comment-indent-new-line"
+ (before-each
+ (delay-mode-hooks (scss-mode)))
+
+ (it "continues commented lines on newline"
+ (insert! "// test{0}\n"
+ "body { color: red; font-size: 2em; }")
+ (+css/comment-indent-new-line)
+ (expect (string-trim (buffer-string)) :to-equal
+ (string-join
+ '("// test"
+ "// "
+ "body { color: red; font-size: 2em; }")
+ "\n"))
+ (expect (eolp))
+ (expect (line-number-at-pos) :to-be 2))
+ (it "preserves indentation within continued comments"
+ (insert! "// test{0}\n"
+ "body { color: red; font-size: 2em; }")
+ (+css/comment-indent-new-line)
+ (expect (string-trim (buffer-string)) :to-equal
+ (string-join
+ '("// test"
+ "// "
+ "body { color: red; font-size: 2em; }")
+ "\n"))
+ (expect (eolp))
+ (expect (line-number-at-pos) :to-be 2)))))
+
+
+;;
diff --git a/modules/private/README.org b/modules/private/README.org
deleted file mode 100644
index b9723dbc7..000000000
--- a/modules/private/README.org
+++ /dev/null
@@ -1,8 +0,0 @@
-#+TITLE: :private modules
-
-Use this directory to store your private configuration modules.
-
-Mine is included as a reference. I recommend you neither delete nor rename it, to avoid merge conflicts upstream.
-
------
-You'll find [[https://github.com/hlissner/doom-emacs/wiki/Customization][more information about customizing Doom]] on the [[https://github.com/hlissner/doom-emacs/wiki][wiki]].
diff --git a/modules/private/default/+bindings.el b/modules/private/default/+bindings.el
deleted file mode 100644
index a525a5a1c..000000000
--- a/modules/private/default/+bindings.el
+++ /dev/null
@@ -1,733 +0,0 @@
-;;; private/default/+bindings.el -*- lexical-binding: t; -*-
-
-;; This files defines a Spacemacs-esque keybinding scheme
-
-(map! [remap evil-jump-to-tag] #'projectile-find-tag
- [remap find-tag] #'projectile-find-tag
-
- ;; Ensure there are no conflicts
- :nmvo doom-leader-key nil
- :nmvo doom-localleader-key nil
-
- ;; --- Global keybindings ---------------------------
- ;; Make M-x available everywhere
- :gnvime "M-x" #'execute-extended-command
- :gnvime "A-x" #'execute-extended-command
-
- ;; A little sandbox to run code in
- :gnvime "M-;" #'eval-expression
- :gnvime "M-:" #'doom/open-scratch-buffer
-
- ;; Text-scaling
- "M-+" (λ! (text-scale-set 0))
- "M-=" #'text-scale-increase
- "M--" #'text-scale-decrease
-
- ;; Simple window navigation/manipulation
- "C-`" #'doom/popup-toggle
- "C-~" #'doom/popup-raise
- "M-t" #'+workspace/new
- "M-T" #'+workspace/display
- "M-w" #'delete-window
- "M-W" #'+workspace/close-workspace-or-frame
- "M-n" #'evil-buffer-new
- "M-N" #'make-frame
- "M-1" (λ! (+workspace/switch-to 0))
- "M-2" (λ! (+workspace/switch-to 1))
- "M-3" (λ! (+workspace/switch-to 2))
- "M-4" (λ! (+workspace/switch-to 3))
- "M-5" (λ! (+workspace/switch-to 4))
- "M-6" (λ! (+workspace/switch-to 5))
- "M-7" (λ! (+workspace/switch-to 6))
- "M-8" (λ! (+workspace/switch-to 7))
- "M-9" (λ! (+workspace/switch-to 8))
- "M-0" #'+workspace/switch-to-last
-
- ;; Other sensible, textmate-esque global bindings
- :ne "M-r" #'+eval/buffer
- :ne "M-R" #'+eval/region-and-replace
- :ne "M-b" #'+eval/build
- :ne "M-a" #'mark-whole-buffer
- :ne "M-c" #'evil-yank
- :ne "M-q" (if (daemonp) #'delete-frame #'save-buffers-kill-emacs)
- :ne "M-f" #'swiper
- :ne "C-M-f" #'doom/toggle-fullscreen
- :n "M-s" #'save-buffer
- :m "A-j" #'+default:multi-next-line
- :m "A-k" #'+default:multi-previous-line
- :nv "C-SPC" #'+evil:fold-toggle
- :gnvimer "M-v" #'clipboard-yank
- ;; Easier window navigation
- :en "C-h" #'evil-window-left
- :en "C-j" #'evil-window-down
- :en "C-k" #'evil-window-up
- :en "C-l" #'evil-window-right
-
- "C-x p" #'doom/other-popup
-
-
- ;; --- -------------------------------------
- (:leader
- :desc "Ex command" :nv ";" #'evil-ex
- :desc "M-x" :nv ":" #'execute-extended-command
- :desc "Pop up scratch buffer" :nv "x" #'doom/open-scratch-buffer
- :desc "Org Capture" :nv "X" #'+org-capture/open
-
- ;; Most commonly used
- :desc "Find file in project" :n "SPC" #'projectile-find-file
- :desc "Switch workspace buffer" :n "," #'persp-switch-to-buffer
- :desc "Switch buffer" :n "<" #'switch-to-buffer
- :desc "Browse files" :n "." #'find-file
- :desc "Toggle last popup" :n "~" #'doom/popup-toggle
- :desc "Eval expression" :n "`" #'eval-expression
- :desc "Blink cursor line" :n "DEL" #'+doom/blink-cursor
- :desc "Jump to bookmark" :n "RET" #'bookmark-jump
-
- ;; C-u is used by evil
- :desc "Universal argument" :n "u" #'universal-argument
- :desc "window" :n "w" evil-window-map
-
- (:desc "previous..." :prefix "["
- :desc "Text size" :nv "[" #'text-scale-decrease
- :desc "Buffer" :nv "b" #'doom/previous-buffer
- :desc "Diff Hunk" :nv "d" #'git-gutter:previous-hunk
- :desc "Todo" :nv "t" #'hl-todo-previous
- :desc "Error" :nv "e" #'previous-error
- :desc "Workspace" :nv "w" #'+workspace/switch-left
- :desc "Smart jump" :nv "h" #'smart-backward
- :desc "Spelling error" :nv "s" #'evil-prev-flyspell-error
- :desc "Spelling correction" :n "S" #'flyspell-correct-previous-word-generic)
-
- (:desc "next..." :prefix "]"
- :desc "Text size" :nv "]" #'text-scale-increase
- :desc "Buffer" :nv "b" #'doom/next-buffer
- :desc "Diff Hunk" :nv "d" #'git-gutter:next-hunk
- :desc "Todo" :nv "t" #'hl-todo-next
- :desc "Error" :nv "e" #'next-error
- :desc "Workspace" :nv "w" #'+workspace/switch-right
- :desc "Smart jump" :nv "l" #'smart-forward
- :desc "Spelling error" :nv "s" #'evil-next-flyspell-error
- :desc "Spelling correction" :n "S" #'flyspell-correct-word-generic)
-
- (:desc "search" :prefix "/"
- :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 "workspace" :prefix "TAB"
- :desc "Display tab bar" :n "TAB" #'+workspace/display
- :desc "New workspace" :n "n" #'+workspace/new
- :desc "Load workspace from file" :n "l" #'+workspace/load
- :desc "Load last session" :n "L" (λ! (+workspace/load-session))
- :desc "Save workspace to file" :n "s" #'+workspace/save
- :desc "Autosave current session" :n "S" #'+workspace/save-session
- :desc "Switch workspace" :n "." #'+workspace/switch-to
- :desc "Kill all buffers" :n "x" #'doom/kill-all-buffers
- :desc "Delete session" :n "X" #'+workspace/kill-session
- :desc "Delete this workspace" :n "d" #'+workspace/delete
- :desc "Load session" :n "L" #'+workspace/load-session
- :desc "Next workspace" :n "]" #'+workspace/switch-right
- :desc "Previous workspace" :n "[" #'+workspace/switch-left
- :desc "Switch to 1st workspace" :n "1" (λ! (+workspace/switch-to 0))
- :desc "Switch to 2nd workspace" :n "2" (λ! (+workspace/switch-to 1))
- :desc "Switch to 3rd workspace" :n "3" (λ! (+workspace/switch-to 2))
- :desc "Switch to 4th workspace" :n "4" (λ! (+workspace/switch-to 3))
- :desc "Switch to 5th workspace" :n "5" (λ! (+workspace/switch-to 4))
- :desc "Switch to 6th workspace" :n "6" (λ! (+workspace/switch-to 5))
- :desc "Switch to 7th workspace" :n "7" (λ! (+workspace/switch-to 6))
- :desc "Switch to 8th workspace" :n "8" (λ! (+workspace/switch-to 7))
- :desc "Switch to 9th workspace" :n "9" (λ! (+workspace/switch-to 8))
- :desc "Switch to last workspace" :n "0" #'+workspace/switch-to-last)
-
- (:desc "buffer" :prefix "b"
- :desc "New empty buffer" :n "n" #'evil-buffer-new
- :desc "Switch workspace buffer" :n "b" #'persp-switch-to-buffer
- :desc "Switch buffer" :n "B" #'switch-to-buffer
- :desc "Kill buffer" :n "k" #'doom/kill-this-buffer
- :desc "Kill other buffers" :n "o" #'doom/kill-other-buffers
- :desc "Save buffer" :n "s" #'save-buffer
- :desc "Pop scratch buffer" :n "x" #'doom/open-scratch-buffer
- :desc "Bury buffer" :n "z" #'bury-buffer
- :desc "Next buffer" :n "]" #'doom/next-buffer
- :desc "Previous buffer" :n "[" #'doom/previous-buffer
- :desc "Sudo edit this file" :n "S" #'doom/sudo-this-file)
-
- (:desc "code" :prefix "c"
- :desc "List errors" :n "x" #'flycheck-list-errors
- :desc "Evaluate buffer/region" :n "e" #'+eval/buffer
- :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 "Open REPL" :n "r" #'+eval/open-repl
- :v "r" #'+eval:repl)
-
- (:desc "file" :prefix "f"
- :desc "Find file" :n "." #'find-file
- :desc "Sudo find file" :n ">" #'doom/sudo-find-file
- :desc "Find file in project" :n "/" #'projectile-find-file
- :desc "Find file from here" :n "?" #'counsel-file-jump
- :desc "Find other file" :n "a" #'projectile-find-other-file
- :desc "Open project editorconfig" :n "c" #'editorconfig-find-current-editorconfig
- :desc "Find file in dotfiles" :n "d" #'+default/find-in-dotfiles
- :desc "Browse dotfiles" :n "D" #'+default/browse-dotfiles
- :desc "Find file in emacs.d" :n "e" #'+default/find-in-emacsd
- :desc "Browse emacs.d" :n "E" #'+default/browse-emacsd
- :desc "Recent files" :n "r" #'recentf-open-files
- :desc "Recent project files" :n "R" #'projectile-recentf
- :desc "Yank filename" :n "y" #'+default/yank-buffer-filename)
-
- (:desc "git" :prefix "g"
- :desc "Git status" :n "S" #'magit-status
- :desc "Git blame" :n "b" #'magit-blame
- :desc "Git time machine" :n "t" #'git-timemachine-toggle
- :desc "Git stage hunk" :n "s" #'git-gutter:stage-hunk
- :desc "Git revert hunk" :n "r" #'git-gutter:revert-hunk
- :desc "Git revert buffer" :n "R" #'vc-revert
- :desc "List gists" :n "g" #'+gist:list
- :desc "Next hunk" :nv "]" #'git-gutter:next-hunk
- :desc "Previous hunk" :nv "[" #'git-gutter:previous-hunk)
-
- (:desc "help" :prefix "h"
- :n "h" help-map
- :desc "Apropos" :n "a" #'apropos
- :desc "Reload theme" :n "R" #'doom//reload-theme
- :desc "Find library" :n "l" #'find-library
- :desc "Toggle Emacs log" :n "m" #'doom/popup-toggle-messages
- :desc "Command log" :n "L" #'global-command-log-mode
- :desc "Describe function" :n "f" #'describe-function
- :desc "Describe key" :n "k" #'describe-key
- :desc "Describe char" :n "c" #'describe-char
- :desc "Describe mode" :n "M" #'describe-mode
- :desc "Describe variable" :n "v" #'describe-variable
- :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 "What face" :n "'" #'doom/what-face
- :desc "What minor modes" :n ";" #'doom/what-minor-mode
- :desc "Info" :n "i" #'info
- :desc "Toggle profiler" :n "p" #'doom/toggle-profiler)
-
- (:desc "insert" :prefix "i"
- :desc "From kill-ring" :nv "y" #'counsel-yank-pop
- :desc "From snippet" :nv "s" #'yas-insert-snippet)
-
- (:desc "notes" :prefix "n"
- :desc "Find file in notes" :n "n" #'+default/find-in-notes
- :desc "Browse notes" :n "N" #'+default/browse-notes
- :desc "Org capture" :n "x" #'+org-capture/open
- :desc "Browse mode notes" :n "m" #'+org/browse-notes-for-major-mode
- :desc "Browse project notes" :n "p" #'+org/browse-notes-for-project)
-
- (:desc "open" :prefix "o"
- :desc "Default browser" :n "b" #'browse-url-of-file
- :desc "Debugger" :n "d" #'+debug/open
- :desc "REPL" :n "r" #'+eval/open-repl
- :v "r" #'+eval:repl
- :desc "Neotree" :n "n" #'+neotree/toggle
- :desc "Terminal" :n "t" #'+term/open-popup
- :desc "Terminal in project" :n "T" #'+term/open-popup-in-project
-
- ;; applications
- :desc "APP: elfeed" :n "E" #'=rss
- :desc "APP: email" :n "M" #'=email
- :desc "APP: twitter" :n "T" #'=twitter
- :desc "APP: regex" :n "X" #'=regex
-
- ;; macos
- (:when IS-MAC
- :desc "Reveal in Finder" :n "o" #'+macos/reveal-in-finder
- :desc "Reveal project in Finder" :n "O" #'+macos/reveal-project-in-finder
- :desc "Send to Transmit" :n "u" #'+macos/send-to-transmit
- :desc "Send project to Transmit" :n "U" #'+macos/send-project-to-transmit
- :desc "Send to Launchbar" :n "l" #'+macos/send-to-launchbar
- :desc "Send project to Launchbar" :n "L" #'+macos/send-project-to-launchbar))
-
- (:desc "project" :prefix "p"
- :desc "Browse project" :n "." #'+default/browse-project
- :desc "Find file in project" :n "/" #'projectile-find-file
- :desc "Run cmd in project root" :nv "!" #'projectile-run-shell-command-in-root
- :desc "Switch project" :n "p" #'projectile-switch-project
- :desc "Recent project files" :n "r" #'projectile-recentf
- :desc "List project tasks" :n "t" #'+ivy/tasks
- :desc "Pop term in project" :n "o" #'+term/open-popup-in-project
- :desc "Invalidate cache" :n "x" #'projectile-invalidate-cache)
-
- (:desc "quit" :prefix "q"
- :desc "Quit" :n "q" #'evil-save-and-quit
- :desc "Quit (forget session)" :n "Q" #'+workspace/kill-session-and-quit)
-
- (:desc "remote" :prefix "r"
- :desc "Upload local" :n "u" #'+upload/local
- :desc "Upload local (force)" :n "U" (λ! (+upload/local t))
- :desc "Download remote" :n "d" #'+upload/remote-download
- :desc "Diff local & remote" :n "D" #'+upload/diff
- :desc "Browse remote files" :n "." #'+upload/browse
- :desc "Detect remote changes" :n ">" #'+upload/check-remote)
-
- (:desc "snippets" :prefix "s"
- :desc "New snippet" :n "n" #'yas-new-snippet
- :desc "Insert snippet" :nv "i" #'yas-insert-snippet
- :desc "Find snippet for mode" :n "s" #'yas-visit-snippet-file
- :desc "Find snippet" :n "S" #'+default/find-in-snippets)
-
- (:desc "toggle" :prefix "t"
- :desc "Flyspell" :n "s" #'flyspell-mode
- :desc "Flycheck" :n "f" #'flycheck-mode
- :desc "Line numbers" :n "l" #'doom/toggle-line-numbers
- :desc "Fullscreen" :n "f" #'doom/toggle-fullscreen
- :desc "Indent guides" :n "i" #'highlight-indentation-mode
- :desc "Indent guides (column)" :n "I" #'highlight-indentation-current-column-mode
- :desc "Impatient mode" :n "h" #'+impatient-mode/toggle
- :desc "Big mode" :n "b" #'doom-big-font-mode
- :desc "Evil goggles" :n "g" #'+evil-goggles/toggle))
-
-
- ;; --- Personal vim-esque bindings ------------------
- :n "zx" #'doom/kill-this-buffer
- :n "ZX" #'bury-buffer
- :n "]b" #'doom/next-buffer
- :n "[b" #'doom/previous-buffer
- :n "]w" #'+workspace/switch-right
- :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
- :n "gp" #'+evil/reselect-paste
- :n "gr" #'+eval:region
- :n "gR" #'+eval/buffer
- :v "gR" #'+eval:replace-region
- :v "@" #'+evil:macro-on-all-lines
- :n "g@" #'+evil:macro-on-all-lines
- ;; repeat in visual mode (FIXME buggy)
- :v "." #'evil-repeat
- ;; don't leave visual mode after shifting
- :v "<" #'+evil/visual-dedent ; vnoremap < " #'+evil/visual-indent ; vnoremap > >gv
- ;; paste from recent yank register (which isn't overwritten)
- :v "C-p" "\"0p"
-
- (:map evil-window-map ; prefix "C-w"
- ;; Navigation
- "C-h" #'evil-window-left
- "C-j" #'evil-window-down
- "C-k" #'evil-window-up
- "C-l" #'evil-window-right
- "C-w" #'ace-window
- ;; Swapping windows
- "H" #'+evil/window-move-left
- "J" #'+evil/window-move-down
- "K" #'+evil/window-move-up
- "L" #'+evil/window-move-right
- "C-S-w" #'ace-swap-window
- ;; Window undo/redo
- "u" #'winner-undo
- "C-u" #'winner-undo
- "C-r" #'winner-redo
- "o" #'doom/window-enlargen
- ;; Delete window
- "c" #'+workspace/close-window-or-workspace
- "C-C" #'ace-delete-window)
-
-
- ;; --- Plugin bindings ------------------------------
- ;; auto-yasnippet
- :i [C-tab] #'aya-expand
- :nv [C-tab] #'aya-create
-
- ;; company-mode (vim-like omnicompletion)
- :i "C-SPC" #'+company/complete
- (:prefix "C-x"
- :i "C-l" #'+company/whole-lines
- :i "C-k" #'+company/dict-or-keywords
- :i "C-f" #'company-files
- :i "C-]" #'company-etags
- :i "s" #'company-ispell
- :i "C-s" #'company-yasnippet
- :i "C-o" #'company-capf
- :i "C-n" #'company-dabbrev-code
- :i "C-p" #'+company/dabbrev-code-previous)
- (:after company
- (:map company-active-map
- ;; Don't interfere with `evil-delete-backward-word' in insert mode
- "C-w" nil
- "C-o" #'company-search-kill-others
- "C-n" #'company-select-next
- "C-p" #'company-select-previous
- "C-h" #'company-quickhelp-manual-begin
- "C-S-h" #'company-show-doc-buffer
- "C-S-s" #'company-search-candidates
- "C-s" #'company-filter-candidates
- "C-SPC" #'company-complete-common
- "C-h" #'company-quickhelp-manual-begin
- [tab] #'company-complete-common-or-cycle
- [backtab] #'company-select-previous
- [escape] (λ! (company-abort) (evil-normal-state 1)))
- ;; Automatically applies to `company-filter-map'
- (:map company-search-map
- "C-n" #'company-search-repeat-forward
- "C-p" #'company-search-repeat-backward
- "C-s" (λ! (company-search-abort) (company-filter-candidates))
- [escape] #'company-search-abort))
-
- ;; counsel
- (:after counsel
- (:map counsel-ag-map
- [backtab] #'+ivy/wgrep-occur ; search/replace on results
- "C-SPC" #'ivy-call-and-recenter ; preview
- "M-RET" (+ivy-do-action! #'+ivy-git-grep-other-window-action)))
-
- ;; evil-commentary
- :n "gc" #'evil-commentary
-
- ;; evil-exchange
- :n "gx" #'evil-exchange
-
- ;; evil-matchit
- :nv [tab] #'+evil/matchit-or-toggle-fold
-
- ;; evil-magit
- (:after evil-magit
- :map (magit-status-mode-map magit-revision-mode-map)
- :n "C-j" nil
- :n "C-k" nil)
-
- ;; evil-mc
- (:prefix "gz"
- :nv "m" #'evil-mc-make-all-cursors
- :nv "u" #'evil-mc-undo-all-cursors
- :nv "z" #'+evil/mc-make-cursor-here
- :nv "t" #'+evil/mc-toggle-cursors
- :nv "n" #'evil-mc-make-and-goto-next-cursor
- :nv "p" #'evil-mc-make-and-goto-prev-cursor
- :nv "N" #'evil-mc-make-and-goto-last-cursor
- :nv "P" #'evil-mc-make-and-goto-first-cursor
- :nv "d" #'evil-mc-make-and-goto-next-match
- :nv "D" #'evil-mc-make-and-goto-prev-match)
- (:after evil-mc
- :map evil-mc-key-map
- :nv "C-n" #'evil-mc-make-and-goto-next-cursor
- :nv "C-N" #'evil-mc-make-and-goto-last-cursor
- :nv "C-p" #'evil-mc-make-and-goto-prev-cursor
- :nv "C-P" #'evil-mc-make-and-goto-first-cursor)
-
- ;; evil-multiedit
- :v "R" #'evil-multiedit-match-all
- :n "M-d" #'evil-multiedit-match-symbol-and-next
- :n "M-D" #'evil-multiedit-match-symbol-and-prev
- :v "M-d" #'evil-multiedit-match-and-next
- :v "M-D" #'evil-multiedit-match-and-prev
- :nv "C-M-d" #'evil-multiedit-restore
- (:after evil-multiedit
- (:map evil-multiedit-state-map
- "M-d" #'evil-multiedit-match-and-next
- "M-D" #'evil-multiedit-match-and-prev
- "RET" #'evil-multiedit-toggle-or-restrict-region)
- (:map (evil-multiedit-state-map evil-multiedit-insert-state-map)
- "C-n" #'evil-multiedit-next
- "C-p" #'evil-multiedit-prev))
-
- ;; evil-snipe
- (:after evil-snipe
- (:after evil-easymotion
- ;; Binding to switch to evil-easymotion/avy after a snipe
- :map evil-snipe-parent-transient-map
- "C-;" (λ! (require 'evil-easymotion)
- (call-interactively
- (evilem-create #'evil-snipe-repeat
- :bind ((evil-snipe-scope 'whole-buffer)
- (evil-snipe-enable-highlight)
- (evil-snipe-enable-incremental-highlight)))))))
-
- ;; evil-surround
- :v "S" #'evil-surround-region
- :o "s" #'evil-surround-edit
- :o "S" #'evil-Surround-edit
-
- ;; expand-region
- :v "v" #'er/expand-region
- :v "V" #'er/contract-region
-
- ;; flycheck
- :m "]e" #'next-error
- :m "[e" #'previous-error
- (:after flycheck
- :map flycheck-error-list-mode-map
- :n "C-n" #'flycheck-error-list-next-error
- :n "C-p" #'flycheck-error-list-previous-error
- :n "j" #'flycheck-error-list-next-error
- :n "k" #'flycheck-error-list-previous-error
- :n "RET" #'flycheck-error-list-goto-error)
-
- ;; flyspell
- :m "]S" #'flyspell-correct-word-generic
- :m "[S" #'flyspell-correct-previous-word-generic
-
- ;; git-gutter
- :m "]d" #'git-gutter:next-hunk
- :m "[d" #'git-gutter:previous-hunk
-
- ;; git-timemachine
- (:after git-timemachine
- (:map git-timemachine-mode-map
- :n "C-p" #'git-timemachine-show-previous-revision
- :n "C-n" #'git-timemachine-show-next-revision
- :n "[[" #'git-timemachine-show-previous-revision
- :n "]]" #'git-timemachine-show-next-revision
- :n "q" #'git-timemachine-quit
- :n "gb" #'git-timemachine-blame))
-
- ;; gist
- (:after gist
- :map gist-list-menu-mode-map
- :n "RET" #'+gist/open-current
- :n "b" #'gist-browse-current-url
- :n "c" #'gist-add-buffer
- :n "d" #'gist-kill-current
- :n "f" #'gist-fork
- :n "q" #'quit-window
- :n "r" #'gist-list-reload
- :n "s" #'gist-star
- :n "S" #'gist-unstar
- :n "y" #'gist-print-current-url)
-
- ;; helm
- (:after helm
- (:map helm-map
- "ESC" nil
- "C-S-n" #'helm-next-source
- "C-S-p" #'helm-previous-source
- "C-u" #'helm-delete-minibuffer-contents
- "C-w" #'backward-kill-word
- "C-r" #'evil-paste-from-register ; Evil registers in helm! Glorious!
- "C-b" #'backward-word
- [left] #'backward-char
- [right] #'forward-char
- [escape] #'helm-keyboard-quit
- [tab] #'helm-execute-persistent-action)
-
- (:after helm-files
- (:map helm-generic-files-map
- :e "ESC" #'helm-keyboard-quit)
- (:map helm-find-files-map
- "C-w" #'helm-find-files-up-one-level
- "TAB" #'helm-execute-persistent-action))
-
- (:after helm-ag
- (:map helm-ag-map
- "" #'helm-ag-edit)))
-
- ;; hl-todo
- :m "]t" #'hl-todo-next
- :m "[t" #'hl-todo-previous
-
- ;; ivy
- (:after ivy
- :map ivy-minibuffer-map
- [escape] #'keyboard-escape-quit
- "C-SPC" #'ivy-call-and-recenter
- "M-v" #'yank
- "M-z" #'undo
- "C-r" #'evil-paste-from-register
- "C-k" #'ivy-previous-line
- "C-j" #'ivy-next-line
- "C-l" #'ivy-alt-done
- "C-w" #'ivy-backward-kill-word
- "C-u" #'ivy-kill-line
- "C-b" #'backward-word
- "C-f" #'forward-word)
-
- ;; neotree
- (:after neotree
- :map neotree-mode-map
- :n "g" nil
- :n [tab] #'neotree-quick-look
- :n "RET" #'neotree-enter
- :n [backspace] #'evil-window-prev
- :n "c" #'neotree-create-node
- :n "r" #'neotree-rename-node
- :n "d" #'neotree-delete-node
- :n "j" #'neotree-next-line
- :n "k" #'neotree-previous-line
- :n "n" #'neotree-next-line
- :n "p" #'neotree-previous-line
- :n "h" #'+neotree/collapse-or-up
- :n "l" #'+neotree/expand-or-open
- :n "J" #'neotree-select-next-sibling-node
- :n "K" #'neotree-select-previous-sibling-node
- :n "H" #'neotree-select-up-node
- :n "L" #'neotree-select-down-node
- :n "G" #'evil-goto-line
- :n "gg" #'evil-goto-first-line
- :n "v" #'neotree-enter-vertical-split
- :n "s" #'neotree-enter-horizontal-split
- :n "q" #'neotree-hide
- :n "R" #'neotree-refresh)
-
- ;; realgud
- (:after realgud
- :map realgud:shortkey-mode-map
- :n "j" #'evil-next-line
- :n "k" #'evil-previous-line
- :n "h" #'evil-backward-char
- :n "l" #'evil-forward-char
- :m "n" #'realgud:cmd-next
- :m "b" #'realgud:cmd-break
- :m "B" #'realgud:cmd-clear
- :n "c" #'realgud:cmd-continue)
-
- ;; rotate-text
- :n "!" #'rotate-text
-
- ;; smart-forward
- :nv "K" #'smart-up
- :m "g]" #'smart-forward
- :m "g[" #'smart-backward
-
- ;; undo-tree -- undo/redo for visual regions
- :v "C-u" #'undo-tree-undo
- :v "C-r" #'undo-tree-redo
-
- ;; yasnippet
- (:after yasnippet
- (:map yas-keymap
- "C-e" #'+snippets/goto-end-of-field
- "C-a" #'+snippets/goto-start-of-field
- "" #'+snippets/goto-end-of-field
- "" #'+snippets/goto-start-of-field
- "" #'+snippets/delete-to-start-of-field
- [escape] #'evil-normal-state
- [backspace] #'+snippets/delete-backward-char
- [delete] #'+snippets/delete-forward-char-or-field)
- (:map yas-minor-mode-map
- :i "" yas-maybe-expand
- :v "" #'+snippets/expand-on-region))
-
-
- ;; --- Major mode bindings --------------------------
- (:after markdown-mode
- (:map markdown-mode-map
- ;; fix conflicts with private bindings
- "" nil
- "" nil
- "" nil))
-
-
- ;; --- Custom evil text-objects ---------------------
- :textobj "a" #'evil-inner-arg #'evil-outer-arg
- :textobj "B" #'evil-textobj-anyblock-inner-block #'evil-textobj-anyblock-a-block
- :textobj "i" #'evil-indent-plus-i-indent #'evil-indent-plus-a-indent
- :textobj "I" #'evil-indent-plus-i-indent-up #'evil-indent-plus-a-indent-up
- :textobj "J" #'evil-indent-plus-i-indent-up-down #'evil-indent-plus-a-indent-up-down
-
-
- ;; --- Built-in plugins -----------------------------
- (:after comint
- ;; TAB auto-completion in term buffers
- :map comint-mode-map [tab] #'company-complete)
-
- (:after debug
- ;; For elisp debugging
- :map debugger-mode-map
- :n "RET" #'debug-help-follow
- :n "e" #'debugger-eval-expression
- :n "n" #'debugger-step-through
- :n "c" #'debugger-continue)
-
- (:map help-mode-map
- :n "[[" #'help-go-back
- :n "]]" #'help-go-forward
- :n "o" #'ace-link-help
- :n "q" #'quit-window
- :n "Q" #'+ivy-quit-and-resume)
-
- (:after vc-annotate
- :map vc-annotate-mode-map
- :n "q" #'kill-this-buffer
- :n "d" #'vc-annotate-show-diff-revision-at-line
- :n "D" #'vc-annotate-show-changeset-diff-revision-at-line
- :n "SPC" #'vc-annotate-show-log-revision-at-line
- :n "]]" #'vc-annotate-next-revision
- :n "[[" #'vc-annotate-prev-revision
- :n "TAB" #'vc-annotate-toggle-annotation-visibility
- :n "RET" #'vc-annotate-find-revision-at-line))
-
-
-;;
-;; Keybinding fixes
-;;
-
-;; This section is dedicated to "fixing" certain keys so that they behave
-;; properly, more like vim, or how I like it.
-
-(map! (:map input-decode-map
- [S-iso-lefttab] [backtab]
- (:unless window-system "TAB" [tab])) ; Fix TAB in terminal
-
- ;; I want C-a and C-e to be a little smarter. C-a will jump to
- ;; indentation. Pressing it again will send you to the true bol. Same goes
- ;; for C-e, except it will ignore comments and trailing whitespace before
- ;; jumping to eol.
- :i "C-a" #'doom/backward-to-bol-or-indent
- :i "C-e" #'doom/forward-to-last-non-comment-or-eol
- :i "C-u" #'doom/backward-kill-to-bol-and-indent
-
- ;; textmate-esque newline insertion
- :i [M-return] #'evil-open-below
- :i [S-M-return] #'evil-open-above
- ;; textmate-esque deletion
- [M-backspace] #'doom/backward-kill-to-bol-and-indent
- :i [backspace] #'delete-backward-char
- :i [M-backspace] #'doom/backward-kill-to-bol-and-indent
- ;; Emacsien motions for insert mode
- :i "C-b" #'backward-word
- :i "C-f" #'forward-word
-
- ;; Highjacks space/backspace to:
- ;; a) balance spaces inside brackets/parentheses ( | ) -> (|)
- ;; b) delete space-indented blocks intelligently
- ;; c) do none of this when inside a string
- :i "SPC" #'doom/inflate-space-maybe
- :i [remap delete-backward-char] #'doom/deflate-space-maybe
- :i [remap newline] #'doom/newline-and-indent
-
- (:after org
- (:map org-mode-map
- :i [remap doom/inflate-space-maybe] #'org-self-insert-command
- :i "C-e" #'org-end-of-line
- :i "C-a" #'org-beginning-of-line))
-
- ;; Restore common editing keys (and ESC) in minibuffer
- (:map (minibuffer-local-map
- minibuffer-local-ns-map
- minibuffer-local-completion-map
- minibuffer-local-must-match-map
- minibuffer-local-isearch-map
- evil-ex-completion-map
- evil-ex-search-keymap
- read-expression-map)
- [escape] #'abort-recursive-edit
- "C-r" #'evil-paste-from-register
- "C-a" #'move-beginning-of-line
- "C-w" #'doom/minibuffer-kill-word
- "C-u" #'doom/minibuffer-kill-line
- "C-b" #'backward-word
- "C-f" #'forward-word
- "M-z" #'doom/minibuffer-undo)
-
- (:map messages-buffer-mode-map
- "M-;" #'eval-expression
- "A-;" #'eval-expression)
-
- (:map tabulated-list-mode-map
- [remap evil-record-macro] #'doom/popup-close-maybe)
-
- (:after view
- (:map view-mode-map "" #'View-quit-all)))
diff --git a/modules/private/default/+evil-commands.el b/modules/private/default/+evil-commands.el
deleted file mode 100644
index 306ea8834..000000000
--- a/modules/private/default/+evil-commands.el
+++ /dev/null
@@ -1,91 +0,0 @@
-;;; private/default/+evil-commands.el -*- lexical-binding: t; -*-
-
-(defalias 'ex! 'evil-ex-define-cmd)
-
- ;;; Commands defined elsewhere
-;;(ex! "al[ign]" #'+evil:align)
-;;(ex! "g[lobal]" #'+evil:global)
-
- ;;; Custom commands
-;; Editing
-(ex! "@" #'+evil:macro-on-all-lines) ; TODO Test me
-(ex! "al[ign]" #'+evil:align)
-(ex! "enhtml" #'+web:encode-html-entities)
-(ex! "dehtml" #'+web:decode-html-entities)
-(ex! "mc" #'+evil:mc)
-(ex! "iedit" #'evil-multiedit-ex-match)
-(ex! "na[rrow]" #'+evil:narrow-buffer)
-(ex! "retab" #'+evil:retab)
-;; External resources
-;; 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! "http" #'httpd-start) ; start http server
-(ex! "repl" #'+eval:repl) ; invoke or send to repl
-;; TODO (ex! "rx" 'doom:regex) ; open re-builder
-(ex! "sh[ell]" #'+eshell:run)
-(ex! "t[mux]" #'+tmux:run) ; send to tmux
-(ex! "tcd" #'+tmux:cd-here) ; cd to default-directory in tmux
-(ex! "x" #'doom/open-project-scratch-buffer)
-;; GIT
-(ex! "gist" #'+gist:send) ; send current buffer/region to gist
-(ex! "gistl" #'+gist:list) ; list gists by user
-(ex! "gbrowse" #'+vcs/git-browse) ; show file in github/gitlab
-(ex! "gissues" #'+vcs/git-browse-issues) ; show github issues
-(ex! "git" #'magit-status) ; open magit status window
-(ex! "gstage" #'magit-stage)
-(ex! "gunstage" #'magit-unstage)
-(ex! "gblame" #'magit-blame)
-(ex! "grevert" #'git-gutter:revert-hunk)
-;; Dealing with buffers
-(ex! "clean[up]" #'doom/cleanup-session)
-(ex! "k[ill]" #'doom/kill-this-buffer)
-(ex! "k[ill]all" #'+default:kill-all-buffers)
-(ex! "k[ill]m" #'+default:kill-matching-buffers)
-(ex! "k[ill]o" #'doom/kill-other-buffers)
-(ex! "l[ast]" #'doom/popup-restore)
-(ex! "m[sg]" #'view-echo-area-messages)
-(ex! "pop[up]" #'doom/popup-this-buffer)
-;; Project navigation
-(ex! "a" #'projectile-find-other-file)
-(ex! "cd" #'+default:cd)
-(cond ((featurep! :completion ivy)
- (ex! "ag" #'+ivy:ag)
- (ex! "agc[wd]" #'+ivy:ag-cwd)
- (ex! "rg" #'+ivy:rg)
- (ex! "rgc[wd]" #'+ivy:rg-cwd)
- (ex! "sw[iper]" #'+ivy:swiper)
- (ex! "todo" #'+ivy:todo))
- ((featurep! :completion helm)
- (ex! "ag" #'+helm:ag)
- (ex! "agc[wd]" #'+helm:ag-cwd)
- (ex! "rg" #'+helm:rg)
- (ex! "rgc[wd]" #'+helm:rg-cwd)
- (ex! "sw[oop]" #'+helm:swoop)
- (ex! "todo" #'+helm:todo)))
-;; Project tools
-(ex! "build" #'+eval/build)
-(ex! "debug" #'+debug/run)
-(ex! "er[rors]" #'flycheck-list-errors)
-;; File operations
-(ex! "cp" #'+evil:copy-this-file)
-(ex! "mv" #'+evil:move-this-file)
-(ex! "rm" #'+evil:delete-this-file)
-;; Sessions/tabs
-(ex! "sclear" #'+workspace/kill-session)
-(ex! "sl[oad]" #'+workspace:load-session)
-(ex! "ss[ave]" #'+workspace:save-session)
-(ex! "tabc[lose]" #'+workspace:delete)
-(ex! "tabclear" #'doom/kill-all-buffers)
-(ex! "tabl[ast]" #'+workspace/switch-to-last)
-(ex! "tabload" #'+workspace:load)
-(ex! "tabn[ew]" #'+workspace:new)
-(ex! "tabn[ext]" #'+workspace:switch-next)
-(ex! "tabp[rev]" #'+workspace:switch-previous)
-(ex! "tabr[ename]" #'+workspace:rename)
-(ex! "tabs" #'+workspace/display)
-(ex! "tabsave" #'+workspace:save)
-;; Org-mode
-(ex! "cap" #'+org-capture/dwim)
-
diff --git a/modules/private/default/autoload/default.el b/modules/private/default/autoload/default.el
deleted file mode 100644
index 4381c044b..000000000
--- a/modules/private/default/autoload/default.el
+++ /dev/null
@@ -1,47 +0,0 @@
-;; private/default/autoload/default.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun +default/yank-buffer-filename ()
- "Copy the current buffer's path to the kill ring."
- (interactive)
- (if-let* ((filename (or buffer-file-name (bound-and-true-p list-buffers-directory))))
- (message (kill-new (abbreviate-file-name filename)))
- (error "Couldn't find filename in current buffer")))
-
-;;;###autoload
-(defmacro +default--def-browse-in! (name dir)
- (let ((prefix (cdr (doom-module-from-path (or load-file-name byte-compile-current-file)))))
- `(defun ,(intern (format "%s/browse-%s" prefix name)) ()
- (interactive)
- (doom-project-browse ,dir))))
-
-;;;###autoload
-(defmacro +default--def-find-in! (name dir)
- (let ((prefix (cdr (doom-module-from-path (or load-file-name byte-compile-current-file)))))
- `(defun ,(intern (format "+%s/find-in-%s" prefix name)) ()
- (interactive)
- (doom-project-find-file ,dir))))
-
-
-;;;###autoload (autoload '+default/browse-project "private/default/autoload/default" nil t)
-(+default--def-browse-in! project (doom-project-root))
-
-;;;###autoload (autoload '+default/find-in-templates "private/default/autoload/default" nil t)
-(+default--def-find-in! templates +file-templates-dir)
-;;;###autoload (autoload '+default/browse-templates "private/default/autoload/default" nil t)
-(+default--def-browse-in! templates +file-templates-dir)
-
-;;;###autoload (autoload '+default/find-in-emacsd "private/default/autoload/default" nil t)
-(+default--def-find-in! emacsd doom-emacs-dir)
-;;;###autoload (autoload '+default/browse-emacsd "private/default/autoload/default" nil t)
-(+default--def-browse-in! emacsd doom-emacs-dir)
-
-;;;###autoload (autoload '+default/find-in-notes "private/default/autoload/default" nil t)
-(+default--def-find-in! notes +org-dir)
-;;;###autoload (autoload '+default/browse-notes "private/default/autoload/default" nil t)
-(+default--def-browse-in! notes +org-dir)
-
-;;;###autoload (autoload '+default/find-in-snippets "private/default/autoload/default" nil t)
-(+default--def-find-in! snippets +default-snippets-dir)
-;; NOTE No need for a browse-snippets variant, use `yas-visit-snippet-file'
-
diff --git a/modules/private/default/autoload/evil.el b/modules/private/default/autoload/evil.el
deleted file mode 100644
index 01a37f28c..000000000
--- a/modules/private/default/autoload/evil.el
+++ /dev/null
@@ -1,38 +0,0 @@
-;; private/default/autoload/evil.el -*- lexical-binding: t; -*-
-;;;###if (featurep! :feature evil)
-
-;;;###autoload (autoload '+default:multi-next-line "private/default/autoload/evil" nil t)
-(evil-define-motion +default:multi-next-line (count)
- "Move down 6 lines."
- :type line
- (let ((line-move-visual (or visual-line-mode (derived-mode-p 'text-mode))))
- (evil-line-move (* 6 (or count 1)))))
-
-;;;###autoload (autoload '+default:multi-previous-line "private/default/autoload/evil" nil t)
-(evil-define-motion +default:multi-previous-line (count)
- "Move up 6 lines."
- :type line
- (let ((line-move-visual (or visual-line-mode (derived-mode-p 'text-mode))))
- (evil-line-move (- (* 6 (or count 1))))))
-
-;;;###autoload (autoload '+default:cd "private/default/autoload/evil" nil t)
-(evil-define-command +default:cd ()
- "Change `default-directory' with `cd'."
- (interactive "")
- (cd input))
-
-;;;###autoload (autoload '+default:kill-all-buffers "private/default/autoload/evil" nil t)
-(evil-define-command +default:kill-all-buffers (&optional bang)
- "Kill all buffers. If BANG, kill current session too."
- (interactive "")
- (if bang
- (+workspace/kill-session)
- (doom/kill-all-buffers)))
-
-;;;###autoload (autoload '+default:kill-matching-buffers "private/default/autoload/evil" nil t)
-(evil-define-command +default:kill-matching-buffers (&optional bang pattern)
- "Kill all buffers matching PATTERN regexp. If BANG, only match project
-buffers."
- (interactive "")
- (doom/kill-matching-buffers pattern bang))
-
diff --git a/modules/private/default/config.el b/modules/private/default/config.el
deleted file mode 100644
index 4379a6955..000000000
--- a/modules/private/default/config.el
+++ /dev/null
@@ -1,80 +0,0 @@
-;;; private/default/config.el -*- lexical-binding: t; -*-
-
-(load! +bindings)
-
-
-;;
-;; Plugins
-;;
-
-(def-package! emacs-snippets :after yasnippet)
-
-
-;;
-;; Config
-;;
-
-(after! epa
- (setq epa-file-encrypt-to (or epa-file-encrypt-to user-mail-address)
- ;; With GPG 2.1, this forces gpg-agent to use the Emacs minibuffer to
- ;; prompt for the key passphrase.
- epa-pinentry-mode 'loopback))
-
-
-(when (featurep 'evil)
- (load! +evil-commands)
-
- ;; Makes ; and , the universal repeat-keys in evil-mode
- (defmacro do-repeat! (command next-func prev-func)
- "Repeat motions with ;/,"
- (let ((fn-sym (intern (format "+evil*repeat-%s" command))))
- `(progn
- (defun ,fn-sym (&rest _)
- (define-key evil-motion-state-map (kbd ";") ',next-func)
- (define-key evil-motion-state-map (kbd ",") ',prev-func))
- (advice-add #',command :before #',fn-sym))))
-
- ;; n/N
- (do-repeat! evil-ex-search-next evil-ex-search-next evil-ex-search-previous)
- (do-repeat! evil-ex-search-previous evil-ex-search-next evil-ex-search-previous)
- (do-repeat! evil-ex-search-forward evil-ex-search-next evil-ex-search-previous)
- (do-repeat! evil-ex-search-backward evil-ex-search-next evil-ex-search-previous)
-
- ;; f/F/t/T/s/S
- (after! evil-snipe
- (setq evil-snipe-repeat-keys nil
- evil-snipe-override-evil-repeat-keys nil) ; causes problems with remapped ;
-
- (do-repeat! evil-snipe-f evil-snipe-repeat evil-snipe-repeat-reverse)
- (do-repeat! evil-snipe-F evil-snipe-repeat evil-snipe-repeat-reverse)
- (do-repeat! evil-snipe-t evil-snipe-repeat evil-snipe-repeat-reverse)
- (do-repeat! evil-snipe-T evil-snipe-repeat evil-snipe-repeat-reverse)
- (do-repeat! evil-snipe-s evil-snipe-repeat evil-snipe-repeat-reverse)
- (do-repeat! evil-snipe-S evil-snipe-repeat evil-snipe-repeat-reverse)
- (do-repeat! evil-snipe-x evil-snipe-repeat evil-snipe-repeat-reverse)
- (do-repeat! evil-snipe-X evil-snipe-repeat evil-snipe-repeat-reverse))
-
- ;; */#
- (after! evil-visualstar
- (do-repeat! evil-visualstar/begin-search-forward
- evil-ex-search-next evil-ex-search-previous)
- (do-repeat! evil-visualstar/begin-search-backward
- evil-ex-search-previous evil-ex-search-next))
-
- (after! evil-easymotion
- (let ((prefix (concat doom-leader-key " /")))
- ;; NOTE `evilem-default-keybinds' unsets all other keys on the prefix (in
- ;; motion state)
- (evilem-default-keybindings prefix)
- (evilem-define (kbd (concat prefix " n")) #'evil-ex-search-next)
- (evilem-define (kbd (concat prefix " N")) #'evil-ex-search-previous)
- (evilem-define (kbd (concat prefix " s")) #'evil-snipe-repeat
- :pre-hook (save-excursion (call-interactively #'evil-snipe-s))
- :bind ((evil-snipe-scope 'buffer)
- (evil-snipe-enable-highlight)
- (evil-snipe-enable-incremental-highlight)))
- (evilem-define (kbd (concat prefix " S")) #'evil-snipe-repeat-reverse
- :pre-hook (save-excursion (call-interactively #'evil-snipe-s))
- :bind ((evil-snipe-scope 'buffer)
- (evil-snipe-enable-highlight)
- (evil-snipe-enable-incremental-highlight))))))
diff --git a/modules/private/default/packages.el b/modules/private/default/packages.el
deleted file mode 100644
index 08571bf1e..000000000
--- a/modules/private/default/packages.el
+++ /dev/null
@@ -1,7 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; private/default/packages.el
-
-(package! emacs-snippets
- :recipe (:fetcher github
- :repo "hlissner/emacs-snippets"
- :files ("*")))
diff --git a/modules/tools/ansible/config.el b/modules/tools/ansible/config.el
new file mode 100644
index 000000000..cb75a6dda
--- /dev/null
+++ b/modules/tools/ansible/config.el
@@ -0,0 +1,25 @@
+;;; tools/ansible/config.el -*- lexical-binding: t; -*-
+
+(def-package! ansible
+ :commands ansible::auto-decrypt-encrypt
+ :init
+ (put 'ansible::vault-password-file 'safe-local-variable #'stringp)
+ :config
+ (setq ansible::section-face 'font-lock-variable-name-face
+ ansible::task-label-face 'font-lock-doc-face)
+ (map! :map ansible::key-map
+ :localleader
+ :desc "Decrypt buffer" "d" #'ansible::decrypt-buffer
+ :desc "Encrypt buffer" "e" #'ansible::encrypt-buffer
+ :desc "Look up in Ansible docs" "h" #'ansible-doc))
+
+(after! ansible-doc
+ (set-evil-initial-state! '(ansible-doc-module-mode) 'emacs))
+
+(def-package! jinja2-mode
+ :mode "\\.j2$")
+
+(def-project-mode! +ansible-yaml-mode
+ :modes (yaml-mode)
+ :add-hooks (ansible ansible::auto-decrypt-encrypt ansible-doc-mode)
+ :files ("roles/"))
diff --git a/modules/tools/ansible/packages.el b/modules/tools/ansible/packages.el
new file mode 100644
index 000000000..0601eb054
--- /dev/null
+++ b/modules/tools/ansible/packages.el
@@ -0,0 +1,10 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/ansible/packages.el
+
+(package! yaml-mode)
+(package! ansible)
+(package! ansible-doc)
+(package! jinja2-mode)
+
+(when (featurep! :completion company)
+ (package! company-ansible))
diff --git a/modules/tools/dired/config.el b/modules/tools/dired/config.el
deleted file mode 100644
index e7e7f5657..000000000
--- a/modules/tools/dired/config.el
+++ /dev/null
@@ -1,61 +0,0 @@
-;;; tools/dired/config.el -*- lexical-binding: t; -*-
-
-(setq ;; Always copy/delete recursively
- dired-recursive-copies 'always
- dired-recursive-deletes 'top
- ;; Auto refresh dired, but be quiet about it
- global-auto-revert-non-file-buffers t
- auto-revert-verbose nil
- ;; files
- image-dired-dir (concat doom-cache-dir "image-dired/")
- image-dired-db-file (concat image-dired-dir "image-dired/db.el")
- image-dired-gallery-dir (concat image-dired-dir "gallery/")
- image-dired-temp-image-file (concat image-dired-dir "temp-image")
- image-dired-temp-rotate-image-file (concat image-dired-dir "temp-rotate-image"))
-
-(defun +dired|sort-directories-first ()
- "List directories first in dired buffers."
- (save-excursion
- (let (buffer-read-only)
- (forward-line 2) ;; beyond dir. header
- (sort-regexp-fields t "^.*$" "[ ]*." (point) (point-max))))
- (and (featurep 'xemacs)
- (fboundp 'dired-insert-set-properties)
- (dired-insert-set-properties (point-min) (point-max)))
- (set-buffer-modified-p nil))
-(add-hook 'dired-after-readin-hook #'+dired|sort-directories-first)
-
-;; Automatically create missing directories when creating new files
-(defun +dired|create-non-existent-directory ()
- (let ((parent-directory (file-name-directory buffer-file-name)))
- (when (and (not (file-exists-p parent-directory))
- (y-or-n-p (format "Directory `%s' does not exist! Create it?" parent-directory)))
- (make-directory parent-directory t))))
-(push #'+dired|create-non-existent-directory find-file-not-found-functions)
-
-(after! evil
- (add-transient-hook! 'dired-mode-hook
- (map! :map dired-mode-map
- :n "c" #'find-file
- :n "d" #'dired-do-delete
- :n "r" #'dired-do-rename)))
-
-
-;;
-;; Packages
-;;
-
-(def-package! dired-k
- :after dired
- :config
- (setq dired-k-style 'git)
-
- (defun +dired*dired-k-highlight (orig-fn &rest args)
- "Butt out if the requested directory is remote (i.e. through tramp)."
- (unless (file-remote-p default-directory)
- (apply orig-fn args)))
- (advice-add #'dired-k--highlight :around #'+dired*dired-k-highlight)
-
- (add-hook 'dired-initial-position-hook #'dired-k)
- (add-hook 'dired-after-readin-hook #'dired-k-no-revert))
-
diff --git a/modules/tools/dired/packages.el b/modules/tools/dired/packages.el
deleted file mode 100644
index c1ed6be70..000000000
--- a/modules/tools/dired/packages.el
+++ /dev/null
@@ -1,5 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; emacs/dired/packages.el
-
-(package! dired-k)
-
diff --git a/modules/tools/direnv/config.el b/modules/tools/direnv/config.el
new file mode 100644
index 000000000..7715710fa
--- /dev/null
+++ b/modules/tools/direnv/config.el
@@ -0,0 +1,7 @@
+;;; tools/direnv/config.el -*- lexical-binding: t; -*-
+
+(def-package! direnv
+ :after-call (after-find-file dired-initial-position-hook)
+ :config
+ (when (executable-find "direnv")
+ (direnv-mode +1)))
diff --git a/modules/tools/direnv/doctor.el b/modules/tools/direnv/doctor.el
new file mode 100644
index 000000000..223fcb967
--- /dev/null
+++ b/modules/tools/direnv/doctor.el
@@ -0,0 +1,4 @@
+;;; tools/direnv/doctor.el -*- lexical-binding: t; -*-
+
+(unless (executable-find "direnv")
+ (warn! "Couldn't find direnv executable"))
diff --git a/modules/tools/direnv/packages.el b/modules/tools/direnv/packages.el
new file mode 100644
index 000000000..a0c843e53
--- /dev/null
+++ b/modules/tools/direnv/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/direnv/packages.el
+
+(package! direnv)
diff --git a/modules/tools/docker/config.el b/modules/tools/docker/config.el
new file mode 100644
index 000000000..cf3c8a32f
--- /dev/null
+++ b/modules/tools/docker/config.el
@@ -0,0 +1,10 @@
+;;; tools/docker/config.el -*- lexical-binding: t; -*-
+
+(after! docker
+ (set-evil-initial-state!
+ '(docker-container-mode
+ docker-image-mode
+ docker-network-mode
+ docker-volume-mode
+ docker-machine-mode)
+ 'emacs))
diff --git a/modules/tools/docker/packages.el b/modules/tools/docker/packages.el
new file mode 100644
index 000000000..9266beee4
--- /dev/null
+++ b/modules/tools/docker/packages.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/docker/packages.el
+
+(package! docker)
+(package! docker-tramp)
+(package! dockerfile-mode)
diff --git a/modules/tools/editorconfig/README.org b/modules/tools/editorconfig/README.org
new file mode 100644
index 000000000..f7a7d37a5
--- /dev/null
+++ b/modules/tools/editorconfig/README.org
@@ -0,0 +1,18 @@
+#+TITLE: :tools editorconfig
+
+Editorconfig integration for Doom.
+
+* Table of Contents :TOC:
+- [[Module Flags][Module Flags]]
+- [[Prerequisites][Prerequisites]]
+- [[Configuration][Configuration]]
+
+* Module Flags
+This module provides no flags.
+
+* Prerequisites
+~editorconfig~ is an optional requirement of this package. The elisp-only
+implementation may be sufficient, but has fewer features.
+
+* Configuration
+
diff --git a/modules/tools/editorconfig/config.el b/modules/tools/editorconfig/config.el
new file mode 100644
index 000000000..e200d9fc0
--- /dev/null
+++ b/modules/tools/editorconfig/config.el
@@ -0,0 +1,60 @@
+;;; tools/editorconfig/config.el -*- lexical-binding: t; -*-
+
+;; editorconfig cannot procure the correct settings for extension-less files.
+;; Executable scripts with a shebang line, for example. So why not use Emacs'
+;; major mode to drop editorconfig a hint? This is accomplished by temporarily
+;; appending an extension to `buffer-file-name' when we talk to editorconfig.
+(defvar +editorconfig-mode-alist
+ '((sh-mode . "sh")
+ (python-mode . "py")
+ (ruby-mode . "rb")
+ (enh-ruby-mode . "rb")
+ (perl-mode . "pl")
+ (php-mode . "php"))
+ "An alist mapping major modes to extensions. Used by
+`doom*editorconfig-smart-detection' to give editorconfig filetype hints.")
+
+
+;; Handles whitespace (tabs/spaces) settings externally. This way projects can
+;; specify their own formatting rules.
+(def-package! editorconfig
+ :after-call (doom-switch-buffer-hook after-find-file)
+ :config
+ (defun doom*editorconfig-smart-detection (orig-fn)
+ "Retrieve the properties for the current file. If it doesn't have an
+extension, try to guess one."
+ (let ((buffer-file-name
+ (if (and (not (bound-and-true-p org-src-mode))
+ (file-name-extension buffer-file-name))
+ buffer-file-name
+ (format "%s%s" buffer-file-name
+ (if-let* ((ext (cdr (assq major-mode +editorconfig-mode-alist))))
+ (concat "." ext)
+ "")))))
+ (funcall orig-fn)))
+ (advice-add #'editorconfig-call-editorconfig-exec :around #'doom*editorconfig-smart-detection)
+
+ (defun +editorconfig|disable-ws-butler-maybe (props)
+ "Disable `ws-butler-mode' if trim_trailing_whitespace is true."
+ (when (and (equal (gethash 'trim_trailing_whitespace props) "true")
+ (bound-and-true-p ws-butler-mode))
+ (ws-butler-mode -1)))
+ (add-hook 'editorconfig-after-apply-functions #'+editorconfig|disable-ws-butler-maybe)
+
+ (defun +editorconfig|disable-indent-detection (props)
+ "Inhibit `dtrt-indent' if an explicit indent_style and indent_size is
+specified by editorconfig."
+ (when (or (gethash 'indent_style props)
+ (gethash 'indent_size props))
+ (setq doom-inhibit-indent-detection 'editorconfig)))
+ (add-hook 'editorconfig-after-apply-functions #'+editorconfig|disable-indent-detection)
+
+ ;; Editorconfig makes indentation too rigid in Lisp modes, so tell
+ ;; editorconfig to ignore indentation there. The dynamic indentation support
+ ;; built into Emacs is superior.
+ (dolist (mode '(emacs-lisp-mode lisp-mode))
+ (delq (assq mode editorconfig-indentation-alist)
+ editorconfig-indentation-alist))
+
+ ;;
+ (editorconfig-mode +1))
diff --git a/modules/tools/editorconfig/doctor.el b/modules/tools/editorconfig/doctor.el
new file mode 100644
index 000000000..2ad8898e8
--- /dev/null
+++ b/modules/tools/editorconfig/doctor.el
@@ -0,0 +1,5 @@
+;; -*- lexical-binding: t; no-byte-compile: t; -*-
+;;; tools/editorconfig/doctor.el
+
+(unless (executable-find "editorconfig")
+ (warn! "Couldn't find the editorconfig binary. Using native elisp version (slower)"))
diff --git a/modules/tools/editorconfig/packages.el b/modules/tools/editorconfig/packages.el
new file mode 100644
index 000000000..716c3b55e
--- /dev/null
+++ b/modules/tools/editorconfig/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/editorconfig/packages.el
+
+(package! editorconfig)
diff --git a/modules/tools/ein/README.org b/modules/tools/ein/README.org
new file mode 100644
index 000000000..7f1c3147a
--- /dev/null
+++ b/modules/tools/ein/README.org
@@ -0,0 +1,53 @@
+#+TITLE: tools/ein
+#+DATE: April 11, 2018
+#+SINCE: v2.0
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Prerequisites][Prerequisites]]
+- [[Features][Features]]
+ - [[Interaction with a Jupyter server][Interaction with a Jupyter server]]
+- [[Configuration][Configuration]]
+ - [[Setting the default location of your notebooks][Setting the default location of your notebooks]]
+
+* Description
+Adds Jupyter notebook integration into emacs.
+
+** Module Flags
+This module provides no flags.
+
+** Plugins
++ [[https://github.com/millejoh/emacs-ipython-notebook][ein]]
+
+* Prerequisites
+This module has no prereqisites.
+
+* Features
+** Interaction with a Jupyter server
+Three functions are available to start EIN:
+
+ 1. ~ein:jupyter-server-start~ --- Start a jupyter server within emacs
+ 2. ~ein:notebooklist-login~ --- Login to an existing jupyter server
+ 3. ~ein:notebooklist-open~ --- Open the list of jupyter notebooks
+
+These functions do not have default key bindings.
+
+When ~ein:jupyter-server-start~ is called, after successfully finishing,
+~ein:notebooklist-login~ and ~ein:notebooklist-open~ will be automatically
+called.
+
+When in the ~Notebook List~ buffer, the key =o= calls ~ace-link~ to speed up the
+process of selecting links in the buffer.
+
+If ~company-mode~ is enabled as a module, ~company-ein~ will handle completion.
+
+* Configuration
+** Setting the default location of your notebooks
+Change ~+ein-notebook-dir~ to tell ein where to find your Jupityr notebooks.
+
+#+BEGIN_SRC emacs-lisp
+(setq +ein-notebook-dir "~/my-notebooks")
+#+END_SRC
diff --git a/modules/tools/ein/autoload.el b/modules/tools/ein/autoload.el
new file mode 100644
index 000000000..0dd10d81a
--- /dev/null
+++ b/modules/tools/ein/autoload.el
@@ -0,0 +1,69 @@
+;;; tools/ein/autoload.el -*- lexical-binding: t; -*-
+
+(defun +ein--collect-ein-buffer-links ()
+ (let ((end (window-end))
+ points)
+ (save-excursion
+ (goto-char (window-start))
+ (while (re-search-forward "~?/.+\\|\s\\[" end t)
+ (push (+ (match-beginning 0) 1) points))
+ (nreverse points))))
+
+;;;###autoload
+(defun +ein/ace-link-ein ()
+ "Ace jump to links in ein notebooklist."
+ (interactive)
+ (require 'avy)
+ (let ((res (avy-with +ein/ace-link-ein
+ (avy--process
+ (+ein--collect-ein-buffer-links)
+ #'avy--overlay-pre))))
+ ;(avy--style-fn avy-style)))))
+ (when (numberp res)
+ (goto-char (1+ res))
+ (widget-button-press (point)))))
+
+;;;###autoload (autoload '+ein-hydra/body "tools/ein/autoload" nil t)
+(defhydra +ein-hydra (:hint t :color red)
+ "
+ Operations on Cells^^^^^^ Other
+ ----------------------------^^^^^^ ----------------------------------^^^^
+ [_k_/_j_]^^ select prev/next [_t_]^^ toggle output
+ [_K_/_J_]^^ move up/down [_C-l_/_C-S-l_] clear/clear all output
+ [_C-k_/_C-j_]^^ merge above/below [_C-o_]^^ open console
+ [_O_/_o_]^^ insert above/below [_C-s_/_C-r_] save/rename notebook
+ [_y_/_p_/_d_] copy/paste [_x_]^^ close notebook
+ [_u_]^^^^ change type [_q_]^^ quit
+ [_RET_]^^^^ execute
+"
+ ("q" nil :exit t)
+ ("h" ein:notebook-worksheet-open-prev-or-last)
+ ("j" ein:worksheet-goto-next-input)
+ ("k" ein:worksheet-goto-prev-input)
+ ("l" ein:notebook-worksheet-open-next-or-first)
+ ("H" ein:notebook-worksheet-move-prev)
+ ("J" ein:worksheet-move-cell-down)
+ ("K" ein:worksheet-move-cell-up)
+ ("L" ein:notebook-worksheet-move-next)
+ ("t" ein:worksheet-toggle-output)
+ ("d" ein:worksheet-kill-cell)
+ ("R" ein:worksheet-rename-sheet)
+ ("y" ein:worksheet-copy-cell)
+ ("p" ein:worksheet-yank-cell)
+ ("o" ein:worksheet-insert-cell-below)
+ ("O" ein:worksheet-insert-cell-above)
+ ("u" ein:worksheet-change-cell-type)
+ ("RET" ein:worksheet-execute-cell-and-goto-next)
+ ;; Output
+ ("C-l" ein:worksheet-clear-output)
+ ("C-S-l" ein:worksheet-clear-all-output)
+ ;;Console
+ ("C-o" ein:console-open :exit t)
+ ;; Merge and split cells
+ ("C-k" ein:worksheet-merge-cell)
+ ("C-j" spacemacs/ein:worksheet-merge-cell-next)
+ ("s" ein:worksheet-split-cell-at-point)
+ ;; Notebook
+ ("C-s" ein:notebook-save-notebook-command)
+ ("C-r" ein:notebook-rename-command)
+ ("x" ein:notebook-close :exit t))
diff --git a/modules/tools/ein/config.el b/modules/tools/ein/config.el
new file mode 100644
index 000000000..c986cfd71
--- /dev/null
+++ b/modules/tools/ein/config.el
@@ -0,0 +1,43 @@
+;;; tools/ein/config.el -*- lexical-binding: t; -*-
+
+(defvar +ein-notebook-dir "~/"
+ "Default directory from where Jupyter notebooks are to be opened.")
+
+
+;;
+;; Packages
+
+(after! ein
+ (setq ein:notebook-modes
+ '(ein:notebook-multilang-mode
+ ein:notebook-python-mode
+ ein:notebook-plain-mode)
+ ;; Slice images into rows; easier to navigate around images
+ ein:slice-image t)
+
+ (set-popup-rules!
+ '(("\\*ein: .*" :ignore t)
+ ("\\*ein:tb .*" :side 'bottom :size 0.3 :quit t :ttl nil :select nil)
+ ("\\*ein:notebooklist *" :side 'left :size 50 :select nil)))
+
+ (when (featurep! :completion company)
+ ;; Code completion with company
+ (setq ein:completion-backend 'ein:use-company-backend)
+ (set-company-backend! '(ein:notebook-multilang-mode
+ ein:notebook-python-mode
+ ein:notebook-plain-mode)
+ 'ein:company-backend))
+
+ (after! ein-jupyter
+ (setq ein:jupyter-server-args '("--no-browser"))
+ (unless ein:jupyter-default-notebook-directory
+ (setq ein:jupyter-default-notebook-directory "~/")))
+
+ (defun +ein-buffer-p (buf)
+ (string-match-p "^\\*ein: .*" (buffer-name buf)))
+ (add-to-list 'doom-real-buffer-functions #'+ein-buffer-p nil #'eq)
+
+ (map! :map ein:notebook-mode-map
+ "M-s" #'ein:notebook-save-notebook-command
+ :map ein:notebooklist-mode-map
+ "o" #'+ein/ace-link-ein))
diff --git a/modules/tools/ein/packages.el b/modules/tools/ein/packages.el
new file mode 100644
index 000000000..19ab85d38
--- /dev/null
+++ b/modules/tools/ein/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/ein/packages.el
+
+(package! ein)
diff --git a/modules/tools/electric-indent/config.el b/modules/tools/electric-indent/config.el
deleted file mode 100644
index 809b07800..000000000
--- a/modules/tools/electric-indent/config.el
+++ /dev/null
@@ -1,36 +0,0 @@
-;;; tools/electric-indent/config.el -*- lexical-binding: t; -*-
-
-;; Smarter, keyword-based electric-indent
-
-(defvar doom-electric-indent-p nil
- "TODO")
-
-(defvar-local doom-electric-indent-words '()
- "TODO")
-
-(setq-default electric-indent-chars '(?\n ?\^?))
-
-(defun +electric-indent|char (_c)
- (when (and (eolp) doom-electric-indent-words)
- (save-excursion
- (backward-word)
- (looking-at-p
- (concat "\\<" (regexp-opt doom-electric-indent-words))))))
-(push #'+electric-indent|char electric-indent-functions)
-
-(def-setting! :electric (modes &rest plist)
- "Declare :words (list of strings) or :chars (lists of chars) in MODES that
-trigger electric indentation."
- (declare (indent 1))
- (let ((modes (doom-enlist (doom-unquote modes)))
- (chars (doom-unquote (plist-get plist :chars)))
- (words (doom-unquote (plist-get plist :words))))
- (when (or chars words)
- (let ((fn-name (intern (format "doom--init-electric-%s" (mapconcat #'symbol-name modes "-")))))
- `(progn
- (defun ,fn-name ()
- (electric-indent-local-mode +1)
- ,@(if chars `((setq electric-indent-chars ',chars)))
- ,@(if words `((setq doom-electric-indent-words ',words))))
- (add-hook! ,modes #',fn-name))))))
-
diff --git a/modules/tools/eshell/autoload/eshell.el b/modules/tools/eshell/autoload/eshell.el
deleted file mode 100644
index 84a8e54b8..000000000
--- a/modules/tools/eshell/autoload/eshell.el
+++ /dev/null
@@ -1,169 +0,0 @@
-;;; tools/eshell/autoload/eshell.el -*- lexical-binding: t; -*-
-
-(defvar +eshell-buffers ()
- "List of open eshell buffers.")
-
-(defvar +eshell-buffer-name "*doom:eshell*"
- "The name to use for custom eshell buffers. This only affects `+eshell/open',
-`+eshell/open-popup' and `+eshell/open-workspace'.")
-
-
-;; --- Commands ---------------------------
-
-;;;###autoload
-(defun +eshell/open (&optional command)
- "Open eshell in the current buffer."
- (interactive)
- (let ((buf (generate-new-buffer +eshell-buffer-name)))
- (with-current-buffer buf
- (unless (eq major-mode 'eshell-mode) (eshell-mode)))
- (switch-to-buffer buf)
- (when command
- (+eshell-run-command command))))
-
-;;;###autoload
-(defun +eshell/open-popup (&optional command)
- "Open eshell in a popup window."
- (interactive)
- (let ((buf (get-buffer-create +eshell-buffer-name)))
- (with-current-buffer buf
- (unless (eq major-mode 'eshell-mode) (eshell-mode)))
- (doom-popup-buffer buf '(:autokill t) t)
- (when command
- (+eshell-run-command command))))
-
-;;;###autoload
-(defun +eshell/open-workspace (&optional command)
- "Open eshell in a separate workspace. Requires the (:feature workspaces)
-module to be loaded."
- (interactive)
- (unless (featurep! :feature workspaces)
- (user-error ":feature workspaces is required, but disabled"))
- (unless (+workspace-get "eshell" t)
- (+workspace/new "eshell"))
- (if-let* ((buf (cl-find-if (lambda (it) (string-match-p "^\\*doom:eshell" (buffer-name (window-buffer it))))
- (doom-visible-windows))))
- (select-window (get-buffer-window buf))
- (+eshell/open))
- (doom/workspace-display)
- (when command
- (+eshell-run-command command)))
-
-(defun +eshell-run-command (command)
- (unless (cl-remove-if-not #'buffer-live-p +eshell-buffers)
- (user-error "No living eshell buffers available"))
- (with-current-buffer (car +eshell-buffers)
- (goto-char eshell-last-output-end)
- (when (bound-and-true-p evil-mode)
- (call-interactively #'evil-append-line))
- (insert command)
- (eshell-send-input nil t)))
-
-
-;; --- Hooks ------------------------------
-
-;;;###autoload
-(defun +eshell|init ()
- "Keep track of eshell buffers."
- (cl-pushnew (current-buffer) +eshell-buffers :test #'eq))
-
-;;;###autoload
-(defun +eshell|cleanup ()
- "Close window (or workspace) on quit."
- (setq +eshell-buffers (delete (current-buffer) +eshell-buffers))
- (when (and (featurep! :feature workspaces)
- (string= "eshell" (+workspace-current-name)))
- (+workspace/delete "eshell")))
-
-
-;; --- Keybindings ------------------------
-
-;;;###autoload
-(defun +eshell/quit-or-delete-char (arg)
- (interactive "p")
- (if (and (eolp) (looking-back eshell-prompt-regexp nil))
- (eshell-life-is-too-much)
- (delete-char arg)))
-
-(defun +eshell--outside-prompt-p ()
- (< (point) eshell-last-output-end))
-
-(defun +eshell--current-git-branch ()
- (let ((branch (car (loop for match in (split-string (shell-command-to-string "git branch") "\n")
- when (string-match "^\*" match)
- collect match))))
- (if (not (eq branch nil))
- (concat " [" (substring branch 2) "]")
- "")))
-
-;;;###autoload
-(defun +eshell/split ()
- (interactive)
- (select-window (split-window-vertically))
- (+eshell/open))
-
-;;;###autoload
-(defun +eshell/vsplit ()
- (interactive)
- (select-window (split-window-horizontally))
- (+eshell/open))
-
-;;;###autoload
-(defun +eshell-prompt ()
- (concat (propertize (abbreviate-file-name (eshell/pwd)) 'face 'eshell-prompt)
- (propertize (+eshell--current-git-branch) 'face 'font-lock-function-name-face)
- (propertize " λ " 'face 'font-lock-constant-face)))
-
-;;;###autoload
-(defun +eshell/evil-append ()
- (interactive)
- (goto-char eshell-last-output-end)
- (call-interactively #'evil-append-line))
-
-;;;###autoload
-(defun +eshell/evil-append-maybe ()
- (interactive)
- (if (+eshell--outside-prompt-p)
- (+eshell/evil-append)
- (call-interactively #'evil-append)))
-
-;;;###autoload
-(defun +eshell/evil-prepend ()
- (interactive)
- (goto-char eshell-last-output-end)
- (call-interactively #'evil-insert))
-
-;;;###autoload
-(defun +eshell/evil-prepend-maybe ()
- (interactive)
- (if (+eshell--outside-prompt-p)
- (+eshell/evil-prepend)
- (call-interactively #'evil-insert)))
-
-;;;###autoload
-(defun +eshell/evil-replace-maybe ()
- (interactive)
- (if (+eshell--outside-prompt-p)
- (user-error "Cannot edit read-only region")
- (call-interactively #'evil-replace)))
-
-;;;###autoload
-(defun +eshell/evil-replace-state-maybe ()
- (interactive)
- (if (+eshell--outside-prompt-p)
- (user-error "Cannot edit read-only region")
- (call-interactively #'evil-replace-state)))
-
-;;;###autoload
-(defun +eshell/evil-change ()
- (interactive)
- (when (+eshell--outside-prompt-p)
- (goto-char eshell-last-output-end))
- (call-interactively #'evil-change))
-
-;;;###autoload
-(defun +eshell/evil-change-line ()
- (interactive)
- (when (+eshell--outside-prompt-p)
- (goto-char eshell-last-output-end))
- (call-interactively #'evil-change-line))
diff --git a/modules/tools/eshell/autoload/evil.el b/modules/tools/eshell/autoload/evil.el
deleted file mode 100644
index 0f8d18316..000000000
--- a/modules/tools/eshell/autoload/evil.el
+++ /dev/null
@@ -1,11 +0,0 @@
-;;; tools/eshell/autoload/evil.el -*- lexical-binding: t; -*-
-;;;###if (featurep! :feature evil)
-
-;;;###autoload (autoload '+eshell:run "tools/eshell/autoload/evil" nil t)
-(evil-define-command +eshell:run (command bang)
- ;; TODO Add COMMAND support
- (interactive "")
- (if bang
- (+eshell/open command)
- (+eshell/open-popup command)))
-
diff --git a/modules/tools/eshell/config.el b/modules/tools/eshell/config.el
deleted file mode 100644
index 49efa7074..000000000
--- a/modules/tools/eshell/config.el
+++ /dev/null
@@ -1,78 +0,0 @@
-;;; tools/eshell/config.el -*- lexical-binding: t; -*-
-
-;; This is highly experimental. I don't use eshell often, so this may need work.
-
-;; see:
-;; + `+eshell/open': open in current buffer
-;; + `+eshell/open-popup': open in a popup
-;; + `+eshell/open-workspace': open in separate tab (requires :feature workspaces)
-
-(def-package! eshell ; built-in
- :commands eshell-mode
- :init
- (setq eshell-directory-name (concat doom-etc-dir "/eshell")
- eshell-scroll-to-bottom-on-input 'all
- eshell-scroll-to-bottom-on-output 'all
- eshell-buffer-shorthand t
- eshell-kill-processes-on-exit t
- ;; em-prompt
- eshell-prompt-regexp "^.* λ "
- eshell-prompt-function #'+eshell-prompt
- ;; em-glob
- eshell-glob-case-insensitive t
- eshell-error-if-no-glob t
- ;; em-alias
- eshell-aliases-file (concat doom-local-dir ".eshell-aliases"))
-
- :config
- (set! :evil-state 'eshell-mode 'insert)
-
- ;; Keep track of open eshell buffers
- (add-hook 'eshell-mode-hook #'+eshell|init)
- (add-hook 'eshell-exit-hook #'+eshell|cleanup)
-
- (after! em-term
- ;; Visual commands require a proper terminal. Eshell can't handle that, so it
- ;; delegates these commands to a term buffer.
- (nconc eshell-visual-commands '("tmux" "htop" "bash" "zsh" "fish" "vim" "nvim"))
- (setq eshell-visual-subcommands '(("git" "log" "l" "diff" "show"))))
-
- (defun +eshell|init-keymap ()
- "Setup eshell keybindings. This must be done in a hook because eshell-mode
-redefines its keys every time `eshell-mode' is enabled."
- (map! :map eshell-mode-map
- :n "i" #'+eshell/evil-prepend-maybe
- :n "I" #'+eshell/evil-prepend
- :n "a" #'+eshell/evil-append-maybe
- :n "A" #'+eshell/evil-append
- :n "r" #'+eshell/evil-replace-maybe
- :n "R" #'+eshell/evil-replace-state-maybe
- :n "c" #'+eshell/evil-change
- :n "C" #'+eshell/evil-change-line
- :i [tab] #'eshell-pcomplete
- :i "SPC" #'self-insert-command
- :i "C-u" #'eshell-kill-input
- :i "C-a" #'eshell-bol
- :i "C-d" #'+eshell/quit-or-delete-char
- :i "C-k" #'kill-line
- :i "C-p" #'eshell-previous-input
- :i "" #'eshell-previous-input
- :i "C-n" #'eshell-next-input
- :i "" #'eshell-next-input
- :m "" #'+eshell/evil-append
- :n [remap evil-window-split] #'+eshell/split
- :n [remap evil-window-vsplit] #'+eshell/vsplit
- :n [remap evil-record-macro] #'eshell-life-is-too-much
- [remap kill-this-buffer] #'eshell-life-is-too-much
- [remap +workspace/close-window-or-workspace] #'eshell-life-is-too-much))
- (add-hook 'eshell-mode-hook #'+eshell|init-keymap)
-
- ;; Aliases
- (setq eshell-command-aliases-list
- '(("q" "exit")
- ("l" "ls -1")
- ("ll" "ls -l")
- ("la" "ls -la")
- ("g" "hub")
- ("gs" "hub status --short ."))))
-
diff --git a/modules/tools/flycheck/autoload.el b/modules/tools/flycheck/autoload.el
new file mode 100644
index 000000000..cc8167c4e
--- /dev/null
+++ b/modules/tools/flycheck/autoload.el
@@ -0,0 +1,13 @@
+;;; tools/flycheck/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +flycheck|init-popups ()
+ "Activate `flycheck-posframe-mode' if available and in GUI Emacs.
+Activate `flycheck-popup-tip-mode' otherwise.
+Do nothing if `lsp-ui-mode' is active and `lsp-ui-sideline-enable' is non-nil."
+ (unless (and (bound-and-true-p lsp-ui-mode)
+ lsp-ui-sideline-enable)
+ (if (and (fboundp 'flycheck-posframe-mode)
+ (display-graphic-p))
+ (flycheck-posframe-mode +1)
+ (flycheck-popup-tip-mode +1))))
diff --git a/modules/tools/flycheck/config.el b/modules/tools/flycheck/config.el
new file mode 100644
index 000000000..b7f79b7b7
--- /dev/null
+++ b/modules/tools/flycheck/config.el
@@ -0,0 +1,54 @@
+;;; tools/flycheck/config.el -*- lexical-binding: t; -*-
+
+(defvar +flycheck-on-escape t
+ "If non-nil, flycheck will recheck the current buffer when pressing ESC/C-g.")
+
+
+;;
+;;; Packages
+
+(def-package! flycheck
+ :commands (flycheck-list-errors flycheck-buffer)
+ :after-call (doom-switch-buffer-hook after-find-file)
+ :config
+ ;; new-line checks are a mote excessive; idle checks are more than enough
+ (setq flycheck-check-syntax-automatically
+ (delq 'new-line flycheck-check-syntax-automatically))
+
+ (defun +flycheck|buffer ()
+ "Flycheck buffer on ESC in normal mode."
+ (when (and flycheck-mode +flycheck-on-escape)
+ (ignore-errors (flycheck-buffer))
+ nil))
+ (add-hook 'doom-escape-hook #'+flycheck|buffer t)
+
+ (defun +flycheck|adjust-syntax-check-eagerness ()
+ "Check for errors less often when there aren't any.
+Done to reduce the load flycheck imposes on the current buffer."
+ (if flycheck-current-errors
+ (kill-local-variable 'flycheck-idle-change-delay)
+ (setq-local flycheck-idle-change-delay 3.0)))
+ (add-hook 'flycheck-after-syntax-check-hook #'+flycheck|adjust-syntax-check-eagerness)
+
+ (global-flycheck-mode +1))
+
+
+(def-package! flycheck-popup-tip
+ :commands (flycheck-popup-tip-show-popup flycheck-popup-tip-delete-popup)
+ :init (add-hook 'flycheck-mode-hook #'+flycheck|init-popups)
+ :config
+ (setq flycheck-popup-tip-error-prefix "✕ ")
+ (after! evil
+ ;; Don't display errors while in insert mode, as it can affect the cursor's
+ ;; position or cause disruptive input delays.
+ (add-hook 'flycheck-posframe-inhibit-functions #'evil-insert-state-p)))
+
+
+(def-package! flycheck-posframe
+ :when (and EMACS26+ (featurep! +childframe))
+ :defer t
+ :init (add-hook 'flycheck-mode-hook #'+flycheck|init-popups)
+ :config
+ (setq flycheck-posframe-warning-prefix "⚠ "
+ flycheck-posframe-info-prefix "··· "
+ flycheck-posframe-error-prefix "✕ "))
diff --git a/modules/tools/flycheck/packages.el b/modules/tools/flycheck/packages.el
new file mode 100644
index 000000000..e5e2f0885
--- /dev/null
+++ b/modules/tools/flycheck/packages.el
@@ -0,0 +1,7 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/flycheck/packages.el
+
+(package! flycheck)
+(package! flycheck-popup-tip)
+(when (and EMACS26+ (featurep! +childframe))
+ (package! flycheck-posframe))
diff --git a/modules/tools/flyspell/autoload.el b/modules/tools/flyspell/autoload.el
new file mode 100644
index 000000000..4e6b162cf
--- /dev/null
+++ b/modules/tools/flyspell/autoload.el
@@ -0,0 +1,27 @@
+;;; tools/flyspell/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autodef
+(defalias 'flyspell-mode! #'flyspell-mode)
+
+(defvar +flyspell--predicate-alist nil
+ "TODO")
+
+;;;###autodef
+(defun set-flyspell-predicate! (modes predicate)
+ "TODO"
+ (declare (indent defun))
+ (dolist (mode (doom-enlist modes) +flyspell--predicate-alist)
+ (add-to-list '+flyspell--predicate-alist (cons mode predicate))))
+
+;;;###autoload
+(defun +flyspell|init-predicate ()
+ "TODO"
+ (when-let* ((pred (assq major-mode +flyspell--predicate-alist)))
+ (setq-local flyspell-generic-check-word-predicate (cdr pred))))
+
+;;;###autoload
+(defun +flyspell-correction-at-point-p (&optional point)
+ "TODO"
+ (cl-loop for ov in (overlays-at (or point (point)))
+ if (overlay-get ov 'flyspell-overlay)
+ return t))
diff --git a/modules/tools/flyspell/config.el b/modules/tools/flyspell/config.el
new file mode 100644
index 000000000..2893f0b67
--- /dev/null
+++ b/modules/tools/flyspell/config.el
@@ -0,0 +1,94 @@
+;;; tools/flyspell/config.el -*- lexical-binding: t; -*-
+
+(defvar-local +flyspell-immediately t
+ "If non-nil, spellcheck the current buffer upon starting `flyspell-mode'.
+
+Since spellchecking can be slow in some buffers, this can be disabled with:
+
+ (setq-hook! 'TeX-mode-hook +flyspell-immediately nil)")
+
+
+;;
+;; Packages
+
+(after! ispell
+ (setq-default ispell-dictionary "english")
+ (add-to-list 'ispell-extra-args "--dont-tex-check-comments")
+
+ ;; Enable either aspell or hunspell.
+ ;; If no module flags are given, enable either aspell or hunspell if their
+ ;; binary is found.
+ ;; If one of the flags `+aspell' or `+hunspell' is given, only enable that
+ ;; spell checker.
+ (pcase (cond ((featurep! +aspell) 'aspell)
+ ((featurep! +hunspell) 'hunspell)
+ ((executable-find "aspell") 'aspell)
+ ((executable-find "hunspell") 'hunspell))
+ (`aspell
+ (setq ispell-program-name "aspell"
+ ispell-extra-args '("--sug-mode=ultra" "--run-together"))
+
+ (defun +flyspell|remove-run-together-switch-for-aspell ()
+ (setq-local ispell-extra-args (remove "--run-together" ispell-extra-args)))
+ (add-hook 'text-mode-hook #'+flyspell|remove-run-together-switch-for-aspell)
+
+ (defun +flyspell*setup-ispell-extra-args (orig-fun &rest args)
+ (let ((ispell-extra-args (remove "--run-together" ispell-extra-args)))
+ (ispell-kill-ispell t)
+ (apply orig-fun args)
+ (ispell-kill-ispell t)))
+ (advice-add #'ispell-word :around #'+flyspell*setup-ispell-extra-args)
+ (advice-add #'flyspell-auto-correct-word :around #'+flyspell*setup-ispell-extra-args))
+
+ (`hunspell
+ (setq ispell-program-name "hunspell"
+ ;; Don't use `ispell-cmd-args', it isn't respected with hunspell.
+ ;; Hack ispell-local-dictionary-alist instead.
+ ispell-dictionary-alist
+ `((,ispell-local-dictionary
+ "[[:alpha:]]"
+ "[^[:alpha:]]"
+ "[']"
+ nil
+ ("-d" ,ispell-local-dictionary)
+ nil
+ utf-8))))
+
+ (_ (warn "Spell checker not found. Either install `aspell' or `hunspell'"))))
+
+
+;; `flyspell' (built-in)
+(progn
+ (setq flyspell-issue-welcome-flag nil)
+
+ (defun +flyspell|inhibit-duplicate-detection-maybe ()
+ "Don't mark duplicates when style/grammar linters are present.
+e.g. proselint and langtool."
+ (when (or (and (bound-and-true-p flycheck-mode)
+ (executable-find "proselint"))
+ (featurep 'langtool))
+ (setq-local flyspell-mark-duplications-flag nil)))
+ (add-hook 'flyspell-mode-hook #'+flyspell|inhibit-duplicate-detection-maybe)
+
+ (defun +flyspell|immediately ()
+ "Spellcheck the buffer when `flyspell-mode' is enabled."
+ (when (and flyspell-mode +flyspell-immediately)
+ (flyspell-buffer)))
+ (add-hook 'flyspell-mode-hook #'+flyspell|immediately)
+
+ ;; Ensure mode-local predicates declared with `set-flyspell-predicate!' are
+ ;; used in their respective major modes.
+ (add-hook 'flyspell-mode-hook #'+flyspell|init-predicate))
+
+
+(def-package! flyspell-correct
+ :commands (flyspell-correct-word-generic
+ flyspell-correct-previous-word-generic)
+ :config
+ (cond ((and (featurep! :completion helm)
+ (require 'flyspell-correct-helm nil t)))
+ ((and (featurep! :completion ivy)
+ (require 'flyspell-correct-ivy nil t)))
+ ((require 'flyspell-correct-popup nil t)
+ (setq flyspell-popup-correct-delay 0.8)
+ (define-key popup-menu-keymap [escape] #'keyboard-quit))))
diff --git a/modules/tools/flyspell/doctor.el b/modules/tools/flyspell/doctor.el
new file mode 100644
index 000000000..9d1cb3286
--- /dev/null
+++ b/modules/tools/flyspell/doctor.el
@@ -0,0 +1,4 @@
+
+(unless (or (executable-find "aspell")
+ (executable-find "hunspell"))
+ (warn! "Could not find aspell or hunspell. Flyspell will fall back to ispell, which may not work."))
diff --git a/modules/feature/spellcheck/packages.el b/modules/tools/flyspell/packages.el
similarity index 70%
rename from modules/feature/spellcheck/packages.el
rename to modules/tools/flyspell/packages.el
index bb01a00ba..efb81d804 100644
--- a/modules/feature/spellcheck/packages.el
+++ b/modules/tools/flyspell/packages.el
@@ -1,11 +1,9 @@
;; -*- no-byte-compile: t; -*-
-;;; feature/spellcheck/packages.el
+;;; tools/flyspell/packages.el
(package! flyspell-correct)
(cond ((featurep! :completion ivy)
(package! flyspell-correct-ivy))
((featurep! :completion helm)
(package! flyspell-correct-helm))
- (t
- (package! flyspell-correct-popup)))
-
+ ((package! flyspell-correct-popup)))
diff --git a/modules/tools/gist/autoload/gist.el b/modules/tools/gist/autoload/gist.el
deleted file mode 100644
index 1202b003a..000000000
--- a/modules/tools/gist/autoload/gist.el
+++ /dev/null
@@ -1,16 +0,0 @@
-;;; tools/gist/autoload/gist.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun +gist/open-current ()
- (interactive)
- (gist-fetch-current)
- (when-let* ((win (get-buffer-window "*github:gists*")))
- (doom/popup-close win)))
-
-;;;###autoload
-(defun +gist/kill-cache ()
- "Clears the gist cache. Necessary when a faulty cache causes gist.el to be
-entirely unuseable."
- (interactive)
- (delete-directory (expand-file-name "gh" pcache-directory) t)
- (message "gist.el cache cleared"))
diff --git a/modules/tools/gist/config.el b/modules/tools/gist/config.el
index cc7837de3..4e8f84b52 100644
--- a/modules/tools/gist/config.el
+++ b/modules/tools/gist/config.el
@@ -4,14 +4,13 @@
;; errors. If that happens, try `+gist/kill-cache'. You may have to restart
;; Emacs.
-(def-package! gist
- :commands (gist-list gist-region-or-buffer-private gist-region-or-buffer)
- :config
- (set! :popup "*github:gists*" :size 15 :select t :autokill t)
- (set! :evil-state 'gist-list-mode 'normal)
+(after! gist
+ (set-evil-initial-state! 'gist-list-mode 'normal)
+
+ (set-popup-rule! "^\\*gist-" :ignore t)
(defun +gist*list-render (orig-fn &rest args)
(funcall orig-fn (car args) t)
(unless (cadr args)
- (doom-popup-buffer (current-buffer))))
+ (pop-to-buffer (current-buffer))))
(advice-add #'gist-list-render :around #'+gist*list-render))
diff --git a/modules/tools/imenu/config.el b/modules/tools/imenu/config.el
deleted file mode 100644
index cbef99643..000000000
--- a/modules/tools/imenu/config.el
+++ /dev/null
@@ -1,25 +0,0 @@
-;;; tools/imenu/config.el -*- lexical-binding: t; -*-
-
-(def-package! imenu-anywhere
- :commands (ido-imenu-anywhere ivy-imenu-anywhere helm-imenu-anywhere)
- :config (setq imenu-anywhere-delimiter ": "))
-
-
-(def-package! imenu-list
- :commands imenu-list-minor-mode
- :config
- (setq imenu-list-focus-after-activation t)
- (set! :popup imenu-list-buffer-name :size 35 :align 'right)
-
- ;; use popups
- (defun doom*imenu-list-show ()
- (doom-popup-buffer (get-buffer imenu-list-buffer-name)))
- (advice-add #'imenu-list-show :override #'doom*imenu-list-show)
- (advice-add #'imenu-list-show-noselect :override #'doom*imenu-list-show)
-
- ;; auto kill imenu-list on deactivation
- (defun doom|kill-imenu-list ()
- (when (and (not imenu-list-minor-mode)
- (get-buffer imenu-list-buffer-name))
- (kill-buffer (get-buffer imenu-list-buffer-name))))
- (add-hook 'imenu-list-minor-mode-hook #'doom|kill-imenu-list))
diff --git a/modules/tools/impatient-mode/config.el b/modules/tools/impatient-mode/config.el
deleted file mode 100644
index da036ac8a..000000000
--- a/modules/tools/impatient-mode/config.el
+++ /dev/null
@@ -1,6 +0,0 @@
-;;; tools/impatient-mode/config.el -*- lexical-binding: t; -*-
-
-;; Show off code as you write it
-
-(def-package! impatient-mode
- :commands impatient-mode)
diff --git a/modules/tools/lsp/README.org b/modules/tools/lsp/README.org
new file mode 100644
index 000000000..b7a30aac3
--- /dev/null
+++ b/modules/tools/lsp/README.org
@@ -0,0 +1,67 @@
+#+TITLE: tools/lsp
+#+DATE: March 05, 2019
+#+SINCE: v2.1
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC_3:noexport:
+- [[#description][Description]]
+ - [[#module-flags][Module Flags]]
+ - [[#plugins][Plugins]]
+- [[#prerequisites][Prerequisites]]
+- [[#features][Features]]
+- [[#configuration][Configuration]]
+- [[#troubleshooting][Troubleshooting]]
+
+* Description
+This module integrate [[https://langserver.org/][language servers]] into Doom Emacs. They provide features
+you'd expect from IDEs, like code completion, realtime linting, language-aware
+imenu/xref integration, jump-to-definition/references support, and more.
+
+To get LSP working, you'll need two things:
+
+1. To install a language server appropriate for your targeted language (you'll
+ find a table mapping languages to available servers [[https://github.com/emacs-lsp/lsp-mode#supported-languages][in the lsp-mode project
+ README]]).
+2. To enable this module and the LSP capabilities of the appropriate =:lang=
+ module with the =+lsp= flag. If your language's module doesn't have LSP
+ support, you'll have to configure your own (described in the Configuration
+ section).
+
+As of this writing, this is the state of LSP support in Doom Emacs:
+
+| Module | Major modes | Default language server |
+|------------------+---------------------------------------------------------+---------------------------------------------------------------|
+| [[../../lang/cc/README.org][:lang cc]] | c-mode, c++-mode, objc-mode | ccls |
+| [[../../lang/go/README.org][:lang go]] | go-mode | go-langserver |
+| [[../../lang/haskell/README.org][:lang haskell]] | haskell-mode | haskell-ide-engine |
+| [[../../lang/javascript/README.org][:lang javascript]] | js2-mode, rjsx-mode, typescript-mode | typescript-language-server |
+| [[../../lang/java/README.org][:lang java]] | java-mode | lsp-java |
+| [[../../lang/ocaml/README.org][:lang ocaml]] | taureg-mode | ocaml-language-server |
+| [[../../lang/php/README.org][:lang php]] | php-mode | php-language-server |
+| [[../../lang/python/README.org][:lang python]] | python-mode | lsp-python-ms |
+| [[../../lang/ruby/README.org][:lang ruby]] | ruby-mode, enh-ruby-mode | solargraph |
+| [[../../lang/rust/README.org][:lang rust]] | rust-mode | rls |
+| [[../../lang/sh/README.org][:lang sh]] | sh-mode | bash-language-server |
+| [[../../lang/web/README.org][:lang web]] | web-mode, css-mode, scss-mode, sass-mode, less-css-mode | vscode-css-languageserver-bin, vscode-html-languageserver-bin |
+
+** Module Flags
+This module provides no flags.
+
+** Plugins
++ [[https://github.com/emacs-lsp/lsp-mode][lsp-mode]]
++ [[https://github.com/emacs-lsp/lsp-ui][lsp-ui]]
++ [[https://github.com/tigersoldier/company-lsp][company-lsp]]*
+
+* Prerequisites
+This module has no direct prerequisites, but major-modes require you to install
+language servers.
+
+You'll find a table that lists available language servers and how to install
+them [[https://github.com/emacs-lsp/lsp-mode#supported-languages][in the lsp-mode project README]]. The documentation of the module for your
+targeted language will contain brief instructions as well.
+
+* TODO Features
+
+* TODO Configuration
+
+* TODO Troubleshooting
diff --git a/modules/tools/lsp/autoload.el b/modules/tools/lsp/autoload.el
new file mode 100644
index 000000000..5dd061dba
--- /dev/null
+++ b/modules/tools/lsp/autoload.el
@@ -0,0 +1,4 @@
+;;; feature/lsp/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autodef
+(defalias 'lsp! #'lsp)
diff --git a/modules/tools/lsp/config.el b/modules/tools/lsp/config.el
new file mode 100644
index 000000000..87305a95f
--- /dev/null
+++ b/modules/tools/lsp/config.el
@@ -0,0 +1,36 @@
+;;; tools/lsp/config.el -*- lexical-binding: t; -*-
+
+(setq lsp-session-file (concat doom-etc-dir "lsp-session")
+ lsp-auto-guess-root t
+ lsp-keep-workspace-alive nil)
+
+(after! lsp-mode
+ (define-key lsp-mode-map
+ [remap +lookup/documentation] #'lsp-describe-thing-at-point)
+
+ ;; Don't prompt to restart LSP servers while quitting Emacs
+ (add-hook! 'kill-emacs-hook (setq lsp-restart 'ignore)))
+
+
+(def-package! lsp-ui
+ :hook (lsp-mode . lsp-ui-mode)
+ :config
+ (setq lsp-prefer-flymake nil
+ lsp-ui-doc-max-height 8
+ lsp-ui-doc-max-width 35
+ lsp-ui-sideline-ignore-duplicate t)
+
+ (define-key! lsp-ui-mode-map
+ [remap xref-find-definitions] #'lsp-ui-peek-find-definitions
+ [remap xref-find-references] #'lsp-ui-peek-find-references
+ ;; `set-lookup-handlers!' won't work for lsp-ui-peek commands, because they
+ ;; don't switch buffers
+ [remap +lookup/definition] #'lsp-ui-peek-find-definitions
+ [remap +lookup/references] #'lsp-ui-peek-find-references))
+
+
+(def-package! company-lsp
+ :when (featurep! :completion company)
+ :after lsp-mode
+ :config
+ (set-company-backend! 'lsp-mode 'company-lsp))
diff --git a/modules/tools/lsp/packages.el b/modules/tools/lsp/packages.el
new file mode 100644
index 000000000..efcc53bd1
--- /dev/null
+++ b/modules/tools/lsp/packages.el
@@ -0,0 +1,7 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/lsp/packages.el
+
+(package! lsp-mode)
+(package! lsp-ui)
+(when (featurep! :completion company)
+ (package! company-lsp))
diff --git a/modules/tools/macos/autoload.el b/modules/tools/macos/autoload.el
index 46323f7d3..05b415fa3 100644
--- a/modules/tools/macos/autoload.el
+++ b/modules/tools/macos/autoload.el
@@ -7,7 +7,7 @@
(let* ((path (expand-file-name
(replace-regexp-in-string
"'" "\\'"
- (or path (if (eq major-mode 'dired-mode)
+ (or path (if (derived-mode-p 'dired-mode)
(dired-get-file-for-visit)
(buffer-file-name)))
nil t)))
@@ -18,6 +18,7 @@
(message "Running: %s" command)
(shell-command command)))
+;;;###autoload
(defmacro +macos!open-with (id &optional app dir)
`(defun ,(intern (format "+macos/%s" id)) ()
(interactive)
@@ -30,7 +31,8 @@
(+macos!open-with reveal-in-finder "Finder" default-directory)
;;;###autoload (autoload '+macos/reveal-project-in-finder "tools/macos/autoload" nil t)
-(+macos!open-with reveal-project-in-finder "Finder" (doom-project-root))
+(+macos!open-with reveal-project-in-finder "Finder"
+ (or (doom-project-root) default-directory))
;;;###autoload (autoload '+macos/send-to-transmit "tools/macos/autoload" nil t)
(+macos!open-with send-to-transmit "Transmit")
@@ -42,4 +44,5 @@
(+macos!open-with send-to-launchbar "LaunchBar")
;;;###autoload (autoload '+macos/send-project-to-launchbar "tools/macos/autoload" nil t)
-(+macos!open-with send-project-to-launchbar "LaunchBar" (doom-project-root))
+(+macos!open-with send-project-to-launchbar "LaunchBar"
+ (or (doom-project-root) default-directory))
diff --git a/modules/tools/magit/autoload.el b/modules/tools/magit/autoload.el
new file mode 100644
index 000000000..4b423ebc6
--- /dev/null
+++ b/modules/tools/magit/autoload.el
@@ -0,0 +1,148 @@
+;;; tools/magit/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +magit-display-buffer (buffer)
+ "Like `magit-display-buffer-fullframe-status-v1' with two differences:
+
+1. Magit sub-buffers that aren't spawned from a status screen are opened as
+ popups.
+2. The status screen isn't buried when viewing diffs or logs from the status
+ screen."
+ (let ((buffer-mode (buffer-local-value 'major-mode buffer)))
+ (display-buffer
+ buffer (cond
+ ;; If opened from an eshell window or popup, use the same window.
+ ((or (derived-mode-p 'eshell-mode)
+ (eq (window-dedicated-p) 'side))
+ '(display-buffer-same-window))
+ ;; Open target buffers below the current one (we want previous
+ ;; magit windows to be visible; especially magit-status).
+ ((or (bound-and-true-p git-commit-mode)
+ (derived-mode-p 'magit-mode))
+ (let ((size (if (eq buffer-mode 'magit-process-mode)
+ 0.35
+ 0.7)))
+ `(display-buffer-below-selected
+ . ((window-height . ,(truncate (* (window-height) size)))))))
+ ;; log/stash/process buffers, unless opened from a magit-status
+ ;; window, should be opened in popups.
+ ((memq buffer-mode '(magit-process-mode
+ magit-log-mode
+ magit-stash-mode))
+ '(display-buffer-below-selected))
+ ;; Last resort: use current window
+ ('(display-buffer-same-window))))))
+
+;;;###autoload
+(defun +magit-display-popup-buffer (buffer &optional alist)
+ "TODO"
+ (cond ((eq (window-dedicated-p) 'side)
+ (if (fboundp '+popup-display-buffer-stacked-side-window)
+ (+popup-display-buffer-stacked-side-window buffer alist)
+ (display-buffer-in-side-window buffer alist)))
+ ((derived-mode-p 'magit-mode)
+ (display-buffer-below-selected buffer alist))
+ ((display-buffer-in-side-window buffer alist))))
+
+
+;;
+;; Commands
+
+(defun +magit--refresh-vc-in-buffer (buffer)
+ (with-current-buffer buffer
+ (when (and vc-mode (fboundp 'vc-refresh-state))
+ (vc-refresh-state))
+ (when (and (bound-and-true-p git-gutter-mode)
+ (fboundp '+version-control|update-git-gutter))
+ (+version-control|update-git-gutter))
+ (setq +magit--vc-is-stale-p nil)))
+
+;;;###autoload
+(defvar-local +magit--vc-is-stale-p nil)
+
+;;;###autoload
+(defun +magit|refresh-vc-state-maybe ()
+ "Update `vc' and `git-gutter' if out of date."
+ (when +magit--vc-is-stale-p
+ (+magit--refresh-vc-in-buffer (current-buffer))))
+
+;;;###autoload
+(add-hook 'doom-switch-buffer-hook #'+magit|refresh-vc-state-maybe)
+
+;;;###autoload
+(defun +magit/quit (&optional _kill-buffer)
+ "Clean up magit buffers after quitting `magit-status' and refresh version
+control in buffers."
+ (interactive)
+ (quit-window)
+ (unless (cdr
+ (delq nil
+ (mapcar (lambda (win)
+ (with-selected-window win
+ (eq major-mode 'magit-status-mode)))
+ (window-list))))
+ (mapc #'+magit--kill-buffer (magit-mode-get-buffers))
+ (dolist (buffer (doom-buffer-list))
+ (if (get-buffer-window buffer)
+ (+magit--refresh-vc-in-buffer buffer)
+ (with-current-buffer buffer
+ (setq +magit--vc-is-stale-p t))))))
+
+(defun +magit--kill-buffer (buf)
+ "TODO"
+ (when (and (bufferp buf) (buffer-live-p buf))
+ (let ((process (get-buffer-process buf)))
+ (if (not (processp process))
+ (kill-buffer buf)
+ (with-current-buffer buf
+ (if (process-live-p process)
+ (run-with-timer 5 nil #'+magit--kill-buffer buf)
+ (kill-process process)
+ (kill-buffer buf)))))))
+
+(defvar +magit-clone-history nil
+ "History for `+magit/clone' prompt.")
+;;;###autoload
+(defun +magit/clone (url-or-repo dir)
+ "Like `magit-clone', but supports additional formats on top of absolute URLs:
+
++ USER/REPO: assumes {`+magit-default-clone-url'}/USER/REPO
++ REPO: assumes {`+magit-default-clone-url'}/{USER}/REPO, where {USER} is
+ ascertained from your global gitconfig."
+ (interactive
+ (progn
+ (require 'ghub)
+ (let* ((user (ghub--username (ghub--host)))
+ (repo (read-from-minibuffer
+ "Clone repository (user/repo or url): "
+ (if user (concat user "/"))
+ nil nil '+magit-clone-history))
+ (name (car (last (split-string repo "/" t)))))
+ (list repo
+ (read-directory-name
+ "Destination: "
+ magit-clone-default-directory
+ name nil name)))))
+ (magit-clone
+ (cond ((string-match-p "^[^/]+$" url-or-repo)
+ (require 'ghub)
+ (format +magit-default-clone-url (ghub--username (ghub--host)) url-or-repo))
+ ((string-match-p "^\\([^/]+\\)/\\([^/]+\\)/?$" url-or-repo)
+ (apply #'format +magit-default-clone-url (split-string url-or-repo "/" t)))
+ (url-or-repo))
+ dir))
+
+
+;;
+;; Advice
+
+;;;###autoload
+(defun +magit*hub-settings--format-magithub.enabled ()
+ "Change the setting to display 'false' as its default."
+ (magit--format-popup-variable:choices "magithub.enabled" '("true" "false") "false"))
+
+;;;###autoload
+(defun +magit*hub-enabled-p ()
+ "Disables magithub by default."
+ (magithub-settings--value-or "magithub.enabled" nil
+ #'magit-get-boolean))
diff --git a/modules/tools/magit/config.el b/modules/tools/magit/config.el
new file mode 100644
index 000000000..7ad1b5350
--- /dev/null
+++ b/modules/tools/magit/config.el
@@ -0,0 +1,93 @@
+;;; tools/magit/config.el -*- lexical-binding: t; -*-
+
+(defvar +magit-default-clone-url "https://github.com/%s/%s"
+ "The default location for `+magit/clone' to clone relative URLs from.
+It is passed a user and repository name.")
+
+
+;;
+;; Packages
+
+(def-package! magit
+ :commands magit-file-delete
+ :defer-incrementally (dash f s with-editor git-commit package eieio lv transient)
+ :init
+ (setq magit-auto-revert-mode nil) ; we already use `global-auto-revert-mode'
+ :config
+ (setq transient-default-level 5
+ transient-levels-file (concat doom-etc-dir "transient/levels")
+ transient-values-file (concat doom-etc-dir "transient/values")
+ transient-history-file (concat doom-etc-dir "transient/history")
+ magit-revision-show-gravatars '("^Author: " . "^Commit: ")
+ magit-diff-refine-hunk t) ; show granular diffs in selected hunk
+
+ ;; Leave it to `+magit-display-buffer' and `+magit-display-popup-buffer' to
+ ;; manage popup windows.
+ (setq magit-display-buffer-function #'+magit-display-buffer
+ magit-popup-display-buffer-action '((+magit-display-popup-buffer)))
+ (set-popup-rule! "^\\(?:\\*magit\\|magit:\\)" :ignore t)
+
+ ;; so magit buffers can be switched to (except for process buffers)
+ (defun +magit-buffer-p (buf)
+ (with-current-buffer buf
+ (and (derived-mode-p 'magit-mode)
+ (not (eq major-mode 'magit-process-mode)))))
+ (add-to-list 'doom-real-buffer-functions #'+magit-buffer-p nil #'eq)
+
+ ;; properly kill leftover magit buffers on quit
+ (define-key magit-status-mode-map [remap magit-mode-bury-buffer] #'+magit/quit)
+
+ ;; Close transient with ESC
+ (define-key transient-map [escape] #'transient-quit-one))
+
+
+(def-package! forge
+ ;; We defer loading even further because forge's dependencies will try to
+ ;; compile emacsql, which is a slow and blocking operation.
+ :after-call magit-status
+ :init
+ (setq forge-database-file (concat doom-etc-dir "forge/forge-database.sqlite"))
+ :config
+ ;; All forge list modes are derived from `forge-topic-list-mode'
+ (map! :map forge-topic-list-mode-map :n "q" #'kill-this-buffer)
+ (set-popup-rule! "^\\*?[0-9]+:\\(?:new-\\|[0-9]+$\\)" :size 0.45 :modeline t :ttl 0 :quit nil)
+ (set-popup-rule! "^\\*\\(?:[^/]+/[^ ]+ #[0-9]+\\*$\\|Issues\\|Pull-Requests\\|forge\\)" :ignore t))
+
+
+(def-package! magit-todos
+ :after magit
+ :config
+ (setq magit-todos-require-colon nil)
+ (define-key magit-todos-section-map "j" nil)
+ (advice-add #'magit-todos-mode :around #'doom*shut-up)
+ (magit-todos-mode +1))
+
+
+(def-package! magit-gitflow
+ :hook (magit-mode . turn-on-magit-gitflow))
+
+
+(def-package! evil-magit
+ :when (featurep! :feature evil +everywhere)
+ :after magit
+ :init
+ (setq evil-magit-state 'normal
+ evil-magit-use-z-for-folds t)
+ :config
+ (unmap! magit-mode-map "M-1" "M-2" "M-3" "M-4") ; replaced by z1, z2, z3, etc
+ (evil-define-key* '(normal visual) magit-mode-map
+ "zz" #'evil-scroll-line-to-center
+ "%" #'magit-gitflow-popup)
+ (define-key! 'normal
+ (magit-status-mode-map
+ magit-stash-mode-map
+ magit-revision-mode-map
+ magit-diff-mode-map)
+ [tab] #'magit-section-toggle)
+ (after! git-rebase
+ (dolist (key '(("M-k" . "gk") ("M-j" . "gj")))
+ (when-let* ((desc (assoc (car key) evil-magit-rebase-commands-w-descriptions)))
+ (setcar desc (cdr key))))
+ (evil-define-key* evil-magit-state git-rebase-mode-map
+ "gj" #'git-rebase-move-line-down
+ "gk" #'git-rebase-move-line-up)))
diff --git a/modules/tools/magit/packages.el b/modules/tools/magit/packages.el
new file mode 100644
index 000000000..c02d5dcc7
--- /dev/null
+++ b/modules/tools/magit/packages.el
@@ -0,0 +1,9 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/magit/packages.el
+
+(when (package! magit)
+ (package! forge)
+ (package! magit-gitflow)
+ (package! magit-todos)
+ (when (featurep! :feature evil +everywhere)
+ (package! evil-magit)))
diff --git a/modules/tools/make/autoload.el b/modules/tools/make/autoload.el
index ce8de9d3a..121251ede 100644
--- a/modules/tools/make/autoload.el
+++ b/modules/tools/make/autoload.el
@@ -1,14 +1,25 @@
;;; tools/make/autoload.el -*- lexical-binding: t; -*-
+(require 'makefile-executor)
+
;;;###autoload
(defun +make/run ()
- "Run a make task in the current project."
+ "Run a make task in the current project. If multiple makefiles are available,
+you'll be prompted to select one."
(interactive)
- (require 'makefile-executor)
- (let* ((buffer-file (or buffer-file-name default-directory))
- (makefile-dir (locate-dominating-file buffer-file "Makefile")))
- (unless makefile-dir
- (user-error "No makefile found in this project."))
- (let ((default-directory makefile-dir))
- (makefile-executor-execute-target
- (expand-file-name "Makefile")))))
+ (if (doom-project-p)
+ (makefile-executor-execute-project-target)
+ (let ((makefile (cl-loop with buffer-file = (or buffer-file-name default-directory)
+ for file in (list "Makefile" "makefile")
+ if (locate-dominating-file buffer-file file)
+ return file)))
+ (unless makefile
+ (user-error "Cannot find a makefile in the current project"))
+ (let ((default-directory (file-name-directory makefile)))
+ (makefile-executor-execute-target makefile)))))
+
+;;;###autoload
+(defun +make/run-last ()
+ "TODO"
+ (interactive)
+ (makefile-executor-execute-last nil))
diff --git a/modules/tools/neotree/config.el b/modules/tools/neotree/config.el
deleted file mode 100644
index fb8a88086..000000000
--- a/modules/tools/neotree/config.el
+++ /dev/null
@@ -1,37 +0,0 @@
-;;; tools/neotree/config.el -*- lexical-binding: t; -*-
-
-(def-package! neotree
- :commands (neotree-show
- neotree-hide
- neotree-toggle
- neotree-dir
- neotree-find
- neo-global--with-buffer
- neo-global--window-exists-p)
- :config
- (setq neo-create-file-auto-open nil
- neo-auto-indent-point nil
- neo-autorefresh nil
- neo-mode-line-type 'none
- neo-window-width 25
- neo-show-updir-line nil
- neo-theme 'nerd ; fallback
- neo-banner-message nil
- neo-confirm-create-file #'off-p
- neo-confirm-create-directory #'off-p
- neo-show-hidden-files nil
- neo-keymap-style 'concise
- neo-hidden-regexp-list
- '(;; vcs folders
- "^\\.\\(git\\|hg\\|svn\\)$"
- ;; compiled files
- "\\.\\(pyc\\|o\\|elc\\|lock\\|css.map\\)$"
- ;; generated files, caches or local pkgs
- "^\\(node_modules\\|vendor\\|.\\(project\\|cask\\|yardoc\\|sass-cache\\)\\)$"
- ;; org-mode folders
- "^\\.\\(sync\\|export\\|attach\\)$"
- "~$"
- "^#.*#$"))
-
- (when (bound-and-true-p winner-mode)
- (push neo-buffer-name winner-boring-buffers)))
diff --git a/modules/tools/password-store/autoload.el b/modules/tools/password-store/autoload.el
index f22d18393..073a4526c 100644
--- a/modules/tools/password-store/autoload.el
+++ b/modules/tools/password-store/autoload.el
@@ -1,79 +1,140 @@
;;; tools/password-store/autoload.el -*- lexical-binding: t; -*-
-;;;###autoload
-(defun +pass/open ()
- (interactive)
- (cond ((featurep! :completion ivy)
- (+pass/ivy))
- ((featurep! :completion helm)
- (helm-pass))
- (t
- (pass))))
-
-;;;###autoload
-(defalias '+pass--get-entry
- (if (featurep 'auth-store-pass)
- #'auth-source-pass-parse-entry
- #'auth-pass-parse-entry))
-
-;;;###autoload
-(defun +pass-get-field (entry fields)
- (if-let* ((data (if (listp entry) entry (+pass--get-entry entry))))
- (cl-loop for key in (doom-enlist fields)
- when (assoc key data)
- return (cdr it))
- (error "Couldn't find entry: %s" entry)))
-
-;;;###autoload
-(defun +pass-get-user (entry)
- (+pass-get-field entry +pass-user-fields))
-
-;;;###autoload
-(defun +pass-get-secret (entry)
- (+pass-get-field entry 'secret))
-
-(defun +pass-ivy-action--open-url (entry)
+(defun +pass--open-url (entry)
(if-let* ((url (+pass-get-field entry +pass-url-fields)))
(and (or (string-match-p "https?://" url)
(error "Field for %s doesn't look like an url" item))
(browse-url url))
(error "url not found.")))
-(defun +pass-ivy-action--get-field (item)
- (let* ((data (+pass--get-entry item))
- (field (if data (completing-read "Field: " (mapcar #'car data) nil t))))
- (if data
- (progn
- (password-store-clear)
- (message "Copied %s's %s field to clipboard. Will clear in %s seconds"
- item field (password-store-timeout))
- (kill-new (+pass-get-field data field))
- (setq password-store-timeout-timer
- (run-at-time (password-store-timeout) nil 'password-store-clear)))
- (error "Couldn't find entry: %s" item))))
+(defun +pass--copy (entry field text)
+ (password-store-clear)
+ (message "Copied %s's %s field to clipboard. Will clear in %s seconds"
+ entry field (password-store-timeout))
+ (kill-new text)
+ (setq password-store-timeout-timer
+ (run-at-time (password-store-timeout) nil 'password-store-clear)))
-(defun +pass-ivy-action--copy-username (entry)
+(defun +pass--copy-username (entry)
(if-let* ((user (+pass-get-field entry +pass-user-fields)))
(progn (password-store-clear)
(message "Copied username to the kill ring.")
(kill-new user))
(error "Username not found.")))
-(after! ivy
- (ivy-add-actions
- '+pass/ivy
- '(("o" password-store-copy "copy password")
- ("e" password-store-edit "edit entry")
- ("u" +pass-ivy-action--copy-username "copy username")
- ("b" +pass-ivy-action--open-url "open url in browser")
- ("f" +pass-ivy-action--get-field "get field"))))
+(defun +pass--completing-read-field (entry)
+ (let* ((data (+pass-get-entry entry))
+ (field (if data (completing-read "Field: " (mapcar #'car data) nil t))))
+ (+pass-get-field data field)))
+
+
+;;
+;; API
+
+;;;###autoload (autoload 'auth-source-pass-parse-entry "auth-source-pass")
+;;;###autoload
+(defalias '+pass-get-entry #'auth-source-pass-parse-entry)
;;;###autoload
-(defun +pass/ivy (&optional browse-url)
+(defun +pass-get-field (entry fields &optional noerror)
+ "Fetches the value of a field. FIELDS can be a list of string field names or a
+single one. If a list, the first field found will be returned. Will error out
+otherwise, unless NOERROR is non-nill."
+ (if-let* ((data (if (listp entry) entry (+pass-get-entry entry))))
+ (cl-loop for key in (doom-enlist fields)
+ when (assoc key data)
+ return (cdr it))
+ (unless noerror
+ (error "Couldn't find entry: %s" entry))))
+
+;;;###autoload
+(defun +pass-get-user (entry)
+ "Fetches the user field from ENTRY. Each of `+pass-user-fields' are tried in
+search of your username. May prompt for your gpg passphrase."
+ (+pass-get-field entry +pass-user-fields))
+
+;;;###autoload
+(defun +pass-get-secret (entry)
+ "Fetches your secret from ENTRY. May prompt for your gpg passphrase."
+ (+pass-get-field entry 'secret))
+
+
+;;
+;; Commands
+
+;;;###autoload (autoload 'password-store-dir "password-store")
+;;;###autoload (autoload 'password-store-list "password-store")
+;;;###autoload (autoload 'password-store--completing-read "password-store")
+
+;;;###autoload
+(defun +pass/edit-entry (entry)
+ "Interactively search for and open a pass entry for editing."
+ (interactive
+ (list (password-store--completing-read)))
+ (find-file (concat (expand-file-name entry (password-store-dir)) ".gpg")))
+
+;;;###autoload
+(defun +pass/copy-field (entry)
+ "Interactively search for an entry and copy a particular field to your
+clipboard."
+ (interactive
+ (list (password-store--completing-read)))
+ (let* ((data (+pass-get-entry entry))
+ (field (if data (completing-read "Field: " (mapcar #'car data) nil t))))
+ (+pass--copy entry field (+pass-get-field data field))))
+
+;;;###autoload
+(defun +pass/copy-secret (entry)
+ "Interactively search for an entry and copy its secret/password to your
+clipboard."
+ (interactive
+ (list (password-store--completing-read)))
+ (+pass--copy entry 'secret (+pass-get-secret entry)))
+
+;;;###autoload
+(defun +pass/copy-user (entry)
+ "Interactively search for an entry and copy the login to your clipboard. The
+fields in `+pass-user-fields' is used to find the login field."
+ (interactive
+ (list (password-store--completing-read)))
+ (+pass--copy-username entry))
+
+;;;###autoload
+(defun +pass/browse-url (entry)
+ "Interactively search for an entry and open its url in your browser. The
+fields in `+pass-url-fields' is used to find the url field."
+ (interactive
+ (list (password-store--completing-read)))
+ (+pass--open-url entry))
+
+
+;;
+;; Ivy interface
+
+;;;###autoload
+(defun +pass/ivy (arg)
+ "TODO"
(interactive "P")
(ivy-read "Pass: " (password-store-list)
- :action (if browse-url
+ :action (if arg
#'password-store-url
#'password-store-copy)
:caller '+pass/ivy))
+(after! ivy
+ (ivy-add-actions
+ '+pass/ivy
+ '(("o" password-store-copy "copy password")
+ ("e" +pass/edit-entry "edit entry")
+ ("u" +pass/copy-user "copy username")
+ ("b" +pass/copy-url "open url in browser")
+ ("f" +pass/copy-field "get field"))))
+
+
+;;
+;; TODO Helm interface
+
+;; (defun +pass/helm ()
+;; (interactive)
+;; )
+
diff --git a/modules/tools/password-store/config.el b/modules/tools/password-store/config.el
index 28a69a17a..b161badae 100644
--- a/modules/tools/password-store/config.el
+++ b/modules/tools/password-store/config.el
@@ -8,35 +8,33 @@
;;
-;; Plugins
-;;
+;; Packages
-(def-package! password-store
- :defer t
- :config
- (setq password-store-password-length 12))
+;; `password-store'
+(setq password-store-password-length 12)
+
+;; Fix hard-coded password-store location; respect PASSWORD_STORE_DIR envvar
+(defun +password-store*read-entry (entry)
+ "Return a string with the file content of ENTRY."
+ (with-temp-buffer
+ (insert-file-contents
+ (expand-file-name (format "%s.gpg" entry) (password-store-dir)))
+ (buffer-substring-no-properties (point-min) (point-max))))
+(advice-add #'auth-source-pass--read-entry :override #'+password-store*read-entry)
-(def-package! pass
- :commands pass
- :config
- (set! :evil-state 'pass-mode 'emacs)
- (set! :popup "*Password-Store*" :align 'left :size 32 :select t :autokill t :noesc t)
- (map! :map pass-mode-map
- "j" #'pass-next-entry
- "k" #'pass-prev-entry
- "d" #'pass-kill
- "C-j" #'pass-next-directory
- "C-k" #'pass-next-directory))
-
-
-(def-package! helm-pass
- :when (featurep! :completion helm)
- :commands helm-pass)
+;; `pass'
+(after! pass
+ (set-evil-initial-state! 'pass-mode 'emacs)
+ (set-popup-rule! "^\\*Password-Store" :side 'left :size 0.25 :quit nil)
+ (define-key! pass-mode-map
+ "j" #'pass-next-entry
+ "k" #'pass-prev-entry
+ "d" #'pass-kill
+ "\C-j" #'pass-next-directory
+ "\C-k" #'pass-prev-directory))
;; Is built into Emacs 26+
-(if (require 'auth-store-pass nil t)
- (auth-source-pass-enable)
- (def-package! auth-password-store
- :config (auth-pass-enable)))
+(when (and EMACS26+ (featurep! +auth))
+ (auth-source-pass-enable))
diff --git a/modules/tools/password-store/packages.el b/modules/tools/password-store/packages.el
index fe97395c3..10744af9d 100644
--- a/modules/tools/password-store/packages.el
+++ b/modules/tools/password-store/packages.el
@@ -3,7 +3,13 @@
(package! pass)
(package! password-store)
-(package! auth-password-store)
+(package! password-store-otp)
+;; `auto-source-pass' is built into Emacs 26+
+(unless EMACS26+
+ (package! auth-source-pass))
+
+(when (featurep! :completion ivy)
+ (package! ivy-pass))
(when (featurep! :completion helm)
(package! helm-pass))
diff --git a/modules/tools/password-store/test/autoload-pass.el b/modules/tools/password-store/test/autoload-pass.el
deleted file mode 100644
index d9d190097..000000000
--- a/modules/tools/password-store/test/autoload-pass.el
+++ /dev/null
@@ -1,42 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; tools/password-store/test/autoload-pass.el
-
-(load! ../autoload)
-
-(defmacro with-passwords!! (buffer-args &rest body)
- (declare (indent defun))
- `(cl-letf
- (((symbol-function '+pass--get-entry)
- (lambda (entry)
- (when (equal entry "fake/source")
- '((secret . "defuse-account-gad")
- ("login" . "HL2532-GANDI")
- ("alt-login" . "hlissner")
- ("email" . "henrik@lissner.net")
- ("url" . "https://www.gandi.net/login"))))))
- ,@body))
-
-;;
-(def-test! get-field
- (with-passwords!!
- (should (equal (+pass-get-field "fake/source" "login")
- "HL2532-GANDI"))
- (should (equal (+pass-get-field "fake/source" "email")
- "henrik@lissner.net"))
- (should (equal (+pass-get-field "fake/source" '("alt-login" "email"))
- "hlissner"))
- (should (equal (+pass-get-field "fake/source" '("username" "email"))
- "henrik@lissner.net"))))
-
-(def-test! missing-fields-return-nil
- (with-passwords!!
- (should-not (+pass-get-field "fake/source" '("x" "y" "z")))))
-
-(def-test! missing-entries-throw-error
- (with-passwords!!
- (should-error (+pass-get-field "nonexistent/source" "login"))))
-
-(def-test! get-login
- (with-passwords!!
- (should (equal (+pass-get-user "fake/source") "HL2532-GANDI"))
- (should (equal (+pass-get-secret "fake/source") "defuse-account-gad"))))
diff --git a/modules/tools/password-store/test/test-pass.el b/modules/tools/password-store/test/test-pass.el
new file mode 100644
index 000000000..b1138fa75
--- /dev/null
+++ b/modules/tools/password-store/test/test-pass.el
@@ -0,0 +1,41 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/password-store/test/test-pass.el
+
+(describe "tools/password-store"
+ (before-all
+ (load! "../autoload"))
+
+ (before-each
+ (spy-on 'auth-source-pass-parse-entry :and-call-fake
+ (lambda (entry)
+ (when (equal entry "fake/source")
+ '((secret . "defuse-account-gad")
+ ("login" . "HL2532")
+ ("alt-login" . "hlissner")
+ ("email" . "henrik@lissner.net")
+ ("url" . "https://some-place.net/login"))))))
+
+ (describe "get field"
+ (it "returns specific fields"
+ (expect (+pass-get-field "fake/source" "email")
+ :to-equal "henrik@lissner.net"))
+ (it "returns first existing of a list of fields"
+ (expect (+pass-get-field "fake/source" '("alt-login" "email"))
+ :to-equal "hlissner")
+ (expect (+pass-get-field "fake/source" '("username" "email"))
+ :to-equal "henrik@lissner.net"))
+ (it "returns nil for missing fields"
+ (expect (+pass-get-field "fake/source" '("x" "y" "z"))
+ :to-be nil))
+ (it "throws error on missing entries"
+ (expect (+pass-get-field "nonexistent/source" "login")
+ :to-throw)))
+
+ (describe "get user/secret"
+ (it "returns the user"
+ (let ((+pass-user-fields '("login" "user" "username" "email")))
+ (expect (+pass-get-user "fake/source")
+ :to-equal "HL2532")))
+ (it "returns the secret"
+ (expect (+pass-get-secret "fake/source")
+ :to-equal "defuse-account-gad"))))
diff --git a/modules/tools/pdf/config.el b/modules/tools/pdf/config.el
new file mode 100644
index 000000000..2f22d3983
--- /dev/null
+++ b/modules/tools/pdf/config.el
@@ -0,0 +1,32 @@
+;;; tools/pdf/config.el -*- lexical-binding: t; -*-
+
+(def-package! pdf-tools
+ :mode ("\\.pdf\\'" . pdf-view-mode)
+ :config
+ (unless noninteractive
+ (pdf-tools-install))
+
+ (map! :map pdf-view-mode-map :gn "q" #'kill-this-buffer)
+
+ (defun +pdf|cleanup-windows ()
+ "Kill left-over annotation buffers when the document is killed."
+ (when (buffer-live-p pdf-annot-list-document-buffer)
+ (pdf-info-close pdf-annot-list-document-buffer))
+ (when (buffer-live-p pdf-annot-list-buffer)
+ (kill-buffer pdf-annot-list-buffer))
+ (let ((contents-buffer (get-buffer "*Contents*")))
+ (when (and contents-buffer (buffer-live-p contents-buffer))
+ (kill-buffer contents-buffer))))
+ (add-hook! 'pdf-view-mode-hook
+ (add-hook 'kill-buffer-hook #'+pdf|cleanup-windows nil t))
+
+ (setq-default pdf-view-display-size 'fit-page)
+ ;; Turn off cua so copy works
+ (add-hook! 'pdf-view-mode-hook (cua-mode 0))
+ ;; Handle PDF-tools related popups better
+ (set-popup-rule! "^\\*Outline*" :side 'right :size 40 :select nil)
+ ;; The next rules are not needed, they are defined in modules/ui/popups/+hacks.el
+ ;; (set-popup-rule! "\\*Contents\\*" :side 'right :size 40)
+ ;; (set-popup-rule! "* annots\\*$" :side 'left :size 40 :select nil)
+ ;; Fix #1107: flickering pdfs when evil-mode is enabled
+ (setq-hook! 'pdf-view-mode-hook evil-normal-state-cursor (list nil)))
diff --git a/modules/tools/pdf/packages.el b/modules/tools/pdf/packages.el
new file mode 100644
index 000000000..59dae0cdd
--- /dev/null
+++ b/modules/tools/pdf/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/pdf/packages.el
+
+(package! pdf-tools)
diff --git a/modules/feature/services/autoload.el b/modules/tools/prodigy/autoload.el
similarity index 54%
rename from modules/feature/services/autoload.el
rename to modules/tools/prodigy/autoload.el
index dd2e7a8f3..0c013df59 100644
--- a/modules/feature/services/autoload.el
+++ b/modules/tools/prodigy/autoload.el
@@ -1,18 +1,18 @@
-;;; feature/services/autoload.el -*- lexical-binding: t; -*-
+;;; tools/prodigy/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
-(defun +services/create ()
+(defun +prodigy/create ()
"Interactively create a new prodigy service."
(interactive)
;; TODO
)
;;;###autoload
-(defun +services/prodigy-delete (arg)
+(defun +prodigy/delete (arg)
"Delete service at point. Asks for confirmation."
(interactive "P")
(prodigy-with-refresh
- (-when-let (service (prodigy-service-at-pos))
+ (when-let* ((service (prodigy-service-at-pos)))
(let ((name (plist-get service :name)))
(cond ((or arg
(y-or-n-p (format "Delete '%s' service?" name)))
@@ -24,7 +24,7 @@
(message "Aborted")))))))
;;;###autoload
-(defun +services/cleanup ()
+(defun +prodigy/cleanup ()
"Delete all services associated with projects that don't exist."
(interactive)
(cl-loop for service in prodigy-services
@@ -32,3 +32,17 @@
(file-directory-p (plist-get service :project)))
collect service into services
finally do (setq prodigy-service services)))
+
+;;;###autoload
+(defun +prodigy*services (orig-fn &rest args)
+ "Adds a new :project property to prodigy services, which hides the service
+unless invoked from the relevant project."
+ (let ((project-root (downcase (or (doom-project-root) default-directory)))
+ (services (apply orig-fn args)))
+ (if current-prefix-arg
+ services
+ (cl-remove-if-not (lambda (service)
+ (let ((project (plist-get service :project)))
+ (or (not project)
+ (file-in-directory-p project-root project))))
+ services))))
diff --git a/modules/tools/prodigy/config.el b/modules/tools/prodigy/config.el
new file mode 100644
index 000000000..76f8b1078
--- /dev/null
+++ b/modules/tools/prodigy/config.el
@@ -0,0 +1,13 @@
+;;; tools/prodigy/config.el -*- lexical-binding: t; -*-
+
+(after! prodigy
+ (set-evil-initial-state! 'prodigy-mode 'emacs)
+
+ ;; Make services, etc persistent between Emacs sessions
+ (doom-cache-persist
+ :prodigy '(prodigy-services prodigy-tags prodigy-filters))
+
+ (advice-add #'prodigy-services :around #'+prodigy*services)
+
+ (define-key prodigy-mode-map "d" #'+prodigy/delete))
+
diff --git a/modules/feature/services/packages.el b/modules/tools/prodigy/packages.el
similarity index 60%
rename from modules/feature/services/packages.el
rename to modules/tools/prodigy/packages.el
index dcca97f5d..bf9396134 100644
--- a/modules/feature/services/packages.el
+++ b/modules/tools/prodigy/packages.el
@@ -1,4 +1,4 @@
;; -*- no-byte-compile: t; -*-
-;;; feature/services/packages.el
+;;; tools/prodigy/packages.el
(package! prodigy)
diff --git a/modules/tools/rgb/autoload.el b/modules/tools/rgb/autoload.el
new file mode 100644
index 000000000..5fdedbe29
--- /dev/null
+++ b/modules/tools/rgb/autoload.el
@@ -0,0 +1,13 @@
+;;; tools/rgb/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload (autoload '+rgb-kurecolor-hydra/body "tools/rgb/autoload" nil t)
+(defhydra +rgb-kurecolor-hydra (:color pink :hint nil)
+ "
+Inc/Dec _w_/_W_ brightness _d_/_D_ saturation _e_/_E_ hue "
+ ("w" kurecolor-decrease-brightness-by-step)
+ ("W" kurecolor-increase-brightness-by-step)
+ ("d" kurecolor-decrease-saturation-by-step)
+ ("D" kurecolor-increase-saturation-by-step)
+ ("e" kurecolor-decrease-hue-by-step)
+ ("E" kurecolor-increase-hue-by-step)
+ ("q" nil "cancel" :color blue))
diff --git a/modules/tools/rgb/config.el b/modules/tools/rgb/config.el
deleted file mode 100644
index b69768ce7..000000000
--- a/modules/tools/rgb/config.el
+++ /dev/null
@@ -1,22 +0,0 @@
-;;; tools/rgb/config.el -*- lexical-binding: t; -*-
-
-;;
-;; Plugins
-;;
-
-(def-package! rainbow-mode)
-
-
-(def-package! kurecolor
- :after rainbow-mode
- :config
- (def-hydra! +rgb@kurecolor (:color pink :hint nil)
- "
-Inc/Dec _w_/_W_ brightness _d_/_D_ saturation _e_/_E_ hue "
- ("w" kurecolor-decrease-brightness-by-step)
- ("W" kurecolor-increase-brightness-by-step)
- ("d" kurecolor-decrease-saturation-by-step)
- ("D" kurecolor-increase-saturation-by-step)
- ("e" kurecolor-decrease-hue-by-step)
- ("E" kurecolor-increase-hue-by-step)
- ("q" nil "cancel" :color blue)))
diff --git a/modules/tools/rotate-text/config.el b/modules/tools/rotate-text/config.el
deleted file mode 100644
index 488ce72a7..000000000
--- a/modules/tools/rotate-text/config.el
+++ /dev/null
@@ -1,21 +0,0 @@
-;;; tools/rotate-text/config.el -*- lexical-binding: t; -*-
-
-(def-package! rotate-text
- :commands (rotate-text rotate-text-backward)
- :config
- (push '("true" "false") rotate-text-words))
-
-
-(def-setting! :rotate (modes &rest plist)
- "Declare :symbols, :words or :patterns that `rotate-text' will cycle through."
- (declare (indent 1))
- (let* ((modes (doom-enlist (doom-unquote modes)))
- (fn-name (intern (format "doom--rotate-%s" (mapconcat #'symbol-name modes "-")))))
- `(progn
- (defun ,fn-name ()
- (let ((plist (list ,@plist)))
- (setq rotate-text-local-symbols (plist-get plist :symbols)
- rotate-text-local-words (plist-get plist :words)
- rotate-text-local-patterns (plist-get plist :patterns))))
- (add-hook! ,modes #',fn-name))))
-
diff --git a/modules/tools/term/autoload.el b/modules/tools/term/autoload.el
deleted file mode 100644
index efab41c95..000000000
--- a/modules/tools/term/autoload.el
+++ /dev/null
@@ -1,27 +0,0 @@
-;;; tools/term/autoload.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun +term/open (&optional project-root)
- "Open a terminal buffer in the current window. If PROJECT-ROOT (C-u) is
-non-nil, cd into the current project's root."
- (interactive "P")
- (let ((default-directory (if project-root (doom-project-root) default-directory)))
- (call-interactively #'multi-term)))
-
-;;;###autoload
-(defun +term/open-popup (&optional project-root)
- "Open a terminal popup window. If PROJECT-ROOT (C-u) is non-nil, cd into the
-current project's root."
- (interactive "P")
- (require 'multi-term)
- (let ((default-directory (if project-root (doom-project-root) default-directory))
- (buffer (multi-term-get-buffer current-prefix-arg)))
- (select-window (doom-popup-buffer buffer))
- (setq multi-term-buffer-list (nconc multi-term-buffer-list (list buffer)))
- (multi-term-internal)))
-
-;;;###autoload
-(defun +term/open-popup-in-project ()
- "Open a terminal popup window in the root of the current project."
- (interactive)
- (+term/open-popup t))
diff --git a/modules/tools/term/config.el b/modules/tools/term/config.el
deleted file mode 100644
index c3588e746..000000000
--- a/modules/tools/term/config.el
+++ /dev/null
@@ -1,7 +0,0 @@
-;;; tools/term/config.el -*- lexical-binding: t; -*-
-
-(def-package! multi-term
- :commands (multi-term multi-term-next multi-term-prev)
- :config
- (setq multi-term-program (getenv "SHELL")
- multi-term-switch-after-close 'PREVIOUS))
diff --git a/modules/tools/terraform/config.el b/modules/tools/terraform/config.el
new file mode 100644
index 000000000..89b0315fc
--- /dev/null
+++ b/modules/tools/terraform/config.el
@@ -0,0 +1,15 @@
+;;; tools/terraform/config.el -*- lexical-binding: t; -*-
+
+(map! :after terraform-mode
+ :map terraform-mode-map
+ :localleader
+ :desc "terraform apply" "a" (λ! (compile "terraform apply"))
+ :desc "terraform init" "i" (λ! (compile "terraform init"))
+ :desc "terraform plan" "p" (λ! (compile "terraform plan")))
+
+
+(def-package! company-terraform
+ :when (featurep! :completion company)
+ :after terraform-mode
+ :config
+ (set-company-backend! 'terraform-mode 'company-terraform))
diff --git a/modules/tools/terraform/packages.el b/modules/tools/terraform/packages.el
new file mode 100644
index 000000000..9c9217ad1
--- /dev/null
+++ b/modules/tools/terraform/packages.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/terraform/packages.el
+
+(package! terraform-mode)
+(when (featurep! :completion company)
+ (package! company-terraform))
diff --git a/modules/tools/tmux/autoload/tmux.el b/modules/tools/tmux/autoload/tmux.el
index 2cab93d2e..63f51156e 100644
--- a/modules/tools/tmux/autoload/tmux.el
+++ b/modules/tools/tmux/autoload/tmux.el
@@ -13,7 +13,6 @@
;;
;; Commands
-;;
;;;###autoload
(defun +tmux (command &rest args)
@@ -63,19 +62,14 @@ but do not execute them."
(interactive "P")
(unless +tmux-last-command
(user-error "No last command to run"))
- (apply #'+tmux (car +tmux-last-command) (cdr +tmux-last-command)))
+ (apply #'+tmux +tmux-last-command))
;;;###autoload
-(defun +tmux/cd (&optional directory)
+(defun +tmux/cd (&optional arg directory)
"Change the pwd of the currently active tmux pane to DIRECTORY (defaults to
`default-directory', or to `doom-project-root' with the universal argument)."
- (interactive
- (list
- (when current-prefix-arg
- (read-directory-name
- "cd: " nil
- (if current-prefix-arg (doom-project-root) default-directory) t))))
- (+tmux "cd %s" (or directory default-directory)))
+ (interactive "PD")
+ (+tmux/run (format "cd %s" (or directory default-directory)) arg))
;;;###autoload
(defun +tmux/cd-to-here ()
@@ -92,7 +86,6 @@ but do not execute them."
;;
;; Data functions
-;;
;;;###autoload
(defun +tmux-list-sessions ()
@@ -113,8 +106,8 @@ but do not execute them."
(if session
(concat "-t " (car session))
"-a")))))
- (cl-loop for line in (string-split lines "\n" t)
- collect (let ((window (string-split line ";")))
+ (cl-loop for line in (split-string lines "\n" t)
+ collect (let ((window (split-string line ";")))
(list (nth 0 window)
:session-id (nth 1 window)
:name (nth 3 window)
@@ -131,7 +124,7 @@ but do not execute them."
"-t "
(car sess-or-win))
"-a")))))
- (cl-loop for line in (string-split lines "\n" t)
+ (cl-loop for line in (split-string lines "\n" t)
collect (let ((pane (split-string line ";")))
(list (nth 0 pane)
:window-id (nth 1 pane)
diff --git a/modules/tools/upload/autoload.el b/modules/tools/upload/autoload.el
deleted file mode 100644
index fb18193ad..000000000
--- a/modules/tools/upload/autoload.el
+++ /dev/null
@@ -1,34 +0,0 @@
-;;; tools/upload/autoload.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun +upload/local (&optional force-p)
- "TODO"
- (interactive)
- (if force-p
- (ssh-deploy-upload-handler-forced)
- (ssh-deploy-upload-handler)))
-
-;;;###autoload
-(defun +upload/remote-download ()
- "TODO"
- (interactive)
- (ssh-deploy-download-handler))
-
-;;;###autoload
-(defun +upload/diff ()
- "TODO"
- (interactive)
- (ssh-deploy-diff-handler))
-
-;;;###autoload
-(defun +upload/browse ()
- "TODO"
- (interactive)
- (ssh-deploy-browse-remove-handler))
-
-;;;###autoload
-(defun +upload/check-remote ()
- "TODO"
- (interactive)
- (ssh-deploy-remote-changes-handler))
-
diff --git a/modules/tools/upload/config.el b/modules/tools/upload/config.el
index a82cdf325..177ca4bf5 100644
--- a/modules/tools/upload/config.el
+++ b/modules/tools/upload/config.el
@@ -1,25 +1,37 @@
;;; tools/upload/config.el -*- lexical-binding: t; -*-
-;; Uses `ssh-deploy' to map a local folder to a remote one. Use
-;; `ssh-deploy-root-remote' and `ssh-deploy-root-local' to set up this mapping.
+;; Uses `ssh-deploy' to map a local folder to a remote one. Set
+;; `ssh-deploy-root-remote' and `ssh-deploy-root-local' in a .dir-locals.el file
+;; to establish this mapping.
;;
;; Example:
-;; (setq ssh-deploy-root-local "/home/hlissner/work/site/"
-;; ssh-deploy-root-remote "/ssh:hlissner@myserver.com:/var/www/site/"
-;; ssh-deploy-on-explicity-save t)
+;; ((nil . ((ssh-deploy-root-local . "/local/path/to/project")
+;; (ssh-deploy-root-remote . "/ssh:user@server:/remote/project/")
+;; (ssh-deploy-on-explicity-save . t))))
;;
;; Note: `ssh-deploy-root-local' is optional, and will resort to
;; `doom-project-root' if unspecified.
-;;
-;; Can be used via .dir-locals.el file in your project.
(def-package! ssh-deploy
:commands (ssh-deploy-upload-handler
ssh-deploy-upload-handler-forced
ssh-deploy-diff-handler
- ssh-deploy-browse-remove-handler
+ ssh-deploy-browse-remote-handler
ssh-deploy-remote-changes-handler)
:init
+ (setq ssh-deploy-revision-folder (concat doom-cache-dir "ssh-revisions/")
+ ssh-deploy-on-explicit-save t
+ ssh-deploy-automatically-detect-remote-changes nil)
+
+ ;; Make these safe as file-local variables
+ (dolist (sym '((ssh-deploy-root-local . stringp)
+ (ssh-deploy-root-remote . stringp)
+ (ssh-deploy-script . functionp)
+ (ssh-deploy-on-explicitly-save . booleanp)
+ (ssh-deploy-async . booleanp)
+ (ssh-deploy-exclude-list . listp)))
+ (put (car sym) 'safe-local-variable (cdr sym)))
+
;; Maybe auto-upload on save
(defun +upload|init-after-save ()
(when (and (bound-and-true-p ssh-deploy-root-remote)
@@ -36,9 +48,4 @@
(setq ssh-deploy-root-local (doom-project-root)))
(when ssh-deploy-automatically-detect-remote-changes
(ssh-deploy-remote-changes-handler))))
- (add-hook 'find-file-hook #'+upload|init-find-file)
- :config
- (setq ssh-deploy-revision-folder (concat doom-cache-dir "ssh-revisions/")
- ssh-deploy-on-explicit-save t
- ssh-deploy-automatically-detect-remote-changes t))
-
+ (add-hook 'find-file-hook #'+upload|init-find-file))
diff --git a/modules/tools/vterm/autoload.el b/modules/tools/vterm/autoload.el
new file mode 100644
index 000000000..d3b36d73a
--- /dev/null
+++ b/modules/tools/vterm/autoload.el
@@ -0,0 +1,33 @@
+;;; tools/vterm/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defun +vterm/open (arg)
+ "Open a terminal buffer in the current window. If ARG (universal argument) is
+non-nil, cd into the current project's root."
+ (interactive "P")
+ (unless (fboundp 'module-load)
+ (user-error "Your build of Emacs lacks dynamic modules support and cannot load vterm"))
+ (let ((default-directory
+ (if arg
+ (or (doom-project-root) default-directory)
+ default-directory)))
+ (switch-to-buffer (save-window-excursion (vterm)))))
+
+;;;###autoload
+(defun +vterm/open-popup (arg)
+ "Open a terminal popup window. If ARG (universal argument) is
+non-nil, cd into the current project's root."
+ (interactive "P")
+ (unless (fboundp 'module-load)
+ (user-error "Your build of Emacs lacks dynamic modules support and cannot load vterm"))
+ (let ((default-directory
+ (if arg
+ (or (doom-project-root) default-directory)
+ default-directory)))
+ (pop-to-buffer (save-window-excursion (vterm)))))
+
+;;;###autoload
+(defun +vterm/open-popup-in-project ()
+ "Open a terminal popup window in the root of the current project."
+ (interactive)
+ (+vterm/open-popup t))
diff --git a/modules/tools/vterm/config.el b/modules/tools/vterm/config.el
new file mode 100644
index 000000000..2751cfdfe
--- /dev/null
+++ b/modules/tools/vterm/config.el
@@ -0,0 +1,40 @@
+;;; tools/vterm/config.el -*- lexical-binding: t; -*-
+
+(def-package! vterm
+ :when (fboundp 'module-load)
+ :defer t
+ :preface (setq vterm-install t)
+ :config
+ (set-popup-rule! "^vterm" :size 0.25 :vslot -4 :select t :quit nil :ttl 0)
+
+ (add-hook 'vterm-mode-hook #'doom|mark-buffer-as-real)
+ ;; Automatically kill buffer when vterm exits.
+ (add-to-list 'vterm-exit-functions (lambda (buffer) (if buffer (kill-buffer buffer))))
+
+ (when (featurep! :feature evil)
+ (evil-set-initial-state 'vterm-mode 'insert)
+ ;; Go back to normal state but don't move cursor backwards. Moving cursor
+ ;; backwards is the default Vim behavior but it is not appropriate in some
+ ;; cases like terminals.
+ (setq-hook! 'vterm-mode-hook evil-move-cursor-back nil)
+ ;; Those keys are commonly needed by terminals.
+ (evil-define-key* 'insert vterm-mode-map
+ (kbd "C-a") #'vterm--self-insert
+ (kbd "C-b") #'vterm--self-insert ; Should not be necessary.
+ (kbd "C-d") #'vterm--self-insert
+ (kbd "C-e") #'vterm--self-insert
+ (kbd "C-f") #'vterm--self-insert ; Should not be necessary.
+ (kbd "C-k") #'vterm--self-insert
+ (kbd "C-l") #'vterm--self-insert ; Should not be necessary.
+ (kbd "C-n") #'vterm--self-insert
+ (kbd "C-o") #'vterm--self-insert
+ (kbd "C-p") #'vterm--self-insert
+ (kbd "C-q") #'vterm--self-insert ; Should not be necessary.
+ (kbd "C-r") #'vterm--self-insert
+ (kbd "C-s") #'vterm--self-insert ; Should not be necessary.
+ (kbd "C-t") #'vterm--self-insert
+ (kbd "C-u") #'vterm--self-insert ; Should not be necessary.
+ (kbd "C-v") #'vterm--self-insert ; Should not be necessary.
+ (kbd "C-w") #'vterm--self-insert
+ (kbd "C-y") #'vterm--self-insert
+ (kbd "C-z") #'vterm--self-insert)))
diff --git a/modules/tools/vterm/doctor.el b/modules/tools/vterm/doctor.el
new file mode 100644
index 000000000..0700bafdb
--- /dev/null
+++ b/modules/tools/vterm/doctor.el
@@ -0,0 +1,13 @@
+;;; tools/vterm/doctor.el -*- lexical-binding: t; -*-
+
+(unless (executable-find "vterm-ctrl")
+ (warn! "Couldn't find libvterm. Vterm module won't compile"))
+
+(unless (executable-find "make")
+ (warn! "Couldn't find make command. Vterm module won't compile"))
+
+(unless (executable-find "cmake")
+ (warn! "Couldn't find cmake command. Vterm module won't compile"))
+
+(unless (fboundp 'module-load)
+ (warn! "Your emacs doesn't have MODULES support. Vterm module won't work"))
diff --git a/modules/tools/vterm/packages.el b/modules/tools/vterm/packages.el
new file mode 100644
index 000000000..c0d33a1d5
--- /dev/null
+++ b/modules/tools/vterm/packages.el
@@ -0,0 +1,7 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/vterm/packages.el
+
+(package! vterm :recipe
+ (:fetcher github
+ :repo "akermu/emacs-libvterm"
+ :files ("*")))
diff --git a/modules/tools/vterm/readme.org b/modules/tools/vterm/readme.org
new file mode 100644
index 000000000..077e6a668
--- /dev/null
+++ b/modules/tools/vterm/readme.org
@@ -0,0 +1,94 @@
+#+TITLE: tools/vterm
+#+DATE: January 16, 2019
+#+SINCE: {replace with next tagged release version}
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC_3:noexport:
+- [[Description][Description]]
+- [[Prerequisites][Prerequisites]]
+ - [[Emacs requirement][Emacs requirement]]
+ - [[System requirement][System requirement]]
+ - [[Module requirement][Module requirement]]
+
+* Description
+An [[https://github.com/akermu/emacs-libvterm][emacs-libvterm]] module.
+
+* Prerequisites
+
+** Emacs requirement
+
+You have to compile emacs with =--with-modules= option.
+
+Check the =system-configuration-options= variable to see if your emacs has this
+option.
+
+- On Archlinux or Manjaro, if you install emacs with pacman, this option is
+ enabled.
+- On macOS:
+ - If you use [[https://emacsformacosx.com/][Emacs For Mac OS X]], this option is enabled.
+ - If you use [[https://github.com/d12frosted/homebrew-emacs-plus][emacs-plus]], this option is enabled by default.
+ - If you use [[https://github.com/railwaycat/homebrew-emacsmacport][emacs-mac]], this options is *not* enabled by default. You may have
+ to reinstall emacs with the option:
+ #+BEGIN_SRC sh
+ brew install emacs-mac --with-modules
+ #+END_SRC
+
+** System requirement
+
+You need to have =libvterm= installed in your system.
+
+On Ubuntu or Debian:
+
+#+BEGIN_SRC sh
+sudo apt install libvterm-dev
+#+END_SRC
+
+On ArchLinux or Manjaro:
+
+#+BEGIN_SRC sh
+sudo pacman -S libvterm
+#+END_SRC
+
+On macOS:
+
+#+BEGIN_SRC sh
+brew install libvterm
+#+END_SRC
+
+
+** Module requirement
+
+You have to compile and install the requied module =vterm-module.so=.
+
+In order to compile it you need to have:
+
+- Compilation tools. This include =make=, =cmake= and a c compiler such as
+ =gcc=.
+- Internet connection, because =cmake= will automatically download some requied
+ libraries from the internet.
+
+There are several ways to install the module:
+
+1. You can use =M-x vterm-module-compile= to let emacs automatically compile and
+ install the module.
+
+ *WARNING*: Emacs will hang during the compilation. It may take a while.
+
+2. A fully byte-compile will cause emacs to automatically compile and install
+ the module if it cannot find an executable =vterm-module.so= file.
+
+3. You can compile and install the module yourself. Go to the vterm installation
+ directory, which is usually
+ =~/.emacs.d/.local/packages/elpa/vterm-=, and run the following:
+
+ #+BEGIN_SRC sh
+ mkdir -p build
+ cd build
+ cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
+ make
+ #+END_SRC
+
+4. You can also compile the module at other place, and install the compiled
+ =vterm-module.so= file to your vterm installation folder, which is usually
+ =~/.emacs.d/.local/packages/elpa/vterm-/=. Also make sure the module
+ file is executable.
diff --git a/modules/tools/wakatime/README.org b/modules/tools/wakatime/README.org
new file mode 100644
index 000000000..2427fee03
--- /dev/null
+++ b/modules/tools/wakatime/README.org
@@ -0,0 +1,35 @@
+#+TITLE: tools/wakatime
+#+DATE: June 7, 2018
+#+SINCE: v2.0.4
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC_3:noexport:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Prerequisites][Prerequisites]]
+- [[Configuration][Configuration]]
+ - [[Obfuscate project file names][Obfuscate project file names]]
+
+* Description
+This module integrates [[https://wakatime.com][wakatime]] into Emacs.
+
+** Module Flags
+This module provides no flags.
+
+** Plugins
++ wakatime-mode
+
+* Prerequisites
+This module requires an account and API key from wakatime.com.
+
+Run ~M-x +wakatime/setup~ to set things up.
+
+* Configuration
+** Obfuscate project file names
+#+BEGIN_SRC emacs-lisp
+(setq +wakatime-hide-filenames t)
+#+END_SRC
+
+This will hide what files and folders you're working in from prying eyes on your
+profile.
diff --git a/modules/tools/wakatime/autoload.el b/modules/tools/wakatime/autoload.el
new file mode 100644
index 000000000..e07702ab8
--- /dev/null
+++ b/modules/tools/wakatime/autoload.el
@@ -0,0 +1,65 @@
+;;; tools/wakatime/autoload.el -*- lexical-binding: t; -*-
+
+(defvar +wakatime-home (concat doom-cache-dir "wakatime/")
+ "Path to the directory where wakatime files are stored.")
+
+(defvar +wakatime-hide-filenames nil
+ "If non-nil, obfuscate files and only show what projects you're working on.")
+
+;;;###autoload
+(add-hook 'doom-init-modules-hook #'+wakatime|delayed-autostart)
+
+;;;###autoload
+(defun +wakatime/setup ()
+ "Setup Wakatime in Emacs and start `global-wakatime-mode'.
+
+This will prompt you for your api key. You only need to run this when your api
+changes."
+ (interactive)
+ (when (y-or-n-p "No API key is registered. Open a browser on the wakatime api key page?")
+ (browse-url "https://wakatime.com/settings/api-key"))
+ (let ((api-key (read-string "Enter your wakatime API key: ")))
+ (unless api-key
+ (user-error "No api key was received."))
+ (require 'wakatime-mode)
+ (customize-set-variable 'wakatime-api-key api-key)
+ (customize-save-customized)
+ (unless (or (and wakatime-cli-path (file-executable-p wakatime-cli-path))
+ (not (equal (wakatime-find-binary "wakatime") "wakatime")))
+ (user-error "Couldn't find wakatime executable (%s)"
+ (or wakatime-cli-path "wakatime")))
+ (global-wakatime-mode +1)
+ (message "Wakatime enabled. You're good to go!")))
+
+;;;###autoload
+(defun +wakatime|autostart (&rest _)
+ "Initialize wakatime (if `wakatime-api-key' is set, otherwise no-op with a
+warning)."
+ (interactive)
+ (require 'wakatime-mode)
+ (if (not wakatime-api-key)
+ (message "wakatime-mode isn't set up. Run `M-x +wakatime/setup' to do so.")
+ (when +wakatime-home
+ (unless (file-directory-p +wakatime-home)
+ (make-directory +wakatime-home t)))
+ (global-wakatime-mode +1))
+ ;;
+ (remove-hook 'doom-switch-buffer-hook #'+wakatime|autostart)
+ (advice-remove 'after-find-file #'+wakatime|autostart))
+
+;;;###autoload
+(defun +wakatime|delayed-autostart (&rest _)
+ "Lazily initialize `wakatime-mode' until the next time you switch buffers or
+open a file."
+ (add-hook 'doom-switch-buffer-hook #'+wakatime|autostart)
+ ;; this is necessary in case the user opens emacs with file arguments
+ (advice-add 'after-find-file :before #'+wakatime|autostart))
+
+(defun +wakatime*append-options (ret)
+ "Modifies the wakatime command string so that `+wakatime-hide-filenames' and
+`+wakatime-home' are respected."
+ (concat (when +wakatime-home
+ (format "WAKATIME_HOME=%s " (shell-quote-argument +wakatime-home)))
+ ret
+ (if +wakatime-hide-filenames " --hide-filenames")))
+(advice-add #'wakatime-client-command :filter-return #'+wakatime*append-options)
diff --git a/modules/tools/wakatime/packages.el b/modules/tools/wakatime/packages.el
new file mode 100644
index 000000000..2df681c92
--- /dev/null
+++ b/modules/tools/wakatime/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; tools/wakatime/packages.el
+
+(package! wakatime-mode)
diff --git a/modules/ui/deft/README.org b/modules/ui/deft/README.org
new file mode 100644
index 000000000..4737cfdbb
--- /dev/null
+++ b/modules/ui/deft/README.org
@@ -0,0 +1,11 @@
+#+TITLE: :ui deft
+
+[[https://jblevins.org/projects/deft/][Deft]] is a major mode for browsing and filtering notes written in plain text
+formats, such as org-mode, markdown, and LaTeX.
+
+To use this module, in your config file set the value of the variable ~deft-directory~ to the folder in which you
+want to keep your notes.
+
+The default note format is org-mode. You can change this by setting the value of
+the variable ~deft-default-extension~. Changing the value to ~"md"~ for example,
+will change the default note format to markdown.
diff --git a/modules/ui/deft/config.el b/modules/ui/deft/config.el
new file mode 100644
index 000000000..d1d0d24dc
--- /dev/null
+++ b/modules/ui/deft/config.el
@@ -0,0 +1,32 @@
+;;; ui/deft/config.el -*- lexical-binding: t; -*-
+
+(def-package! deft
+ :commands deft
+ :init
+ (setq deft-extensions '("org" "md" "tex" "txt")
+ deft-default-extension "org"
+ ;; de-couples filename and note title:
+ deft-use-filename-as-title nil
+ deft-use-filter-string-for-filename t
+ deft-org-mode-title-prefix t
+ ;; converts the filter string into a readable file-name using kebab-case:
+ deft-file-naming-rules
+ '((noslash . "-")
+ (nospace . "-")
+ (case-fn . downcase)))
+ :config
+ ;; start filtering immediately
+ (set-evil-initial-state! 'deft-mode 'insert)
+ (map! :map deft-mode-map
+ :localleader
+ "RET" #'deft-new-file-named
+ "a" #'deft-archive-file
+ "c" #'deft-filter-clear
+ "d" #'deft-delete-file
+ "f" #'deft-find-file
+ "g" #'deft-refresh
+ "l" #'deft-filter
+ "n" #'deft-new-file
+ "r" #'deft-rename-file
+ "s" #'deft-toggle-sort-method
+ "t" #'deft-toggle-incremental-search))
diff --git a/modules/ui/deft/packages.el b/modules/ui/deft/packages.el
new file mode 100644
index 000000000..9c4b8f9c1
--- /dev/null
+++ b/modules/ui/deft/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; ui/deft/packages.el
+
+(package! deft)
diff --git a/modules/ui/doom-dashboard/autoload.el b/modules/ui/doom-dashboard/autoload.el
index 55331891c..a116497ee 100644
--- a/modules/ui/doom-dashboard/autoload.el
+++ b/modules/ui/doom-dashboard/autoload.el
@@ -1,24 +1,28 @@
;;; ui/doom-dashboard/autoload.el -*- lexical-binding: t; -*-
-;;;###autoload
-(defun +doom-dashboard/next-button ()
- (interactive)
- (ignore-errors (goto-char (next-button (point)))))
+(defun +doom-dashboard--help-echo ()
+ (when-let* ((btn (button-at (point)))
+ (msg (button-get btn 'help-echo)))
+ (message "%s" msg)))
;;;###autoload
-(defun +doom-dashboard/previous-button ()
- (interactive)
- (ignore-errors (goto-char (previous-button (point)))))
+(defun +doom-dashboard/open (frame)
+ "Switch to the dashboard in the current window, of the current FRAME."
+ (interactive (list (selected-frame)))
+ (with-selected-frame frame
+ (switch-to-buffer (doom-fallback-buffer))
+ (+doom-dashboard-reload t)))
;;;###autoload
-(defun +doom-dashboard/first-button ()
- (interactive)
- (goto-char (point-min))
- (+doom-dashboard/next-button))
+(defun +doom-dashboard/forward-button (n)
+ "Like `forward-button', but don't wrap."
+ (interactive "p")
+ (forward-button n nil)
+ (+doom-dashboard--help-echo))
;;;###autoload
-(defun +doom-dashboard/last-button ()
- (interactive)
- (goto-char (point-max))
- (+doom-dashboard/previous-button)
- (beginning-of-line-text))
+(defun +doom-dashboard/backward-button (n)
+ "Like `backward-button', but don't wrap."
+ (interactive "p")
+ (backward-button n nil)
+ (+doom-dashboard--help-echo))
diff --git a/modules/ui/doom-dashboard/config.el b/modules/ui/doom-dashboard/config.el
index debcc73fc..5444cb559 100644
--- a/modules/ui/doom-dashboard/config.el
+++ b/modules/ui/doom-dashboard/config.el
@@ -1,17 +1,33 @@
;;; ui/doom-dashboard/config.el -*- lexical-binding: t; -*-
-(defvar +doom-dashboard-name " *doom*"
+(defvar +doom-dashboard-name "*doom*"
"The name to use for the dashboard buffer.")
-(defvar +doom-dashboard-widgets '(banner shortmenu loaded)
- "List of widgets to display in a blank scratch buffer.")
+(defvar +doom-dashboard-functions
+ '(doom-dashboard-widget-banner
+ doom-dashboard-widget-shortmenu
+ doom-dashboard-widget-loaded
+ doom-dashboard-widget-footer)
+ "List of widget functions to run in the dashboard buffer to construct the
+dashboard. These functions take no arguments and the dashboard buffer is current
+while they run.")
+
+(defvar +doom-dashboard-banner-file "default.png"
+ "The path to the image file to be used in on the dashboard. The path is
+relative to `+doom-dashboard-banner-dir'. If nil, always use the ASCII banner.")
+
+(defvar +doom-dashboard-banner-dir (concat (DIR!) "banners/")
+ "Where to look for `+doom-dashboard-banner-file'.")
+
+(defvar +doom-dashboard-banner-padding '(4 . 4)
+ "Number of newlines to pad the banner with, above and below, respectively.")
(defvar +doom-dashboard-inhibit-refresh nil
"If non-nil, the doom buffer won't be refreshed.")
(defvar +doom-dashboard-inhibit-functions ()
- "A list of functions that determine whether to inhibit the dashboard from
-loading.")
+ "A list of functions which take no arguments. If any of them return non-nil,
+dashboard reloading is inhibited.")
(defvar +doom-dashboard-pwd-policy 'last-project
"The policy to use when setting the `default-directory' in the dashboard.
@@ -25,144 +41,279 @@ Possible values:
a STRING a fixed path
nil `default-directory' will never change")
+(defvar +doom-dashboard-menu-sections
+ '(("Reload last session"
+ :icon (all-the-icons-octicon "history" :face 'font-lock-keyword-face)
+ :when (cond ((require 'persp-mode nil t)
+ (file-exists-p (expand-file-name persp-auto-save-fname persp-save-dir)))
+ ((require 'desktop nil t)
+ (file-exists-p (desktop-full-file-name))))
+ :face (:inherit (font-lock-keyword-face bold))
+ :action doom/quickload-session)
+ ("Open org-agenda"
+ :icon (all-the-icons-octicon "calendar" :face 'font-lock-keyword-face)
+ :when (fboundp 'org-agenda)
+ :action org-agenda)
+ ("Recently opened files"
+ :icon (all-the-icons-octicon "file-text" :face 'font-lock-keyword-face)
+ :action recentf-open-files)
+ ("Open project"
+ :icon (all-the-icons-octicon "briefcase" :face 'font-lock-keyword-face)
+ :action projectile-switch-project)
+ ("Jump to bookmark"
+ :icon (all-the-icons-octicon "bookmark" :face 'font-lock-keyword-face)
+ :action bookmark-jump)
+ ("Open private configuration"
+ :icon (all-the-icons-octicon "tools" :face 'font-lock-keyword-face)
+ :when (file-directory-p doom-private-dir)
+ :action doom/open-private-config)
+ ("Open user manual"
+ :icon (all-the-icons-octicon "book" :face 'font-lock-keyword-face)
+ :when (file-exists-p (expand-file-name "index.org" doom-docs-dir))
+ :action doom/open-manual))
+ "An alist of menu buttons used by `doom-dashboard-widget-shortmenu'. Each
+element is a cons cell (LABEL . PLIST). LABEL is a string to display after the
+icon and before the key string.
+
+PLIST can have the following properties:
+
+ :icon FORM
+ Uses the return value of FORM as an icon (can be literal string).
+ :key STRING
+ The keybind displayed next to the button.
+ :when FORM
+ If FORM returns nil, don't display this button.
+ :face FACE
+ Displays the icon and text with FACE (a face symbol).
+ :action FORM
+ Run FORM when the button is pushed.")
+
;;
(defvar +doom-dashboard--last-cwd nil)
(defvar +doom-dashboard--width 80)
-(defvar +doom-dashboard--height 0)
(defvar +doom-dashboard--old-fringe-indicator fringe-indicator-alist)
+(defvar +doom-dashboard--pwd-alist ())
(defvar all-the-icons-scale-factor)
(defvar all-the-icons-default-adjust)
-;;
-(setq doom-fallback-buffer +doom-dashboard-name)
+;;
+;;; Bootstrap
+
+(defun +doom-dashboard|init ()
+ "Initializes Doom's dashboard."
+ (unless noninteractive
+ ;; Ensure the dashboard is up-to-date whenever it is switched to or resized.
+ (add-hook 'window-configuration-change-hook #'+doom-dashboard|resize)
+ (add-hook 'window-size-change-functions #'+doom-dashboard|resize)
+ (add-hook 'kill-buffer-query-functions #'+doom-dashboard|reload-on-kill)
+ (add-hook 'doom-switch-buffer-hook #'+doom-dashboard|reload-on-kill)
+ (add-hook 'delete-frame-functions #'+doom-dashboard|reload-frame)
+
+ ;; `persp-mode' integration: update `default-directory' when switching perspectives
+ (add-hook 'persp-created-functions #'+doom-dashboard|record-project)
+ (add-hook 'persp-activated-functions #'+doom-dashboard|detect-project)
+ (add-hook 'persp-before-switch-functions #'+doom-dashboard|record-project)
+
+ ;; Ensure the dashboard becomes Emacs' go-to buffer when there's nothing
+ ;; else to show.
+ (setq doom-fallback-buffer-name +doom-dashboard-name
+ initial-buffer-choice #'doom-fallback-buffer)
+ (when (equal (buffer-name) "*scratch*")
+ (switch-to-buffer (doom-fallback-buffer))
+ (if (daemonp)
+ (add-hook 'after-make-frame-functions #'+doom-dashboard|reload-frame)
+ (+doom-dashboard-reload)))))
+
+(add-hook 'doom-init-ui-hook #'+doom-dashboard|init)
+
+
+;;
+;;; Major mode
(define-derived-mode +doom-dashboard-mode special-mode
(format "DOOM v%s" doom-version)
"Major mode for the DOOM dashboard buffer."
- (read-only-mode +1)
+ :syntax-table nil
+ :abbrev-table nil
(setq truncate-lines t)
(setq-local whitespace-style nil)
(setq-local show-trailing-whitespace nil)
+ (setq-local hscroll-margin 0)
+ (setq-local tab-width 2)
+ ;; Don't scroll to follow cursor
+ (setq-local scroll-preserve-screen-position nil)
+ (setq-local auto-hscroll-mode nil)
(cl-loop for (car . _cdr) in fringe-indicator-alist
collect (cons car nil) into alist
- finally do (setq fringe-indicator-alist alist)))
+ finally do (setq fringe-indicator-alist alist))
+ ;; Ensure point is always on a button
+ (add-hook 'post-command-hook #'+doom-dashboard|reposition-point nil t))
+
+(define-key! +doom-dashboard-mode-map
+ [remap forward-button] #'+doom-dashboard/forward-button
+ [remap backward-button] #'+doom-dashboard/backward-button
+ "n" #'forward-button
+ "p" #'backward-button
+ "C-n" #'forward-button
+ "C-p" #'backward-button
+ [down] #'forward-button
+ [up] #'backward-button
+ [tab] #'forward-button
+ [backtab] #'backward-button)
+
+(map! :when (featurep 'evil)
+ :map +doom-dashboard-mode-map
+ :n "j" #'forward-button
+ :n "k" #'backward-button
+ :n "n" #'forward-button
+ :n "p" #'backward-button
+ :n "C-n" #'forward-button
+ :n "C-p" #'backward-button
+ :n [down] #'forward-button
+ :n [up] #'backward-button
+ :n [tab] #'forward-button
+ :n [backtab] #'backward-button
+ [left-margin mouse-1] #'ignore
+ [remap evil-next-visual-line] #'forward-button
+ [remap evil-previous-visual-line] #'backward-button
+ [remap evil-delete] #'ignore
+ [remap evil-delete-line] #'ignore
+ [remap evil-insert] #'ignore
+ [remap evil-append] #'ignore
+ [remap evil-replace] #'ignore
+ [remap evil-replace-state] #'ignore
+ [remap evil-change] #'ignore
+ [remap evil-change-line] #'ignore
+ [remap evil-visual-char] #'ignore
+ [remap evil-visual-line] #'ignore)
-(map! :map +doom-dashboard-mode-map
- "n" #'+doom-dashboard/next-button
- "p" #'+doom-dashboard/previous-button
- "N" #'+doom-dashboard/last-button
- "P" #'+doom-dashboard/first-button
- :em "j" #'+doom-dashboard/next-button
- :em "k" #'+doom-dashboard/previous-button
- :em "gg" #'+doom-dashboard/first-button
- :em "G" #'+doom-dashboard/last-button
- [remap evil-insert] #'evil-normal-state
- [remap evil-change] #'evil-normal-state
- [remap evil-visual-char] #'evil-normal-state
- [remap evil-visual-line] #'evil-normal-state
- [remap evil-delete] #'evil-normal-state
- [remap evil-delete-char] #'evil-normal-state)
;;
-(defun +doom-dashboard|init ()
- "Initialize doom-dashboard and set up its hooks; possibly open the dashboard
-if in a GUI/non-daemon session."
- (add-hook 'window-configuration-change-hook #'+doom-dashboard-reload)
- (add-hook 'focus-in-hook #'+doom-dashboard-reload)
- (add-hook 'kill-buffer-query-functions #'+doom-dashboard|reload-on-kill)
- (when (and (display-graphic-p) (not (daemonp)))
- (let ((default-directory doom-emacs-dir))
- (+doom-dashboard/open (selected-frame)))))
+;;; Hooks
+
+(defun +doom-dashboard|reposition-point ()
+ "Trap the point in the buttons."
+ (when (region-active-p)
+ (deactivate-mark t)
+ (when (bound-and-true-p evil-local-mode)
+ (evil-change-to-previous-state)))
+ (or (ignore-errors
+ (if (button-at (point))
+ (forward-button 0)
+ (backward-button 1)))
+ (progn (goto-char (point-min))
+ (forward-button 1))))
(defun +doom-dashboard|reload-on-kill ()
- "If this isn't a dashboard buffer, move along, but record its
-`default-directory' if the buffer is real. See `doom-real-buffer-p' for an
-explanation for what 'real' means.
+ "A `kill-buffer-query-functions' hook.
+
+If this isn't a dashboard buffer, move along, but record its `default-directory'
+if the buffer is real. See `doom-real-buffer-p' for an explanation for what
+'real' means.
If this is the dashboard buffer, reload the dashboard."
- (or (unless (+doom-dashboard-p)
- (when (doom-real-buffer-p)
- (setq +doom-dashboard--last-cwd default-directory)
- (+doom-dashboard-update-pwd))
- t)
+ (or (let ((buf (current-buffer)))
+ (unless (+doom-dashboard-p buf)
+ (when (doom-real-buffer-p buf)
+ (setq +doom-dashboard--last-cwd default-directory)
+ (+doom-dashboard-update-pwd))
+ t))
(ignore
(let (+doom-dashboard-inhibit-refresh)
(ignore-errors (+doom-dashboard-reload))))))
-(defun +doom-dashboard|make-frame (frame)
+(defun +doom-dashboard|reload-frame (_frame)
"Reload the dashboard after a brief pause. This is necessary for new frames,
whose dimensions may not be fully initialized by the time this is run."
- (run-with-timer 0.1 nil #'+doom-dashboard/open frame))
+ (run-with-timer 0.1 nil #'+doom-dashboard-reload t))
-(defun +doom-dashboard|server-visit (&rest _)
- "Inhibit dashboard refresh when opening files via emacsclient."
- (setq +doom-dashboard-inhibit-refresh t))
+(defun +doom-dashboard|resize (&rest _)
+ "Recenter the dashboard, and reset its margins and fringes."
+ (let ((windows (get-buffer-window-list (doom-fallback-buffer) nil t))
+ buffer-list-update-hook)
+ (let (window-configuration-change-hook
+ window-size-change-functions)
+ (dolist (win windows)
+ (set-window-start win 0)
+ (set-window-fringes win 0 0)
+ (set-window-margins
+ win (max 0 (/ (- (window-total-width win) +doom-dashboard--width) 2)))))
+ (when windows
+ (with-current-buffer (doom-fallback-buffer)
+ (save-excursion
+ (with-silent-modifications
+ (goto-char (point-min))
+ (delete-region (line-beginning-position)
+ (save-excursion (skip-chars-forward "\n")
+ (point)))
+ (insert (make-string
+ (max 0 (- (/ (window-height (get-buffer-window)) 2)
+ (round (/ (+ (count-lines (point-min) (point-max))
+ (car +doom-dashboard-banner-padding))
+ 2))))
+ ?\n))))))))
-(add-hook 'window-setup-hook #'+doom-dashboard|init)
-(add-hook 'after-make-frame-functions #'+doom-dashboard|make-frame)
-(add-hook 'server-visit-hook #'+doom-dashboard|server-visit)
+(defun +doom-dashboard|detect-project (&rest _)
+ "Check for a `last-project-root' parameter in the perspective, and set the
+dashboard's `default-directory' to it if it exists.
+
+This and `+doom-dashboard|record-project' provides `persp-mode' integration with
+the Doom dashboard. It ensures that the dashboard is always in the correct
+project (which may be different across perspective)."
+ (when (bound-and-true-p persp-mode)
+ (when-let* ((pwd (persp-parameter 'last-project-root)))
+ (+doom-dashboard-update-pwd pwd))))
+
+(defun +doom-dashboard|record-project (&optional persp &rest _)
+ "Record the last `doom-project-root' for the current perspective. See
+`+doom-dashboard|detect-project' for more information."
+ (when (bound-and-true-p persp-mode)
+ (set-persp-parameter
+ 'last-project-root (doom-project-root)
+ (if (perspective-p persp) persp (get-current-persp)))))
;;
-(defun +doom-dashboard/open (frame)
- (interactive (list (selected-frame)))
- (unless (run-hook-with-args-until-success '+doom-dashboard-inhibit-functions)
- (unless +doom-dashboard-inhibit-refresh
- (with-selected-frame frame
- (switch-to-buffer (doom-fallback-buffer))
- (+doom-dashboard-reload)))
- (setq +doom-dashboard-inhibit-refresh nil)))
+;;; Library
-;
-(defun +doom-dashboard-p (&optional buffer)
+(defun +doom-dashboard-p (buffer)
"Returns t if BUFFER is the dashboard buffer."
- (eq (or buffer (current-buffer))
- (doom-fallback-buffer)))
+ (eq buffer (get-buffer +doom-dashboard-name)))
-(defun +doom-dashboard-update-pwd ()
- "TODO"
- (with-current-buffer (doom-fallback-buffer)
- (cd (or (+doom-dashboard--get-pwd)
- default-directory))))
+(defun +doom-dashboard-update-pwd (&optional pwd)
+ "Update `default-directory' in the Doom dashboard buffer. What it is set to is
+controlled by `+doom-dashboard-pwd-policy'."
+ (if pwd
+ (with-current-buffer (doom-fallback-buffer)
+ (setq-local default-directory pwd))
+ (let ((new-pwd (+doom-dashboard--get-pwd)))
+ (when (and new-pwd (file-directory-p new-pwd))
+ (unless (string-suffix-p "/" new-pwd)
+ (setq new-pwd (concat new-pwd "/")))
+ (+doom-dashboard-update-pwd new-pwd)))))
(defun +doom-dashboard-reload (&optional force)
"Update the DOOM scratch buffer (or create it, if it doesn't exist)."
- (let ((fallback-buffer (doom-fallback-buffer)))
- (when (or (and after-init-time
- (not +doom-dashboard-inhibit-refresh)
- (get-buffer-window fallback-buffer)
- (not (window-minibuffer-p (frame-selected-window))))
- force)
- (with-current-buffer fallback-buffer
- (+doom-dashboard-update-pwd)
- (with-silent-modifications
+ (when (or (and (not +doom-dashboard-inhibit-refresh)
+ (get-buffer-window (doom-fallback-buffer))
+ (not (window-minibuffer-p (frame-selected-window)))
+ (not (run-hook-with-args-until-success '+doom-dashboard-inhibit-functions)))
+ force)
+ (with-current-buffer (doom-fallback-buffer)
+ (with-silent-modifications
+ (let ((pt (point)))
(unless (eq major-mode '+doom-dashboard-mode)
(+doom-dashboard-mode))
(erase-buffer)
- (let ((+doom-dashboard--height
- (window-height (get-buffer-window fallback-buffer)))
- (lines 1)
- content)
- (with-temp-buffer
- (dolist (widget-name +doom-dashboard-widgets)
- (funcall (intern (format "doom-dashboard-widget--%s" widget-name)))
- (insert "\n"))
- (setq content (buffer-string)
- lines (count-lines (point-min) (point-max))))
- (insert (make-string (max 0 (- (/ +doom-dashboard--height 2)
- (/ lines 2)))
- ?\n)
- content))
- (unless (button-at (point))
- (goto-char (next-button (point-min)))))))
- ;; Update all dashboard windows
- (dolist (win (get-buffer-window-list fallback-buffer nil t))
- (set-window-fringes win 0 0)
- (set-window-margins
- win (max 0 (/ (- (window-total-width win) +doom-dashboard--width) 2)))))
- t)
+ (run-hooks '+doom-dashboard-functions)
+ (goto-char pt)
+ (+doom-dashboard|reposition-point))
+ (+doom-dashboard|resize)
+ (+doom-dashboard|detect-project)
+ (+doom-dashboard-update-pwd)
+ (current-buffer)))))
;; helpers
(defun +doom-dashboard--center (len s)
@@ -188,78 +339,124 @@ whose dimensions may not be fully initialized by the time this is run."
cwd)))
((eq policy 'last)
lastcwd)
- (t
- (warn "`+doom-dashboard-pwd-policy' has an invalid value of '%s'"
+ ((warn "`+doom-dashboard-pwd-policy' has an invalid value of '%s'"
policy)))))
-;; widgets
-(defun doom-dashboard-widget--banner ()
- (mapc (lambda (line)
- (insert (propertize (+doom-dashboard--center +doom-dashboard--width line)
- 'face 'font-lock-comment-face) " ")
- (insert "\n"))
- '("================= =============== =============== ======== ========"
- "\\\\ . . . . . . .\\\\ //. . . . . . .\\\\ //. . . . . . .\\\\ \\\\. . .\\\\// . . //"
- "||. . ._____. . .|| ||. . ._____. . .|| ||. . ._____. . .|| || . . .\\/ . . .||"
- "|| . .|| ||. . || || . .|| ||. . || || . .|| ||. . || ||. . . . . . . ||"
- "||. . || || . .|| ||. . || || . .|| ||. . || || . .|| || . | . . . . .||"
- "|| . .|| ||. _-|| ||-_ .|| ||. . || || . .|| ||. _-|| ||-_.|\\ . . . . ||"
- "||. . || ||-' || || `-|| || . .|| ||. . || ||-' || || `|\\_ . .|. .||"
- "|| . _|| || || || || ||_ . || || . _|| || || || |\\ `-_/| . ||"
- "||_-' || .|/ || || \\|. || `-_|| ||_-' || .|/ || || | \\ / |-_.||"
- "|| ||_-' || || `-_|| || || ||_-' || || | \\ / | `||"
- "|| `' || || `' || || `' || || | \\ / | ||"
- "|| .===' `===. .==='.`===. .===' /==. | \\/ | ||"
- "|| .==' \\_|-_ `===. .===' _|_ `===. .===' _-|/ `== \\/ | ||"
- "|| .==' _-' `-_ `=' _-' `-_ `=' _-' `-_ /| \\/ | ||"
- "|| .==' _-' '-__\\._-' '-_./__-' `' |. /| | ||"
- "||.==' _-' `' | /==.||"
- "==' _-' E M A C S \\/ `=="
- "\\ _-' `-_ /"
- " `'' ``'")))
-(defun doom-dashboard-widget--loaded ()
+;;
+;;; Widgets
+
+(defun doom-dashboard-widget-banner ()
+ (let ((point (point)))
+ (mapc (lambda (line)
+ (insert (propertize (+doom-dashboard--center +doom-dashboard--width line)
+ 'face 'font-lock-comment-face) " ")
+ (insert "\n"))
+ '("================= =============== =============== ======== ========"
+ "\\\\ . . . . . . .\\\\ //. . . . . . .\\\\ //. . . . . . .\\\\ \\\\. . .\\\\// . . //"
+ "||. . ._____. . .|| ||. . ._____. . .|| ||. . ._____. . .|| || . . .\\/ . . .||"
+ "|| . .|| ||. . || || . .|| ||. . || || . .|| ||. . || ||. . . . . . . ||"
+ "||. . || || . .|| ||. . || || . .|| ||. . || || . .|| || . | . . . . .||"
+ "|| . .|| ||. _-|| ||-_ .|| ||. . || || . .|| ||. _-|| ||-_.|\\ . . . . ||"
+ "||. . || ||-' || || `-|| || . .|| ||. . || ||-' || || `|\\_ . .|. .||"
+ "|| . _|| || || || || ||_ . || || . _|| || || || |\\ `-_/| . ||"
+ "||_-' || .|/ || || \\|. || `-_|| ||_-' || .|/ || || | \\ / |-_.||"
+ "|| ||_-' || || `-_|| || || ||_-' || || | \\ / | `||"
+ "|| `' || || `' || || `' || || | \\ / | ||"
+ "|| .===' `===. .==='.`===. .===' /==. | \\/ | ||"
+ "|| .==' \\_|-_ `===. .===' _|_ `===. .===' _-|/ `== \\/ | ||"
+ "|| .==' _-' `-_ `=' _-' `-_ `=' _-' `-_ /| \\/ | ||"
+ "|| .==' _-' '-__\\._-' '-_./__-' `' |. /| | ||"
+ "||.==' _-' `' | /==.||"
+ "==' _-' E M A C S \\/ `=="
+ "\\ _-' `-_ /"
+ " `'' ``'"))
+ (when (and (stringp +doom-dashboard-banner-file)
+ (display-graphic-p)
+ (file-exists-p! +doom-dashboard-banner-file +doom-dashboard-banner-dir))
+ (let* ((image (create-image (expand-file-name +doom-dashboard-banner-file
+ +doom-dashboard-banner-dir)
+ 'png nil))
+ (size (image-size image nil))
+ (margin (+ 1 (/ (- +doom-dashboard--width (car size)) 2))))
+ (add-text-properties
+ point (point) `(display ,image rear-nonsticky (display)))
+ (when (> margin 0)
+ (save-excursion
+ (goto-char point)
+ (insert (make-string (truncate margin) ? )))))
+ (insert (make-string (or (cdr +doom-dashboard-banner-padding) 0) ?\n)))))
+
+(defun doom-dashboard-widget-loaded ()
(insert
- "\n"
+ "\n\n"
(propertize
(+doom-dashboard--center
+doom-dashboard--width
- (format "Loaded %d packages in %d modules in %.02fs"
- (length doom--package-load-path)
- (hash-table-size doom-modules)
- (if (floatp doom-init-time) doom-init-time 0.0)))
+ (doom|display-benchmark 'return))
'face 'font-lock-comment-face)
"\n"))
-(defun doom-dashboard-widget--shortmenu ()
+(defun doom-dashboard-widget-shortmenu ()
(let ((all-the-icons-scale-factor 1.45)
(all-the-icons-default-adjust -0.02))
- (mapc (lambda (btn)
- (when btn
- (cl-destructuring-bind (label icon fn) btn
- (insert
- (with-temp-buffer
- (insert-text-button
- (concat (all-the-icons-octicon icon :face 'font-lock-keyword-face)
- (propertize (concat " " label) 'face 'font-lock-keyword-face))
- 'action `(lambda (_) ,fn)
- 'follow-link t)
- (+doom-dashboard--center (- +doom-dashboard--width 2) (buffer-string)))
- "\n\n"))))
- `(("Homepage" "mark-github"
- (browse-url "https://github.com/hlissner/doom-emacs"))
- ,(when (and (featurep! :feature workspaces)
- (file-exists-p (expand-file-name persp-auto-save-fname persp-save-dir)))
- '("Reload last session" "history"
- (+workspace/load-session)))
- ,(when (featurep! :org org)
- '("See agenda for this week" "calendar"
- (call-interactively 'org-agenda-list)))
- ("Recently opened files" "file-text"
- (call-interactively (command-remapping 'recentf-open-files)))
- ("Open project" "briefcase"
- (call-interactively (command-remapping 'projectile-switch-project)))
- ("Jump to bookmark" "bookmark"
- (call-interactively (command-remapping 'bookmark-jump)))
- ("Edit emacs.d" "tools"
- (find-file (expand-file-name "init.el" doom-emacs-dir)))))))
+ (insert "\n")
+ (dolist (section +doom-dashboard-menu-sections)
+ (cl-destructuring-bind (label &key icon action when face) section
+ (when (and (fboundp action)
+ (or (null when)
+ (eval when t)))
+ (insert
+ (+doom-dashboard--center
+ (- +doom-dashboard--width 1)
+ (let ((icon (if (stringp icon) icon (eval icon t))))
+ (format (format "%s%%s%%-10s" (if icon "%3s\t" "%3s"))
+ (or icon "")
+ (with-temp-buffer
+ (insert-text-button
+ label
+ 'action
+ `(lambda (_)
+ (call-interactively (or (command-remapping #',action)
+ #',action)))
+ 'face (or face 'font-lock-keyword-face)
+ 'follow-link t
+ 'help-echo
+ (format "%s (%s)" label
+ (propertize (symbol-name action) 'face 'font-lock-constant-face)))
+ (format "%-37s" (buffer-string)))
+ ;; Lookup command keys dynamically
+ (or (when-let* ((key (where-is-internal action nil t)))
+ (with-temp-buffer
+ (save-excursion (insert (key-description key)))
+ (while (re-search-forward "<\\([^>]+\\)>" nil t)
+ (let ((str (match-string 1)))
+ (replace-match
+ (upcase (if (< (length str) 3)
+ str
+ (substring str 0 3))))))
+ (propertize (buffer-string) 'face 'font-lock-constant-face)))
+ ""))))
+ (if (display-graphic-p)
+ "\n\n"
+ "\n")))))))
+
+(defun doom-dashboard-widget-footer ()
+ (insert
+ "\n"
+ (+doom-dashboard--center
+ (- +doom-dashboard--width 2)
+ (with-temp-buffer
+ ;; TODO Publish the site!
+ ;; (insert-text-button (propertize "doomemacs.org" 'face 'font-lock-keyword-face)
+ ;; 'action (lambda (_) (browse-url "http://doomemacs.org"))
+ ;; 'follow-link t
+ ;; 'help-echo "Open home page")
+ ;; (insert (propertize " x " 'face 'font-lock-comment-face))
+ (insert-text-button (or (all-the-icons-octicon "octoface" :face 'all-the-icons-green :height 1.3 :v-adjust -0.15)
+ (propertize "github" 'face 'font-lock-keyword-face))
+ 'action (lambda (_) (browse-url "https://github.com/hlissner/doom-emacs"))
+ 'follow-link t
+ 'help-echo "Open Doom Emacs github page")
+ (buffer-string)))
+ "\n"))
diff --git a/modules/ui/doom-dashboard/test/doom-dashboard.el b/modules/ui/doom-dashboard/test/doom-dashboard.el
deleted file mode 100644
index b08188837..000000000
--- a/modules/ui/doom-dashboard/test/doom-dashboard.el
+++ /dev/null
@@ -1,51 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; ui/doom-dashboard/test/doom-dashboard.el
-
-(require! :ui doom-dashboard)
-(+doom-dashboard|init)
-
-(defun -dashboard-test-pwd (spec file)
- (let ((kill-buffer-query-functions '(+doom-dashboard|reload-on-kill))
- (+doom-dashboard-pwd-policy (car spec))
- (fallback-buffer (doom-fallback-buffer))
- +doom-dashboard--last-cwd
- projectile-enable-caching)
- (with-temp-buffer
- (setq buffer-file-name file
- default-directory (file-name-directory file)
- doom-real-buffer-p t))
- (should +doom-dashboard--last-cwd)
- (+doom-dashboard-update-pwd)
- (should (equal (buffer-local-value 'default-directory fallback-buffer)
- (cdr spec)))))
-
-;;
-(def-test! dashboard-p
- (let ((fallback-buffer (doom-fallback-buffer)))
- (should (equal (buffer-name fallback-buffer) +doom-dashboard-name))
- (should (+doom-dashboard-p fallback-buffer))
- (with-current-buffer fallback-buffer
- (should (+doom-dashboard-p)))))
-
-(def-test! get-pwd
- (let ((default-directory doom-core-dir)
- (+doom-dashboard--last-cwd doom-core-dir)
- projectile-enable-caching)
- (dolist (spec (list (cons 'last-project doom-emacs-dir)
- (cons 'last doom-core-dir)
- (cons (lambda (x) "x") "x")
- (cons "~" (expand-file-name "~"))
- (cons nil default-directory)))
- (let ((+doom-dashboard-pwd-policy (car spec)))
- (should (equal (+doom-dashboard--get-pwd) (cdr spec)))))))
-
-(def-test! pwd-policy
- (dolist (spec (list (cons 'last-project doom-emacs-dir)
- (cons 'last doom-core-dir)
- (cons "~" (expand-file-name "~/"))
- (cons (lambda (x) "/tmp") "/tmp/")))
- (-dashboard-test-pwd spec (expand-file-name "core.el" doom-core-dir))))
-
-;;
-(def-test! inhibit-refresh :skip t)
-(def-test! inhibit-functions :skip t)
diff --git a/modules/ui/doom-dashboard/test/test-doom-dashboard.el b/modules/ui/doom-dashboard/test/test-doom-dashboard.el
new file mode 100644
index 000000000..a00d7709e
--- /dev/null
+++ b/modules/ui/doom-dashboard/test/test-doom-dashboard.el
@@ -0,0 +1,40 @@
+;; -*- no-byte-compile: t; -*-
+;;; ui/doom-dashboard/test/test-doom-dashboard.el
+
+(require 'core-projects)
+(require 'projectile)
+(require! :ui doom-dashboard)
+
+(describe "ui/doom-dashboard"
+ :var (default-directory projectile-enable-caching)
+ (before-all
+ (setq projectile-enable-caching nil
+ doom-fallback-buffer-name +doom-dashboard-name))
+
+ (before-each (projectile-mode +1))
+ (after-each (projectile-mode -1))
+
+ (describe "get-pwd"
+ :var (+doom-dashboard--last-cwd)
+ (before-each
+ (setq +doom-dashboard--last-cwd doom-core-dir
+ default-directory doom-core-dir))
+ (it "returns the current directory when policy is nil"
+ (let (+doom-dashboard-pwd-policy)
+ (expect (+doom-dashboard--get-pwd) :to-equal default-directory)))
+ (it "returns a path if policy is a path"
+ (let ((+doom-dashboard-pwd-policy "~"))
+ (expect (+doom-dashboard--get-pwd) :to-equal (expand-file-name "~"))))
+ (it "returns return value of policy as a function"
+ (let ((+doom-dashboard-pwd-policy (lambda (x) "x")))
+ (expect (+doom-dashboard--get-pwd) :to-equal "x")))
+ (it "returns last cwd if policy is 'last"
+ (let ((+doom-dashboard-pwd-policy 'last))
+ (expect (+doom-dashboard--get-pwd) :to-equal doom-core-dir)))
+ (it "returns last project if policy is 'last-project"
+ (let ((+doom-dashboard-pwd-policy 'last-project))
+ (expect (+doom-dashboard--get-pwd) :to-equal doom-emacs-dir))))
+
+ (describe "dashboard-p"
+ (it "changes the fallback buffer to the dashboard buffer"
+ (expect (+doom-dashboard-p (doom-fallback-buffer))))))
diff --git a/modules/ui/doom-modeline/README.org b/modules/ui/doom-modeline/README.org
deleted file mode 100644
index fcb8f4685..000000000
--- a/modules/ui/doom-modeline/README.org
+++ /dev/null
@@ -1,52 +0,0 @@
-#+TITLE: :ui doom-modeline
-
-This module customizes the Emacs mode-line.
-
-The DOOM modeline was designed for minimalism, and offers:
-
-+ A match count panel (for ~evil-search~, ~iedit~ and ~evil-substitute~)
-+ An indicator for recording a macro
-+ Local python/ruby version in the major-mode
-+ A customizable mode-line height (see ~+doom-modeline-height~)
-+ An error/warning count segment for flycheck
-
-[[/../screenshots/ml.png]]
-[[/../screenshots/ml-search.png]]
-[[/../screenshots/ml-subst.png]]
-[[/../screenshots/ml-macro.png]]
-[[/../screenshots/ml-version.png]]
-[[/../screenshots/ml-errors.png]]
-
-* Table of Contents :TOC:
-- [[#install][Install]]
-- [[#extracting-my-modeline][Extracting my modeline]]
-- [[#troubleshooting][Troubleshooting]]
- - [[#where-are-my-minor-modes][Where are my minor modes?]]
-
-* Install
-This module requires the fonts included with ~all-the-icons~ to be installed.
-
-Run ~M-x all-the-icons-install-fonts~ to do so.
-
-* Extracting my modeline
-Some might want my modeline without the DOOM config altogether. I've tried to make this easier for you, but there are a few things you'll need to do:
-
-+ Ensure [[https://github.com/bbatsov/projectile][projectile]] and [[https://github.com/domtronn/all-the-icons.el][all-the-icons]] are installed.
-+ Ensure ~projectile-mode~ is enabled.
-+ Ensure the fonts included with ~all-the-icons~ are installed (~M-x all-the-icons-install-fonts~).
-+ Replace ~def-package!~ calls with ~use-package~.
-+ Replace ~doom-project-root~ calls with ~projectile-project-root~.
-+ The ~+doom-modeline--make-xpm~ function is memoized with the ~def-memoized!~ macro. Change ~def-memoized!~ to ~defun~.
-+ Copy the ~add-hook!~ macro definition from [[/master/core/core-lib.el][core/core-lib.el]].
-+ Copy the following macros and functions from [[/master/core/core-ui.el][core/core-ui.el]]:
- + ~def-modeline-segment!~
- + ~def-modeline!~
- + ~doom--prepare-modeline-segments~
- + ~doom-modeline~
- + ~doom-set-modeline~
-
-That /should/ be everything. As I have never used this out of my config I can't guarantee immediate success, but I'd be happy to help you out if you file an issue.
-
-* Troubleshooting
-** Where are my minor modes?
-I didn't need it, so I removed it. Run ~M-x doom/what-minor-mode~ to investigate what minor modes are currently active.
diff --git a/modules/ui/doom-modeline/config.el b/modules/ui/doom-modeline/config.el
deleted file mode 100644
index 5e21e6bf8..000000000
--- a/modules/ui/doom-modeline/config.el
+++ /dev/null
@@ -1,632 +0,0 @@
-;;; ui/doom-modeline/config.el -*- lexical-binding: t; -*-
-
-(def-package! eldoc-eval
- :config
- (defun +doom-modeline-eldoc (text)
- (concat (when (display-graphic-p)
- (+doom-modeline--make-xpm
- (face-background 'doom-modeline-eldoc-bar nil t)
- +doom-modeline-height
- +doom-modeline-bar-width))
- text))
-
- ;; Show eldoc in the mode-line with `eval-expression'
- (defun +doom-modeline--show-eldoc (input)
- "Display string STR in the mode-line next to minibuffer."
- (with-current-buffer (eldoc-current-buffer)
- (let* ((str (and (stringp input) input))
- (mode-line-format (or (and str (or (+doom-modeline-eldoc str) str))
- mode-line-format))
- mode-line-in-non-selected-windows)
- (force-mode-line-update)
- (sit-for eldoc-show-in-mode-line-delay))))
- (setq eldoc-in-minibuffer-show-fn #'+doom-modeline--show-eldoc)
-
- (eldoc-in-minibuffer-mode +1))
-
-;; anzu and evil-anzu expose current/total state that can be displayed in the
-;; mode-line.
-(def-package! evil-anzu
- :requires evil
- :init
- (add-transient-hook! #'evil-ex-start-search (require 'evil-anzu))
- (add-transient-hook! #'evil-ex-start-word-search (require 'evil-anzu))
- :config
- (setq anzu-cons-mode-line-p nil
- anzu-minimum-input-length 1
- anzu-search-threshold 250)
- ;; Avoid anzu conflicts across buffers
- (mapc #'make-variable-buffer-local
- '(anzu--total-matched anzu--current-position anzu--state
- anzu--cached-count anzu--cached-positions anzu--last-command
- anzu--last-isearch-string anzu--overflow-p))
- ;; Ensure anzu state is cleared when searches & iedit are done
- (add-hook 'isearch-mode-end-hook #'anzu--reset-status t)
- (add-hook '+evil-esc-hook #'anzu--reset-status t)
- (add-hook 'iedit-mode-end-hook #'anzu--reset-status))
-
-
-;; Keep `+doom-modeline-current-window' up-to-date
-(defvar +doom-modeline-current-window (frame-selected-window))
-(defun +doom-modeline|set-selected-window (&rest _)
- "Sets `+doom-modeline-current-window' appropriately"
- (when-let* ((win (frame-selected-window)))
- (unless (minibuffer-window-active-p win)
- (setq +doom-modeline-current-window win))))
-
-(add-hook 'window-configuration-change-hook #'+doom-modeline|set-selected-window)
-(add-hook 'focus-in-hook #'+doom-modeline|set-selected-window)
-(advice-add #'handle-switch-frame :after #'+doom-modeline|set-selected-window)
-(advice-add #'select-window :after #'+doom-modeline|set-selected-window)
-
-;; fish-style modeline
-(def-package! shrink-path
- :commands (shrink-path-prompt shrink-path-file-mixed))
-
-
-;;
-;; Variables
-;;
-
-(defvar +doom-modeline-height 29
- "How tall the mode-line should be (only respected in GUI emacs).")
-
-(defvar +doom-modeline-bar-width 3
- "How wide the mode-line bar should be (only respected in GUI emacs).")
-
-(defvar +doom-modeline-vspc
- (propertize " " 'face 'variable-pitch)
- "TODO")
-
-(defvar +doom-modeline-buffer-file-name-style 'truncate-upto-project
- "Determines the style used by `+doom-modeline-buffer-file-name'.
-
-Given ~/Projects/FOSS/emacs/lisp/comint.el
-truncate-upto-project => ~/P/F/emacs/lisp/comint.el
-truncate-upto-root => ~/P/F/e/lisp/comint.el
-truncate-all => ~/P/F/e/l/comint.el
-relative-from-project => emacs/lisp/comint.el
-relative-to-project => lisp/comint.el
-file-name => comint.el")
-
-;; externs
-(defvar anzu--state nil)
-(defvar evil-mode nil)
-(defvar evil-state nil)
-(defvar evil-visual-selection nil)
-(defvar iedit-mode nil)
-(defvar all-the-icons-scale-factor)
-(defvar all-the-icons-default-adjust)
-
-
-;;
-;; Custom faces
-;;
-
-(defgroup +doom-modeline nil
- ""
- :group 'doom)
-
-(defface doom-modeline-buffer-path
- '((t (:inherit (mode-line-emphasis bold))))
- "Face used for the dirname part of the buffer path."
- :group '+doom-modeline)
-
-(defface doom-modeline-buffer-file
- '((t (:inherit (mode-line-buffer-id bold))))
- "Face used for the filename part of the mode-line buffer path."
- :group '+doom-modeline)
-
-(defface doom-modeline-buffer-modified
- '((t (:inherit (error bold) :background nil)))
- "Face used for the 'unsaved' symbol in the mode-line."
- :group '+doom-modeline)
-
-(defface doom-modeline-buffer-major-mode
- '((t (:inherit (mode-line-emphasis bold))))
- "Face used for the major-mode segment in the mode-line."
- :group '+doom-modeline)
-
-(defface doom-modeline-highlight
- '((t (:inherit mode-line-emphasis)))
- "Face for bright segments of the mode-line."
- :group '+doom-modeline)
-
-(defface doom-modeline-panel
- '((t (:inherit mode-line-highlight)))
- "Face for 'X out of Y' segments, such as `+doom-modeline--anzu', `+doom-modeline--evil-substitute' and
-`iedit'"
- :group '+doom-modeline)
-
-(defface doom-modeline-info
- `((t (:inherit (success bold))))
- "Face for info-level messages in the modeline. Used by `*vc'."
- :group '+doom-modeline)
-
-(defface doom-modeline-warning
- `((t (:inherit (warning bold))))
- "Face for warnings in the modeline. Used by `*flycheck'"
- :group '+doom-modeline)
-
-(defface doom-modeline-urgent
- `((t (:inherit (error bold))))
- "Face for errors in the modeline. Used by `*flycheck'"
- :group '+doom-modeline)
-
-;; Bar
-(defface doom-modeline-bar '((t (:inherit highlight)))
- "The face used for the left-most bar on the mode-line of an active window."
- :group '+doom-modeline)
-
-(defface doom-modeline-eldoc-bar '((t (:inherit shadow)))
- "The face used for the left-most bar on the mode-line when eldoc-eval is
-active."
- :group '+doom-modeline)
-
-(defface doom-modeline-inactive-bar '((t (:inherit warning :inverse-video t)))
- "The face used for the left-most bar on the mode-line of an inactive window."
- :group '+doom-modeline)
-
-
-;;
-;; Modeline helpers
-;;
-
-(defsubst active ()
- (eq (selected-window) +doom-modeline-current-window))
-
-;; Inspired from `powerline's `pl/make-xpm'.
-(def-memoized! +doom-modeline--make-xpm (color height width)
- "Create an XPM bitmap."
- (propertize
- " " 'display
- (let ((data (make-list height (make-list width 1)))
- (color (or color "None")))
- (create-image
- (concat
- (format "/* XPM */\nstatic char * percent[] = {\n\"%i %i 2 1\",\n\". c %s\",\n\" c %s\","
- (length (car data))
- (length data)
- color
- color)
- (apply #'concat
- (cl-loop with idx = 0
- with len = (length data)
- for dl in data
- do (cl-incf idx)
- collect
- (concat "\""
- (cl-loop for d in dl
- if (= d 0) collect (string-to-char " ")
- else collect (string-to-char "."))
- (if (eq idx len) "\"};" "\",\n")))))
- 'xpm t :ascent 'center))))
-
-(defun +doom-modeline-buffer-file-name ()
- "Propertized `buffer-file-name' based on `+doom-modeline-buffer-file-name-style'."
- (propertize
- (pcase +doom-modeline-buffer-file-name-style
- ('truncate-upto-project (+doom-modeline--buffer-file-name 'shrink))
- ('truncate-upto-root (+doom-modeline--buffer-file-name-truncate))
- ('truncate-all (+doom-modeline--buffer-file-name-truncate t))
- ('relative-to-project (+doom-modeline--buffer-file-name-relative))
- ('relative-from-project (+doom-modeline--buffer-file-name-relative 'include-project))
- ('file-name (propertize (file-name-nondirectory buffer-file-name)
- 'face
- (let ((face (or (and (buffer-modified-p)
- 'doom-modeline-buffer-modified)
- (and (active)
- 'doom-modeline-buffer-file))))
- (when face `(:inherit ,face))))))
- 'help-echo buffer-file-truename))
-
-(defun +doom-modeline--buffer-file-name-truncate (&optional truncate-tail)
- "Propertized `buffer-file-name' that truncates every dir along path.
-If TRUNCATE-TAIL is t also truncate the parent directory of the file."
- (let ((dirs (shrink-path-prompt (file-name-directory buffer-file-truename)))
- (active (active)))
- (if (null dirs)
- (propertize "%b" 'face (if active 'doom-modeline-buffer-file))
- (let ((modified-faces (if (buffer-modified-p) 'doom-modeline-buffer-modified)))
- (let ((dirname (car dirs))
- (basename (cdr dirs))
- (dir-faces (or modified-faces (if active 'doom-modeline-project-root-dir)))
- (file-faces (or modified-faces (if active 'doom-modeline-buffer-file))))
- (concat (propertize (concat dirname
- (if truncate-tail (substring basename 0 1) basename)
- "/")
- 'face (if dir-faces `(:inherit ,dir-faces)))
- (propertize (file-name-nondirectory buffer-file-name)
- 'face (if file-faces `(:inherit ,file-faces)))))))))
-
-(defun +doom-modeline--buffer-file-name-relative (&optional include-project)
- "Propertized `buffer-file-name' showing directories relative to project's root only."
- (let ((root (doom-project-root))
- (active (active)))
- (if (null root)
- (propertize "%b" 'face (if active 'doom-modeline-buffer-file))
- (let* ((modified-faces (if (buffer-modified-p) 'doom-modeline-buffer-modified))
- (relative-dirs (file-relative-name (file-name-directory buffer-file-truename)
- (if include-project (concat root "../") root)))
- (relative-faces (or modified-faces (if active 'doom-modeline-buffer-path)))
- (file-faces (or modified-faces (if active 'doom-modeline-buffer-file))))
- (if (equal "./" relative-dirs) (setq relative-dirs ""))
- (concat (propertize relative-dirs 'face (if relative-faces `(:inherit ,relative-faces)))
- (propertize (file-name-nondirectory buffer-file-truename)
- 'face (if file-faces `(:inherit ,file-faces))))))))
-
-(defun +doom-modeline--buffer-file-name (truncate-project-root-parent)
- "Propertized `buffer-file-name'.
-If TRUNCATE-PROJECT-ROOT-PARENT is t space will be saved by truncating it down
-fish-shell style.
-
-Example:
-~/Projects/FOSS/emacs/lisp/comint.el => ~/P/F/emacs/lisp/comint.el"
- (let* ((project-root (doom-project-root))
- (file-name-split (shrink-path-file-mixed project-root
- (file-name-directory buffer-file-truename)
- buffer-file-truename))
- (active (active)))
- (if (null file-name-split)
- (propertize "%b" 'face (if active 'doom-modeline-buffer-file))
- (pcase-let ((`(,root-path-parent ,project ,relative-path ,filename) file-name-split))
- (let ((modified-faces (if (buffer-modified-p) 'doom-modeline-buffer-modified)))
- (let ((sp-faces (or modified-faces (if active 'font-lock-comment-face)))
- (project-faces (or modified-faces (if active 'font-lock-string-face)))
- (relative-faces (or modified-faces (if active 'doom-modeline-buffer-path)))
- (file-faces (or modified-faces (if active 'doom-modeline-buffer-file))))
- (let ((sp-props `(,@(if sp-faces `(:inherit ,sp-faces)) ,@(if active '(:weight bold))))
- (project-props `(,@(if project-faces `(:inherit ,project-faces)) ,@(if active '(:weight bold))))
- (relative-props `(,@(if relative-faces `(:inherit ,relative-faces))))
- (file-props `(,@(if file-faces `(:inherit ,file-faces)))))
- (concat (propertize (if truncate-project-root-parent
- root-path-parent
- (abbreviate-file-name project-root))
- 'face sp-props)
- (propertize (concat project "/") 'face project-props)
- (if relative-path (propertize relative-path 'face relative-props))
- (propertize filename 'face file-props)))))))))
-
-
-;;
-;; Segments
-;;
-
-(def-modeline-segment! buffer-default-directory
- "Displays `default-directory'. This is for special buffers like the scratch
-buffer where knowing the current project directory is important."
- (let ((face (if (active) 'doom-modeline-buffer-path)))
- (concat (if (display-graphic-p) " ")
- (all-the-icons-octicon
- "file-directory"
- :face face
- :v-adjust -0.05
- :height 1.25)
- (propertize (concat " " (abbreviate-file-name default-directory))
- 'face face))))
-
-;;
-(def-modeline-segment! buffer-info
- "Combined information about the current buffer, including the current working
-directory, the file name, and its state (modified, read-only or non-existent)."
- (concat (cond (buffer-read-only
- (concat (all-the-icons-octicon
- "lock"
- :face 'doom-modeline-warning
- :v-adjust -0.05)
- " "))
- ((buffer-modified-p)
- (concat (all-the-icons-faicon
- "floppy-o"
- :face 'doom-modeline-buffer-modified
- :v-adjust -0.0575)
- " "))
- ((and buffer-file-name
- (not (file-exists-p buffer-file-name)))
- (concat (all-the-icons-octicon
- "circle-slash"
- :face 'doom-modeline-urgent
- :v-adjust -0.05)
- " "))
- ((buffer-narrowed-p)
- (concat (all-the-icons-octicon
- "fold"
- :face 'doom-modeline-warning
- :v-adjust -0.05)
- " ")))
- (if buffer-file-name
- (+doom-modeline-buffer-file-name)
- "%b")))
-
-;;
-(def-modeline-segment! buffer-info-simple
- "Display only the current buffer's name, but with fontification."
- (propertize
- "%b"
- 'face (cond ((and buffer-file-name (buffer-modified-p))
- 'doom-modeline-buffer-modified)
- ((active) 'doom-modeline-buffer-file))))
-
-;;
-(def-modeline-segment! buffer-encoding
- "Displays the encoding and eol style of the buffer the same way Atom does."
- (concat (pcase (coding-system-eol-type buffer-file-coding-system)
- (0 "LF ")
- (1 "CRLF ")
- (2 "CR "))
- (let ((sys (coding-system-plist buffer-file-coding-system)))
- (cond ((memq (plist-get sys :category) '(coding-category-undecided coding-category-utf-8))
- "UTF-8")
- (t (upcase (symbol-name (plist-get sys :name))))))
- " "))
-
-;;
-(def-modeline-segment! major-mode
- "The major mode, including process, environment and text-scale info."
- (propertize
- (concat (format-mode-line mode-name)
- (when (stringp mode-line-process)
- mode-line-process)
- (and (featurep 'face-remap)
- (/= text-scale-mode-amount 0)
- (format " (%+d)" text-scale-mode-amount)))
- 'face (if (active) 'doom-modeline-buffer-major-mode)))
-
-;;
-(def-modeline-segment! vcs
- "Displays the current branch, colored based on its state."
- (when (and vc-mode buffer-file-name)
- (let* ((backend (vc-backend buffer-file-name))
- (state (vc-state buffer-file-name backend)))
- (let ((face 'mode-line-inactive)
- (active (active))
- (all-the-icons-default-adjust -0.1))
- (concat " "
- (cond ((memq state '(edited added))
- (if active (setq face 'doom-modeline-info))
- (all-the-icons-octicon
- "git-compare"
- :face face
- :v-adjust -0.05))
- ((eq state 'needs-merge)
- (if active (setq face 'doom-modeline-info))
- (all-the-icons-octicon "git-merge" :face face))
- ((eq state 'needs-update)
- (if active (setq face 'doom-modeline-warning))
- (all-the-icons-octicon "arrow-down" :face face))
- ((memq state '(removed conflict unregistered))
- (if active (setq face 'doom-modeline-urgent))
- (all-the-icons-octicon "alert" :face face))
- (t
- (if active (setq face 'font-lock-doc-face))
- (all-the-icons-octicon
- "git-compare"
- :face face
- :v-adjust -0.05)))
- " "
- (propertize (substring vc-mode (+ (if (eq backend 'Hg) 2 3) 2))
- 'face (if active face))
- " ")))))
-
-;;
-(defun +doom-ml-icon (icon &optional text face voffset)
- "Displays an octicon ICON with FACE, followed by TEXT. Uses
-`all-the-icons-octicon' to fetch the icon."
- (concat (if vc-mode " " " ")
- (when icon
- (concat
- (all-the-icons-material icon :face face :height 1.1 :v-adjust (or voffset -0.2))
- (if text +doom-modeline-vspc)))
- (when text
- (propertize text 'face face))
- (if vc-mode " " " ")))
-
-(def-modeline-segment! flycheck
- "Displays color-coded flycheck error status in the current buffer with pretty
-icons."
- (when (boundp 'flycheck-last-status-change)
- (pcase flycheck-last-status-change
- ('finished (if flycheck-current-errors
- (let-alist (flycheck-count-errors flycheck-current-errors)
- (let ((sum (+ (or .error 0) (or .warning 0))))
- (+doom-ml-icon "do_not_disturb_alt"
- (number-to-string sum)
- (if .error 'doom-modeline-urgent 'doom-modeline-warning)
- -0.25)))
- (+doom-ml-icon "check" nil 'doom-modeline-info)))
- ('running (+doom-ml-icon "access_time" nil 'font-lock-doc-face -0.25))
- ('no-checker (+doom-ml-icon "sim_card_alert" "-" 'font-lock-doc-face))
- ('errored (+doom-ml-icon "sim_card_alert" "Error" 'doom-modeline-urgent))
- ('interrupted (+doom-ml-icon "pause" "Interrupted" 'font-lock-doc-face)))))
- ;; ('interrupted (+doom-ml-icon "x" "Interrupted" 'font-lock-doc-face)))))
-
-;;
-(defsubst doom-column (pos)
- (save-excursion (goto-char pos)
- (current-column)))
-
-(def-modeline-segment! selection-info
- "Information about the current selection, such as how many characters and
-lines are selected, or the NxM dimensions of a block selection."
- (when (and (active) (or mark-active (eq evil-state 'visual)))
- (let ((reg-beg (region-beginning))
- (reg-end (region-end)))
- (propertize
- (let ((lines (count-lines reg-beg (min (1+ reg-end) (point-max)))))
- (cond ((or (bound-and-true-p rectangle-mark-mode)
- (eq 'block evil-visual-selection))
- (let ((cols (abs (- (doom-column reg-end)
- (doom-column reg-beg)))))
- (format "%dx%dB" lines cols)))
- ((eq 'line evil-visual-selection)
- (format "%dL" lines))
- ((> lines 1)
- (format "%dC %dL" (- (1+ reg-end) reg-beg) lines))
- (t
- (format "%dC" (- (1+ reg-end) reg-beg)))))
- 'face 'doom-modeline-highlight))))
-
-
-;;
-(defun +doom-modeline--macro-recording ()
- "Display current Emacs or evil macro being recorded."
- (when (and (active) (or defining-kbd-macro executing-kbd-macro))
- (let ((sep (propertize " " 'face 'doom-modeline-panel)))
- (concat sep
- (propertize (if (bound-and-true-p evil-this-macro)
- (char-to-string evil-this-macro)
- "Macro")
- 'face 'doom-modeline-panel)
- sep
- (all-the-icons-octicon "triangle-right"
- :face 'doom-modeline-panel
- :v-adjust -0.05)
- sep))))
-
-(defsubst +doom-modeline--anzu ()
- "Show the match index and total number thereof. Requires `anzu', also
-`evil-anzu' if using `evil-mode' for compatibility with `evil-search'."
- (when (and anzu--state (not iedit-mode))
- (propertize
- (let ((here anzu--current-position)
- (total anzu--total-matched))
- (cond ((eq anzu--state 'replace-query)
- (format " %d replace " total))
- ((eq anzu--state 'replace)
- (format " %d/%d " here total))
- (anzu--overflow-p
- (format " %s+ " total))
- (t
- (format " %s/%d " here total))))
- 'face (if (active) 'doom-modeline-panel))))
-
-(defsubst +doom-modeline--evil-substitute ()
- "Show number of matches for evil-ex substitutions and highlights in real time."
- (when (and evil-mode
- (or (assq 'evil-ex-substitute evil-ex-active-highlights-alist)
- (assq 'evil-ex-global-match evil-ex-active-highlights-alist)
- (assq 'evil-ex-buffer-match evil-ex-active-highlights-alist)))
- (propertize
- (let ((range (if evil-ex-range
- (cons (car evil-ex-range) (cadr evil-ex-range))
- (cons (line-beginning-position) (line-end-position))))
- (pattern (car-safe (evil-delimited-arguments evil-ex-argument 2))))
- (if pattern
- (format " %s matches " (how-many pattern (car range) (cdr range)))
- " - "))
- 'face (if (active) 'doom-modeline-panel))))
-
-(defun doom-themes--overlay-sort (a b)
- (< (overlay-start a) (overlay-start b)))
-
-(defsubst +doom-modeline--iedit ()
- "Show the number of iedit regions matches + what match you're on."
- (when (and iedit-mode iedit-occurrences-overlays)
- (propertize
- (let ((this-oc (or (let ((inhibit-message t))
- (iedit-find-current-occurrence-overlay))
- (progn (iedit-prev-occurrence)
- (iedit-find-current-occurrence-overlay))))
- (length (length iedit-occurrences-overlays)))
- (format " %s/%d "
- (if this-oc
- (- length
- (length (memq this-oc (sort (append iedit-occurrences-overlays nil)
- #'doom-themes--overlay-sort)))
- -1)
- "-")
- length))
- 'face (if (active) 'doom-modeline-panel))))
-
-(def-modeline-segment! matches
- "Displays: 1. the currently recording macro, 2. A current/total for the
-current search term (with anzu), 3. The number of substitutions being conducted
-with `evil-ex-substitute', and/or 4. The number of active `iedit' regions."
- (let ((meta (concat (+doom-modeline--macro-recording)
- (+doom-modeline--anzu)
- (+doom-modeline--evil-substitute)
- (+doom-modeline--iedit))))
- (or (and (not (equal meta "")) meta)
- (if buffer-file-name " %I "))))
-
-;; TODO Include other information
-(def-modeline-segment! media-info
- "Metadata regarding the current file, such as dimensions for images."
- (cond ((eq major-mode 'image-mode)
- (cl-destructuring-bind (width . height)
- (image-size (image-get-display-property) :pixels)
- (format " %dx%d " width height)))))
-
-(def-modeline-segment! bar
- "The bar regulates the height of the mode-line in GUI Emacs.
-Returns \"\" to not break --no-window-system."
- (if (display-graphic-p)
- (+doom-modeline--make-xpm
- (face-background (if (active)
- 'doom-modeline-bar
- 'doom-modeline-inactive-bar)
- nil t)
- +doom-modeline-height
- +doom-modeline-bar-width)
- ""))
-
-
-;;
-;; Mode lines
-;;
-
-(def-modeline! main
- (bar matches " " buffer-info " %l:%c %p " selection-info)
- (buffer-encoding major-mode vcs flycheck))
-
-(def-modeline! minimal
- (bar matches " " buffer-info)
- (media-info major-mode))
-
-(def-modeline! special
- (bar matches " " buffer-info-simple " %l:%c %p " selection-info)
- (buffer-encoding major-mode flycheck))
-
-(def-modeline! project
- (bar buffer-default-directory)
- (major-mode))
-
-(def-modeline! media
- (bar " %b ")
- (media-info major-mode))
-
-
-;;
-;; Hooks
-;;
-
-(defun +doom-modeline|init ()
- "Set the default modeline."
- (doom-set-modeline 'main t)
-
- ;; This scratch buffer is already created and doesn't get a modeline. For the
- ;; love of Emacs, someone give the man a modeline!
- (with-current-buffer "*scratch*"
- (doom-set-modeline 'main)))
-
-(defun +doom-modeline|set-special-modeline ()
- (doom-set-modeline 'special))
-
-(defun +doom-modeline|set-media-modeline ()
- (doom-set-modeline 'media))
-
-(defun +doom-modeline|set-project-modeline ()
- (doom-set-modeline 'project))
-
-
-;;
-;; Bootstrap
-;;
-
-(add-hook 'doom-init-ui-hook #'+doom-modeline|init)
-(add-hook 'doom-scratch-buffer-hook #'+doom-modeline|set-special-modeline)
-(add-hook '+doom-dashboard-mode-hook #'+doom-modeline|set-project-modeline)
-
-(add-hook 'image-mode-hook #'+doom-modeline|set-media-modeline)
-(add-hook 'org-src-mode-hook #'+doom-modeline|set-special-modeline)
-(add-hook 'circe-mode-hook #'+doom-modeline|set-special-modeline)
diff --git a/modules/ui/doom-modeline/packages.el b/modules/ui/doom-modeline/packages.el
deleted file mode 100644
index 38ee48bd5..000000000
--- a/modules/ui/doom-modeline/packages.el
+++ /dev/null
@@ -1,20 +0,0 @@
-;; -*- no-byte-compile: t; -*-
-;;; ui/doom-modeline/packages.el
-
-;;; These are the invisible dependencies
-;; Required
-;;(require 'evil)
-;;(require 'projectile)
-;;(require 'all-the-icons)
-
-;; Optional
-;;(require 'flycheck)
-;;(require 'anzu)
-;;(require 'iedit)
-;;(require 'evil-multiedit)
-
-(package! eldoc-eval)
-(when (featurep! :feature evil)
- (package! evil-anzu))
-
-(package! shrink-path)
diff --git a/modules/ui/doom-quit/config.el b/modules/ui/doom-quit/config.el
index 8e9bc50fe..4f6d7bc0e 100644
--- a/modules/ui/doom-quit/config.el
+++ b/modules/ui/doom-quit/config.el
@@ -26,12 +26,11 @@
"A list of quit messages, picked randomly by `+doom-quit'. Taken from
http://doom.wikia.com/wiki/Quit_messages and elsewhere.")
-(defun +doom|quit (&rest _)
+(defun +doom-quit (&rest _)
(doom-quit-p
(format "%s Quit?"
(nth (random (length +doom-quit-messages))
+doom-quit-messages))))
;;
-(remove-hook 'kill-emacs-query-functions #'doom-quit-p)
-(add-hook 'kill-emacs-query-functions #'+doom|quit)
+(setq confirm-kill-emacs #'+doom-quit)
diff --git a/modules/ui/doom/config.el b/modules/ui/doom/config.el
index 0e73270c9..26082e7e8 100644
--- a/modules/ui/doom/config.el
+++ b/modules/ui/doom/config.el
@@ -1,101 +1,93 @@
;;; ui/doom/config.el -*- lexical-binding: t; -*-
+(defvar +doom-solaire-themes
+ '((doom-city-lights . t)
+ (doom-dracula . t)
+ (doom-molokai)
+ (doom-nord . t)
+ (doom-nord-light . t)
+ (doom-nova)
+ (doom-one . t)
+ (doom-one-light . t)
+ (doom-opera . t)
+ (doom-solarized-light)
+ (doom-spacegrey)
+ (doom-vibrant)
+ (doom-tomorrow-night))
+ "An alist of themes that support `solaire-mode'. If CDR is t, then use
+`solaire-mode-swap-bg'.")
+
+
+;;
+;; Packages
+
;;
(def-package! doom-themes
- :config
+ :defer t
+ :init
(unless doom-theme
- (setq doom-theme 'doom-one)
- (after! solaire-mode
- (add-hook 'doom-init-ui-hook #'solaire-mode-swap-bg t)))
-
- ;; Ensure `doom/reload-load-path' reloads common faces
- (defun +doom|reload-theme () (load "doom-themes-common.el" nil t))
- (add-hook 'doom-pre-reload-theme-hook #'+doom|reload-theme)
-
+ (setq doom-theme 'doom-one))
+ :config
;; improve integration w/ org-mode
- (add-hook 'doom-init-ui-hook #'doom-themes-org-config)
-
- ;; more Atom-esque file icons for neotree
- (add-hook 'doom-init-ui-hook #'doom-themes-neotree-config)
- (setq doom-neotree-enable-variable-pitch t
- doom-neotree-file-icons 'simple
- doom-neotree-line-spacing 2)
-
- ;; blink mode-line on errors
- ;; FIXME Breaks modeline
- ;; (add-hook 'doom-init-ui-hook #'doom-themes-visual-bell-config)
-
- (after! neotree
- (defun +doom|neotree-fix-popup ()
- "Ensure the fringe settings are maintained on popup restore."
- (neo-global--when-window
- (doom--neotree-no-fringes)))
- (add-hook 'doom-popup-mode-hook #'+doom|neotree-fix-popup)))
+ (add-hook 'doom-load-theme-hook #'doom-themes-org-config)
+ ;; more Atom-esque file icons for neotree/treemacs
+ (when (featurep! :ui neotree)
+ (add-hook 'doom-load-theme-hook #'doom-themes-neotree-config)
+ (setq doom-neotree-enable-variable-pitch t
+ doom-neotree-file-icons 'simple
+ doom-neotree-line-spacing 2))
+ (when (featurep! :ui treemacs)
+ (add-hook 'doom-load-theme-hook #'doom-themes-treemacs-config)
+ (setq doom-treemacs-enable-variable-pitch t)))
(def-package! solaire-mode
- :hook (after-change-major-mode . turn-on-solaire-mode)
- :hook (doom-popup-mode . turn-off-solaire-mode)
+ :defer t
+ :init
+ (defun +doom|solaire-mode-swap-bg-maybe ()
+ (when-let* ((rule (assq doom-theme +doom-solaire-themes)))
+ (require 'solaire-mode)
+ (if (cdr rule) (solaire-mode-swap-bg))))
+ (add-hook 'doom-load-theme-hook #'+doom|solaire-mode-swap-bg-maybe t)
:config
- (setq solaire-mode-real-buffer-fn #'doom-real-buffer-p)
+ ;; fringe can become unstyled when deleting or focusing frames
+ (add-hook 'focus-in-hook #'solaire-mode-reset)
+ ;; Prevent color glitches when reloading either DOOM or loading a new theme
+ (add-hook! :append '(doom-load-theme-hook doom-reload-hook)
+ #'solaire-mode-reset)
+ ;; org-capture takes an org buffer and narrows it. The result is erroneously
+ ;; considered an unreal buffer, so solaire-mode must be restored.
+ (add-hook 'org-capture-mode-hook #'turn-on-solaire-mode)
- ;; Prevent color glitches when reloading either DOOM or the theme
- (add-hook! '(doom-init-ui-hook doom-reload-hook) #'solaire-mode-reset)
+ ;; On Emacs 26+, when point is on the last line and solaire-mode is remapping
+ ;; the hl-line face, hl-line's highlight bleeds into the rest of the window
+ ;; after eob.
+ (when EMACS26+
+ (defun +doom--line-range ()
+ (cons (line-beginning-position)
+ (cond ((let ((eol (line-end-position)))
+ (and (= eol (point-max))
+ (/= eol (line-beginning-position))))
+ (1- (line-end-position)))
+ ((or (eobp)
+ (= (line-end-position 2) (point-max)))
+ (line-end-position))
+ ((line-beginning-position 2)))))
+ (setq hl-line-range-function #'+doom--line-range))
- (add-hook!
- (gist-mode twittering-mode mu4e-view-mode org-tree-slide-mode +regex-mode)
- #'solaire-mode))
+ ;; Because fringes can't be given a buffer-local face, they can look odd, so
+ ;; we remove them in the minibuffer and which-key popups (they serve no
+ ;; purpose there anyway).
+ (defun +doom|disable-fringes-in-minibuffer (&rest _)
+ (set-window-fringes (minibuffer-window) 0 0 nil))
+ (add-hook 'solaire-mode-hook #'+doom|disable-fringes-in-minibuffer)
+ (defun doom*no-fringes-in-which-key-buffer (&rest _)
+ (+doom|disable-fringes-in-minibuffer)
+ (set-window-fringes (get-buffer-window which-key--buffer) 0 0 nil))
+ (advice-add 'which-key--show-buffer-side-window :after #'doom*no-fringes-in-which-key-buffer)
-(after! hideshow
- (defface +doom-folded-face
- `((((background dark))
- (:inherit font-lock-comment-face :background ,(doom-color 'base0)))
- (((background light))
- (:inherit font-lock-comment-face :background ,(doom-color 'base3))))
- "Face to hightlight `hideshow' overlays."
- :group 'doom)
+ (add-hook! '(minibuffer-setup-hook window-configuration-change-hook)
+ #'+doom|disable-fringes-in-minibuffer)
- ;; Nicer code-folding overlays (with fringe indicators)
- (setq hs-set-up-overlay
- (lambda (ov)
- (when (eq 'code (overlay-get ov 'hs))
- (when (featurep 'vimish-fold)
- (overlay-put
- ov 'before-string
- (propertize "…" 'display
- (list vimish-fold-indication-mode
- 'empty-line
- 'vimish-fold-fringe))))
- (overlay-put
- ov 'display (propertize " [...] " 'face '+doom-folded-face))))))
-
-
-;; NOTE Adjust these bitmaps if you change `doom-fringe-size'
-(after! flycheck
- ;; because git-gutter is in the left fringe
- (setq flycheck-indication-mode 'right-fringe)
- ;; A non-descript, left-pointing arrow
- (fringe-helper-define 'flycheck-fringe-bitmap-double-arrow 'center
- "...X...."
- "..XX...."
- ".XXX...."
- "XXXX...."
- ".XXX...."
- "..XX...."
- "...X...."))
-
-;; subtle diff indicators in the fringe
-(after! git-gutter-fringe
- ;; places the git gutter outside the margins.
- (setq-default fringes-outside-margins t)
- ;; thin fringe bitmaps
- (fringe-helper-define 'git-gutter-fr:added '(center repeated)
- "XXX.....")
- (fringe-helper-define 'git-gutter-fr:modified '(center repeated)
- "XXX.....")
- (fringe-helper-define 'git-gutter-fr:deleted 'bottom
- "X......."
- "XX......"
- "XXX....."
- "XXXX...."))
+ (solaire-global-mode +1))
diff --git a/modules/ui/evil-goggles/README.org b/modules/ui/evil-goggles/README.org
deleted file mode 100644
index a358d75be..000000000
--- a/modules/ui/evil-goggles/README.org
+++ /dev/null
@@ -1,16 +0,0 @@
-#+TITLE: :ui evil-goggles
-
-This module uses ~evil goggles~ to displays visual hints when editing with evil.
-
-* Table of Contents :TOC:
-- [[#install][Install]]
-- [[#configure][Configure]]
-
-* Install
-This module requires:
-
-+ ~evil~ (inherently tied to evil mode)
-
-* Configure
-By default, ~evil-goggles~ will be enabled by default and requires no additional configuration.
-
diff --git a/modules/ui/evil-goggles/autoload.el b/modules/ui/evil-goggles/autoload.el
deleted file mode 100644
index 544419859..000000000
--- a/modules/ui/evil-goggles/autoload.el
+++ /dev/null
@@ -1,9 +0,0 @@
-;;; feature/ui/evil-goggles/autoload.el -*- lexical-binding: t; -*-
-
-;;;###autoload
-(defun +evil-goggles/toggle ()
- "Toggle evil goggles mode."
- (interactive)
- (if evil-goggles-mode
- (evil-goggles-mode -1)
- (evil-goggles-mode +1)))
diff --git a/modules/ui/evil-goggles/config.el b/modules/ui/evil-goggles/config.el
deleted file mode 100644
index 2a1b7a1eb..000000000
--- a/modules/ui/evil-goggles/config.el
+++ /dev/null
@@ -1,8 +0,0 @@
-;;; ui/evil-goggles/config.el -*- lexical-binding: t; -*-
-
-(def-package! evil-goggles
- :when (featurep! :feature evil)
- :hook (doom-post-init . evil-goggles-mode)
- :init
- (setq evil-goggles-duration 0.1
- evil-goggles-enable-delete nil))
diff --git a/modules/ui/fill-column/config.el b/modules/ui/fill-column/config.el
new file mode 100644
index 000000000..3a2629e4f
--- /dev/null
+++ b/modules/ui/fill-column/config.el
@@ -0,0 +1,4 @@
+;;; ui/fill-column/config.el -*- lexical-binding: t; -*-
+
+(def-package! hl-fill-column
+ :hook ((text-mode prog-mode conf-mode) . hl-fill-column-mode))
diff --git a/modules/ui/fill-column/packages.el b/modules/ui/fill-column/packages.el
new file mode 100644
index 000000000..0117dad31
--- /dev/null
+++ b/modules/ui/fill-column/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; ui/fill-column/packages.el
+
+(package! hl-fill-column)
diff --git a/modules/ui/indent-guides/config.el b/modules/ui/indent-guides/config.el
new file mode 100644
index 000000000..ec427dde4
--- /dev/null
+++ b/modules/ui/indent-guides/config.el
@@ -0,0 +1,20 @@
+;;; ui/indent-guides/config.el -*- lexical-binding: t; -*-
+
+(def-package! highlight-indent-guides
+ :hook ((prog-mode text-mode conf-mode) . highlight-indent-guides-mode)
+ :init
+ (setq highlight-indent-guides-method 'character
+ highlight-indent-guides-responsive 'top)
+ :config
+ ;; Don't display first level of indentation
+ (defun +indent-guides-for-all-but-first-column (level responsive display)
+ (unless (< level 1)
+ (highlight-indent-guides--highlighter-default level responsive display)))
+ (setq highlight-indent-guides-highlighter-function #'+indent-guides-for-all-but-first-column)
+
+ (defun +indent-guides|disable-maybe ()
+ (when highlight-indent-guides-mode
+ (highlight-indent-guides-mode -1)))
+ ;; `highlight-indent-guides' breaks in `visual-line-mode'
+ (add-hook 'visual-line-mode-hook #'+indent-guides|disable-maybe)
+ (add-hook 'org-indent-mode-hook #'+indent-guides|disable-maybe))
diff --git a/modules/ui/indent-guides/packages.el b/modules/ui/indent-guides/packages.el
new file mode 100644
index 000000000..210c00e9d
--- /dev/null
+++ b/modules/ui/indent-guides/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; ui/indent-guides/packages.el
+
+(package! highlight-indent-guides)
diff --git a/modules/ui/modeline/README.org b/modules/ui/modeline/README.org
new file mode 100644
index 000000000..2b0c0f76b
--- /dev/null
+++ b/modules/ui/modeline/README.org
@@ -0,0 +1,116 @@
+#+TITLE: ui/modeline
+#+DATE: July 29, 2018
+#+SINCE: v2.0.9
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC_2:noexport:
+- [[Description][Description]]
+ - [[Module Flags][Module Flags]]
+ - [[Plugins][Plugins]]
+- [[Prerequisites][Prerequisites]]
+- [[Usage][Usage]]
+ - [[Hiding the modeline][Hiding the modeline]]
+ - [[Switching the modeline and header line][Switching the modeline and header line]]
+- [[Configuration][Configuration]]
+ - [[Changing the default modeline][Changing the default modeline]]
+ - [[Activating a format][Activating a format]]
+ - [[Defining a modeline format][Defining a modeline format]]
+ - [[Defining a modeline segment][Defining a modeline segment]]
+ - [[Extracting Doom's modeline into your config][Extracting Doom's modeline into your config]]
+- [[Troubleshooting][Troubleshooting]]
+ - [[Where are my minor modes?][Where are my minor modes?]]
+ - [[Icons in my modeline look strange][Icons in my modeline look strange]]
+- [[Appendix][Appendix]]
+ - [[Commands][Commands]]
+ - [[Autodefs][Autodefs]]
+ - [[Functions][Functions]]
+ - [[Variables][Variables]]
+ - [[Faces][Faces]]
+ - [[Modeline segments][Modeline segments]]
+
+* Description
+This module provides an Atom-inspired, minimalistic modeline for Doom Emacs, as
+well as an API for building your own.
+
++ A match count panel (for ~evil-search~, ~iedit~ and ~evil-substitute~)
++ An indicator for recording a macro
++ Local python/ruby version in the major-mode
++ A customizable mode-line height (see ~+doom-modeline-height~)
++ An error/warning count segment for flycheck
+
+[[/../screenshots/ml.png]]
+[[/../screenshots/ml-search.png]]
+[[/../screenshots/ml-subst.png]]
+[[/../screenshots/ml-macro.png]]
+[[/../screenshots/ml-version.png]]
+[[/../screenshots/ml-errors.png]]
+
+** Module Flags
+This module provides no flags.
+
+** Plugins
++ [[https://github.com/syohex/emacs-anzu][anzu]]
++ [[https://github.com/syohex/emacs-evil-anzu][evil-anzu]]
++ [[https://gitlab.com/bennya/shrink-path.el][shrink-path]]
+
+* Prerequisites
+This module has no prerequisites.
+
+* Usage
+** TODO Hiding the modeline
+
+** TODO Switching the modeline and header line
+
+* Configuration
+** TODO Changing the default modeline
+
+** TODO Activating a format
+
+** TODO Defining a modeline format
+
+** TODO Defining a modeline segment
+
+** TODO Extracting Doom's modeline into your config
+
+* Troubleshooting
+** Where are my minor modes?
+I rarely need to know what minor modes are active, so I removed them. ~M-x
+doom/what-minor-mode~ was written to substitute for it.
+
+** TODO Icons in my modeline look strange
+
+* Appendix
+** Commands
+This module exposes no public commands.
+** Autodefs
++ ~def-modeline-format! NAME LEFT &optional RIGHT~
++ ~def-modeline-segment! NAME &rest REST~
++ ~set-modeline! NAME &optional DEFAULT~
+** Functions
++ +modeline-file-path
+** Variables
++ +modeline-width
++ +modeline-height
++ +modeline-buffer-path-function
++ +modeline-format-left
++ +modeline-format-rigth
+** Faces
++ doom-modeline-buffer-path
++ doom-modeline-buffer-file
++ doom-modeline-buffer-modified
++ doom-modeline-buffer-major-mode
++ doom-modeline-highlight
++ doom-modeline-panel
++ doom-modeline-info
++ doom-modeline-warning
++ doom-modeline-urgent
++ doom-modeline-bar
+** Modeline segments
++ +modeline-buffer-state
++ +modeline-buffer-id
++ +modeline-buffer-directory
++ +modeline-vcs
++ +modeline-encoding
++ +modeline-major-mode
++ +modeline-matches
++ +modeline-selection-info
diff --git a/modules/ui/modeline/autoload.el b/modules/ui/modeline/autoload.el
new file mode 100644
index 000000000..3c7478a65
--- /dev/null
+++ b/modules/ui/modeline/autoload.el
@@ -0,0 +1,45 @@
+;;; ui/modeline/autoload/modeline.el -*- lexical-binding: t; -*-
+
+;;;###autodef
+(defalias 'def-modeline-format! #'doom-modeline-def-modeline)
+
+;;;###autodef
+(defalias 'def-modeline-segment! #'doom-modeline-def-segment)
+
+;;;###autodef
+(defalias 'set-modeline! #'doom-modeline-set-modeline)
+
+
+(defvar +modeline--old-bar-height nil)
+;;;###autoload
+(defun +modeline|resize-for-big-font ()
+ "Adjust the modeline's height when `doom-big-font-mode' is enabled. This was
+made to be added to `doom-big-font-mode-hook'."
+ (unless +modeline--old-bar-height
+ (setq +modeline--old-bar-height doom-modeline-height))
+ (let ((default-height +modeline--old-bar-height))
+ (if doom-big-font-mode
+ (let* ((font-size (font-get doom-font :size))
+ (big-size (font-get doom-big-font :size))
+ (ratio (/ (float big-size) font-size)))
+ (setq doom-modeline-height (ceiling (* default-height ratio 0.75))))
+ (setq doom-modeline-height default-height))
+ ;; already has a variable watcher in Emacs 26+
+ (unless EMACS26+ (doom-modeline-refresh-bars))))
+
+;;;###autoload
+(defun +modeline|update-env-in-all-windows (&rest _)
+ "Update version strings in all buffers."
+ (dolist (window (window-list))
+ (with-selected-window window
+ (doom-modeline-update-env)
+ (force-mode-line-update))))
+
+;;;###autoload
+(defun +modeline|clear-env-in-all-windows (&rest _)
+ "Blank out version strings in all buffers."
+ (dolist (buffer (buffer-list))
+ (with-current-buffer buffer
+ (setq doom-modeline-env--version
+ (bound-and-true-p doom-modeline-load-string))))
+ (force-mode-line-update t))
diff --git a/modules/ui/modeline/config.el b/modules/ui/modeline/config.el
new file mode 100644
index 000000000..e66bdeebd
--- /dev/null
+++ b/modules/ui/modeline/config.el
@@ -0,0 +1,111 @@
+;;; ui/modeline/config.el -*- lexical-binding: t; -*-
+
+;; TODO Add themes (default, minimal, spacemacs, etc)
+
+(def-package! doom-modeline
+ :hook (after-init . doom-modeline-mode)
+ :init
+ (unless after-init-time
+ ;; prevent flash of unstyled modeline at startup
+ (setq-default mode-line-format nil))
+ ;; We display project info in the modeline ourselves
+ (setq projectile-dynamic-mode-line nil)
+ ;; Set these early so they don't trigger variable watchers
+ (setq doom-modeline-bar-width 3
+ doom-modeline-github nil
+ doom-modeline-mu4e nil
+ doom-modeline-persp-name nil
+ doom-modeline-minor-modes nil
+ doom-modeline-major-mode-icon nil
+ doom-modeline-buffer-file-name-style 'relative-from-project)
+
+ ;; Fix modeline icons in daemon-spawned graphical frames. We have our own
+ ;; mechanism for disabling all-the-icons, so we don't need doom-modeline to do
+ ;; it for us. However, this may cause unwanted padding in the modeline in
+ ;; daemon-spawned terminal frames. If it bothers you, you may prefer
+ ;; `doom-modeline-icon' set to `nil'.
+ (when (daemonp)
+ (setq doom-modeline-icon t))
+ :config
+ (add-hook 'doom-modeline-mode-hook #'size-indication-mode) ; filesize in modeline
+ (add-hook 'doom-modeline-mode-hook #'column-number-mode) ; cursor column in modeline
+
+ (add-hook 'doom-big-font-mode-hook #'+modeline|resize-for-big-font)
+ (add-hook 'doom-load-theme-hook #'doom-modeline-refresh-bars)
+
+ (add-hook '+doom-dashboard-mode-hook #'doom-modeline-set-project-modeline)
+
+ ;; Don't eager-load project.el. Doom only uses projectile anyway, for now.
+ (defun +modeline*project-root ()
+ (or doom-modeline-project-root
+ (setq doom-modeline-project-root
+ (file-local-name
+ (or (and (featurep 'projectile) (ignore-errors (projectile-project-root)))
+ default-directory)))))
+ (advice-add #'doom-modeline-project-root :override #'+modeline*project-root)
+
+ ;; Magit -- modeline only where it's useful
+ (defun +modeline|hide-in-non-status-buffer ()
+ (if (eq major-mode 'magit-status-mode)
+ (doom-modeline-set-project-modeline)
+ (hide-mode-line-mode)))
+ (add-hook 'magit-mode-hook #'+modeline|hide-in-non-status-buffer)
+
+ ;; Show indentation style in modeline. I'm not using
+ ;; `doom-modeline-def-segment' to prevent eager macro expansion from loading
+ ;; the package too soon.
+ (defun +modeline-indent-segment ()
+ "indent modeline segment"
+ (propertize (format "%s%d"
+ (if indent-tabs-mode "⭾" "␣")
+ tab-width)
+ 'face (if (doom-modeline--active) 'mode-line 'mode-line-inactive)
+ 'mouse-face 'mode-line-highlight
+ 'help-echo
+ (let ((subsegs
+ (list (format "Indentation style: %s (%d wide)"
+ (if indent-tabs-mode "tabs" "spaces")
+ tab-width)
+ (cond ((eq doom-inhibit-indent-detection 'editorconfig)
+ (propertize "✓ Editorconfig applied" 'face 'success))
+ (doom-inhibit-indent-detection
+ (propertize "✘ Indentation auto-detection disabled" 'face 'warning))
+ ((bound-and-true-p dtrt-indent-original-indent)
+ (propertize (format "✓ Indentation auto-detected (original: %s)"
+ dtrt-indent-original-indent)
+ 'face 'success)))
+ (when (bound-and-true-p ws-butler-mode)
+ (propertize "✓ ws-butler active (whitespace cleanup on save)"
+ 'face 'success)))))
+ (string-join (delq nil subsegs) " "))))
+ (add-to-list 'doom-modeline-fn-alist '(indent . +modeline-indent-segment))
+
+ ;; Remove unused segments & extra padding
+ (doom-modeline-def-modeline 'main
+ '(bar window-number matches buffer-info remote-host buffer-position selection-info)
+ '(misc-info persp-name irc mu4e github debug indent input-method buffer-encoding lsp major-mode process vcs checker))
+
+ (doom-modeline-def-modeline 'special
+ '(bar window-number matches buffer-info-simple buffer-position selection-info)
+ '(misc-info persp-name debug input-method irc-buffers buffer-encoding lsp major-mode process checker))
+
+ (doom-modeline-def-modeline 'project
+ '(bar window-number buffer-default-directory)
+ '(misc-info mu4e github debug fancy-battery " " major-mode process))
+
+ ;; Some functions modify the buffer, causing the modeline to show a false
+ ;; modified state, so we try to force them to behave.
+ (defun +modeline*inhibit-modification-hooks (orig-fn &rest args)
+ (with-silent-modifications (apply orig-fn args)))
+ (advice-add #'ws-butler-after-save :around #'+modeline*inhibit-modification-hooks))
+
+
+;;
+;; Extensions
+
+(def-package! anzu
+ :after-call isearch-mode)
+
+(def-package! evil-anzu
+ :when (featurep! :feature evil)
+ :after-call (evil-ex-start-search evil-ex-start-word-search))
diff --git a/modules/ui/modeline/packages.el b/modules/ui/modeline/packages.el
new file mode 100644
index 000000000..5933333f4
--- /dev/null
+++ b/modules/ui/modeline/packages.el
@@ -0,0 +1,7 @@
+;; -*- no-byte-compile: t; -*-
+;;; ui/modeline/packages.el
+
+(package! doom-modeline)
+(package! anzu)
+(when (featurep! :feature evil)
+ (package! evil-anzu))
diff --git a/modules/ui/nav-flash/autoload.el b/modules/ui/nav-flash/autoload.el
index 53d7bfb60..1e8f2a03d 100644
--- a/modules/ui/nav-flash/autoload.el
+++ b/modules/ui/nav-flash/autoload.el
@@ -1,22 +1,40 @@
;;; ui/nav-flash/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
-(defun +doom*blink-cursor-maybe (orig-fn &rest args)
- "Blink current line if the window has moved."
- (let ((point (save-excursion (goto-char (window-start))
- (point-marker))))
- (apply orig-fn args)
- (unless (or (derived-mode-p 'term-mode)
- (equal point
- (save-excursion (goto-char (window-start))
- (point-marker))))
- (+doom/blink-cursor))))
-
-;;;###autoload
-(defun +doom/blink-cursor (&rest _)
- "Blink current line using `nav-flash'."
- (interactive)
+(defun +nav-flash-blink-cursor (&rest _)
+ "Blinks the current line in the current window, to make it clear where the
+cursor has landed (typically after a large motion, like switching windows or
+jumping to another part of the file)."
(unless (minibufferp)
(nav-flash-show)
;; only show in the current window
(overlay-put compilation-highlight-overlay 'window (selected-window))))
+
+;;;###autoload
+(defun +nav-flash-blink-cursor-maybe (&rest _)
+ "Like `+nav-flash-blink-cursor', but no-ops if in special-mode or term-mode,
+or triggered from one of `+nav-flash-exclude-commands'."
+ (unless (or (derived-mode-p 'special-mode 'term-mode)
+ (memq this-command +nav-flash-exclude-commands))
+ (+nav-flash-blink-cursor)))
+
+;;;###autoload
+(defun +nav-flash|delayed-blink-cursor (&rest _)
+ "Like `+nav-flash-blink-cursor', but links after a tiny pause, in case it
+isn't clear at run-time if the point will be in the correct window/buffer (like
+for `org-follow-link-hook')."
+ (run-at-time 0.1 nil #'+nav-flash|blink-cursor))
+
+;;;###autoload
+(defalias '+nav-flash|blink-cursor #'+nav-flash-blink-cursor)
+;;;###autoload
+(defalias '+nav-flash|blink-cursor-maybe #'+nav-flash-blink-cursor-maybe)
+
+;;;###autoload
+(defalias '+nav-flash*blink-cursor #'+nav-flash-blink-cursor-maybe)
+
+;;;###autoload
+(defun +nav-flash/blink-cursor (&rest _)
+ "Blink current line using `nav-flash'."
+ (interactive)
+ (+nav-flash-blink-cursor-maybe))
diff --git a/modules/ui/nav-flash/config.el b/modules/ui/nav-flash/config.el
index cf2ce8be0..e84837bbb 100644
--- a/modules/ui/nav-flash/config.el
+++ b/modules/ui/nav-flash/config.el
@@ -1,18 +1,29 @@
;;; ui/nav-flash/config.el -*- lexical-binding: t; -*-
+(defvar +nav-flash-exclude-commands
+ '(mouse-set-point evil-mouse-drag-region
+ +org/dwim-at-point org-find-file org-find-file-at-mouse)
+ "A list of commands that should not trigger nav-flash.")
+
(def-package! nav-flash
- :commands nav-flash-show
+ :defer t
:init
- ;; NOTE In :feature jump `recenter' is hooked to a bunch of jumping commands,
- ;; which will trigger nav-flash.
- (advice-add #'windmove-do-window-select :around #'+doom*blink-cursor-maybe)
- (advice-add #'recenter :around #'+doom*blink-cursor-maybe)
+ ;; NOTE In :feature lookup `recenter' is hooked to a bunch of jumping
+ ;; commands, which will trigger nav-flash.
+ (add-hook!
+ '(doom-switch-window-hook
+ imenu-after-jump-hook evil-jumps-post-jump-hook
+ counsel-grep-post-action-hook dumb-jump-after-jump-hook)
+ #'+nav-flash|blink-cursor-maybe)
- (after! evil
- (advice-add #'evil--jumps-jump :after #'+doom/blink-cursor)
+ ;; `org'
+ (add-hook 'org-follow-link-hook #'+nav-flash|delayed-blink-cursor)
- (advice-add #'evil-window-top :after #'+doom/blink-cursor)
- (advice-add #'evil-window-middle :after #'+doom/blink-cursor)
- (advice-add #'evil-window-bottom :after #'+doom/blink-cursor)))
+ ;; `saveplace'
+ (advice-add #'save-place-find-file-hook :after #'+nav-flash*blink-cursor)
+ ;; `evil'
+ (advice-add #'evil-window-top :after #'+nav-flash*blink-cursor)
+ (advice-add #'evil-window-middle :after #'+nav-flash*blink-cursor)
+ (advice-add #'evil-window-bottom :after #'+nav-flash*blink-cursor))
diff --git a/modules/tools/neotree/README.org b/modules/ui/neotree/README.org
similarity index 100%
rename from modules/tools/neotree/README.org
rename to modules/ui/neotree/README.org
diff --git a/modules/tools/neotree/autoload.el b/modules/ui/neotree/autoload.el
similarity index 69%
rename from modules/tools/neotree/autoload.el
rename to modules/ui/neotree/autoload.el
index c24ebe1ff..cf53d5ac5 100644
--- a/modules/tools/neotree/autoload.el
+++ b/modules/ui/neotree/autoload.el
@@ -1,11 +1,26 @@
-;;; tools/neotree/autoload.el -*- lexical-binding: t; -*-
+;;; ui/neotree/autoload.el -*- lexical-binding: t; -*-
+
+;; `neotree-show' and `neotree-find' don't respect the current project, and open
+;; neotree in `default-directory'. `+neotree/open' and `neotree/find-this-file'
+;; will ensure the neotree pane is always rooted in the project root.
;;;###autoload
-(defun +neotree/toggle ()
- "Toggle the neotree window."
+(defun +neotree/open ()
+ "Open the neotree window in the current project."
+ (interactive)
+ (require 'neotree)
+ (if (neo-global--window-exists-p)
+ (neotree-hide)
+ (neotree-dir (or (doom-project-root)
+ default-directory))))
+
+;;;###autoload
+(defun +neotree/find-this-file ()
+ "Open the neotree window in the current project, and find the current file."
(interactive)
(let ((path buffer-file-name)
- (project-root (doom-project-root)))
+ (project-root (or (doom-project-root)
+ default-directory)))
(require 'neotree)
(cond ((and (neo-global--window-exists-p)
(get-buffer-window neo-buffer-name t))
diff --git a/modules/ui/neotree/config.el b/modules/ui/neotree/config.el
new file mode 100644
index 000000000..07422fdab
--- /dev/null
+++ b/modules/ui/neotree/config.el
@@ -0,0 +1,57 @@
+;;; ui/neotree/config.el -*- lexical-binding: t; -*-
+
+(def-package! neotree
+ :commands (neotree-show
+ neotree-hide
+ neotree-toggle
+ neotree-dir
+ neotree-find
+ neo-global--with-buffer
+ neo-global--window-exists-p)
+ :config
+ (setq neo-create-file-auto-open nil
+ neo-auto-indent-point nil
+ neo-autorefresh nil
+ neo-mode-line-type 'none
+ neo-window-width 28
+ neo-show-updir-line nil
+ neo-theme 'nerd ; fallback
+ neo-banner-message nil
+ neo-confirm-create-file #'off-p
+ neo-confirm-create-directory #'off-p
+ neo-show-hidden-files nil
+ neo-keymap-style 'concise
+ neo-show-hidden-files t
+ neo-hidden-regexp-list
+ '(;; vcs folders
+ "^\\.\\(?:git\\|hg\\|svn\\)$"
+ ;; compiled files
+ "\\.\\(?:pyc\\|o\\|elc\\|lock\\|css.map\\|class\\)$"
+ ;; generated files, caches or local pkgs
+ "^\\(?:node_modules\\|vendor\\|.\\(project\\|cask\\|yardoc\\|sass-cache\\)\\)$"
+ ;; org-mode folders
+ "^\\.\\(?:sync\\|export\\|attach\\)$"
+ ;; temp files
+ "~$"
+ "^#.*#$"))
+
+ (set-popup-rule! "^ ?\\*NeoTree"
+ :side neo-window-position :size neo-window-width
+ :quit 'current :select t)
+
+ (after! winner
+ (add-to-list 'winner-boring-buffers neo-buffer-name))
+
+ ;; The cursor always sits at bol. `+neotree*fix-cursor' and
+ ;; `+neotree*indent-cursor' change that behavior, so that the cursor is always
+ ;; on the first non-blank character on the line, in the neo buffer.
+ (defun +neotree*fix-cursor (&rest _)
+ (with-current-buffer neo-global--buffer
+ (+neotree*indent-cursor)))
+ (add-hook 'neo-enter-hook #'+neotree*fix-cursor)
+
+ (defun +neotree*indent-cursor (&rest _)
+ (beginning-of-line)
+ (skip-chars-forward " \t\r"))
+ (advice-add #'neotree-next-line :after #'+neotree*indent-cursor)
+ (advice-add #'neotree-previous-line :after #'+neotree*indent-cursor))
diff --git a/modules/tools/neotree/packages.el b/modules/ui/neotree/packages.el
similarity index 62%
rename from modules/tools/neotree/packages.el
rename to modules/ui/neotree/packages.el
index 86ec08eaf..47aae4652 100644
--- a/modules/tools/neotree/packages.el
+++ b/modules/ui/neotree/packages.el
@@ -1,4 +1,4 @@
;; -*- no-byte-compile: t; -*-
-;;; tools/neotree/packages.el
+;;; ui/neotree/packages.el
(package! neotree)
diff --git a/modules/ui/ophints/README.org b/modules/ui/ophints/README.org
new file mode 100644
index 000000000..49868b9b0
--- /dev/null
+++ b/modules/ui/ophints/README.org
@@ -0,0 +1,23 @@
+#+TITLE: ui/ophints
+#+DATE: June 4, 2017
+#+SINCE: v2.0
+#+STARTUP: inlineimages
+
+* Table of Contents :TOC_3:noexport:
+- [[#description][Description]]
+ - [[#module-flags][Module Flags]]
+ - [[#plugins][Plugins]]
+
+* Description
+This module provides op-hints (operation hinting), i.e. visual feedback for
+certain operations. It highlights regions of text that the last operation (like
+yank) acted on.
+
+Uses ~evil-goggles~ for evil users and ~volatile-highlights~ otherwise.
+
+** Module Flags
+This module provides no flags.
+
+** Plugins
++ [[https://github.com/edkolev/evil-goggles/][evil-goggles]]*
++ [[https://github.com/k-talo/volatile-highlights.el][volatile-highlights]]*
diff --git a/modules/ui/ophints/config.el b/modules/ui/ophints/config.el
new file mode 100644
index 000000000..1382822d5
--- /dev/null
+++ b/modules/ui/ophints/config.el
@@ -0,0 +1,24 @@
+;;; ui/ophints/config.el -*- lexical-binding: t; -*-
+
+(def-package! evil-goggles
+ :when (featurep! :feature evil)
+ :after-call pre-command-hook
+ :init
+ (setq evil-goggles-duration 0.1
+ evil-goggles-pulse nil ; too slow
+ ;; evil-goggles provides a good indicator of what has been affected.
+ ;; delete/change is obvious, so I'd rather disable it for these.
+ evil-goggles-enable-delete nil
+ evil-goggles-enable-change nil)
+ :config
+ (evil-goggles-mode +1))
+
+
+(def-package! volatile-highlights
+ :unless (featurep! :feature evil)
+ :after-call pre-command-hook
+ :config
+ (volatile-highlights-mode)
+ (after! undo-tree
+ (vhl/define-extension 'undo-tree 'undo-tree-yank 'undo-tree-move)
+ (vhl/install-extension 'undo-tree)))
diff --git a/modules/ui/ophints/packages.el b/modules/ui/ophints/packages.el
new file mode 100644
index 000000000..44502c727
--- /dev/null
+++ b/modules/ui/ophints/packages.el
@@ -0,0 +1,6 @@
+;; -*- no-byte-compile: t; -*-
+;;; ui/ophints/packages.el
+
+(if (featurep! :feature evil)
+ (package! evil-goggles)
+ (package! volatile-highlights))
diff --git a/modules/ui/popup/+hacks.el b/modules/ui/popup/+hacks.el
new file mode 100644
index 000000000..8b05cff9a
--- /dev/null
+++ b/modules/ui/popup/+hacks.el
@@ -0,0 +1,363 @@
+;;; ui/popup/+hacks.el -*- lexical-binding: t; -*-
+
+;; What follows are all the hacks needed to get various parts of Emacs and other
+;; plugins to cooperate with the popup management system. Essentially, it comes
+;; down to:
+;;
+;; 1. Making plugins that control their own window environment less greedy (e.g.
+;; org agenda, which tries to reconfigure the entire frame (by deleting all
+;; other windows) just to pop up one tiny window).
+;; 2. Forcing plugins to use `display-buffer' and `pop-to-buffer' instead of
+;; `switch-to-buffer' (which is unaffected by `display-buffer-alist', which
+;; this module heavily relies on).
+;; 3. Closing popups (temporarily) before functions that are highly destructive
+;; to the illusion of popup control get run (with the use of the
+;; `save-popups!' macro).
+;;
+;; Keep in mind, all this black magic may break in future updates, and will need
+;; to be watched carefully for corner cases. Also, once this file is loaded, its
+;; changes are irreversible without restarting Emacs! I don't like it either,
+;; but I will address this over time.
+;;
+;; Hacks should be kept in alphabetical order, named after the feature they
+;; modify, and should follow a ;; `package-name' header line.
+
+;;
+;; Core functions
+
+;; Don't try to resize popup windows
+(advice-add #'balance-windows :around #'+popup*save)
+
+
+;;
+;; External functions
+
+;; `buff-menu'
+(define-key Buffer-menu-mode-map (kbd "RET") #'Buffer-menu-other-window)
+
+
+;; `company'
+(progn
+ (defun +popup*dont-select-me (orig-fn &rest args)
+ (let ((+popup--inhibit-select t))
+ (apply orig-fn args)))
+ (advice-add #'company-show-doc-buffer :around #'+popup*dont-select-me))
+
+
+;; `eshell'
+(progn
+ (setq eshell-destroy-buffer-when-process-dies t)
+
+ ;; When eshell runs a visual command (see `eshell-visual-commands'), it spawns
+ ;; a term buffer to run it in, but where it spawns it is the problem...
+ (defun +popup*eshell-undedicate-popup (orig-fn &rest args)
+ "Force spawned term buffer to share with the eshell popup (if necessary)."
+ (when (+popup-window-p)
+ (set-window-dedicated-p nil nil)
+ (add-transient-hook! #'eshell-query-kill-processes :after
+ (set-window-dedicated-p nil t)))
+ (apply orig-fn args))
+ (advice-add #'eshell-exec-visual :around #'+popup*eshell-undedicate-popup))
+
+
+;; `evil'
+(progn
+ (defun +popup*evil-command-window (hist cmd-key execute-fn)
+ "Monkey patch the evil command window to use `pop-to-buffer' instead of
+`switch-to-buffer', allowing the popup manager to handle it."
+ (when (eq major-mode 'evil-command-window-mode)
+ (user-error "Cannot recursively open command line window"))
+ (dolist (win (window-list))
+ (when (equal (buffer-name (window-buffer win))
+ "*Command Line*")
+ (kill-buffer (window-buffer win))
+ (delete-window win)))
+ (setq evil-command-window-current-buffer (current-buffer))
+ (ignore-errors (kill-buffer "*Command Line*"))
+ (with-current-buffer (pop-to-buffer "*Command Line*")
+ (setq-local evil-command-window-execute-fn execute-fn)
+ (setq-local evil-command-window-cmd-key cmd-key)
+ (evil-command-window-mode)
+ (evil-command-window-insert-commands hist)))
+
+ (defun +popup*evil-command-window-execute ()
+ "Execute the command under the cursor in the appropriate buffer, rather than
+the command buffer."
+ (interactive)
+ (let ((result (buffer-substring (line-beginning-position)
+ (line-end-position)))
+ (execute-fn evil-command-window-execute-fn)
+ (execute-window (get-buffer-window evil-command-window-current-buffer))
+ (popup (selected-window)))
+ (if execute-window
+ (select-window execute-window)
+ (user-error "Originating buffer is no longer active"))
+ ;; (kill-buffer "*Command Line*")
+ (delete-window popup)
+ (funcall execute-fn result)
+ (setq evil-command-window-current-buffer nil)))
+
+ ;; Make evil-mode cooperate with popups
+ (advice-add #'evil-command-window :override #'+popup*evil-command-window)
+ (advice-add #'evil-command-window-execute :override #'+popup*evil-command-window-execute)
+
+ ;; Don't mess with popups
+ (advice-add #'+evil--window-swap :around #'+popup*save)
+ (advice-add #'evil-window-move-very-bottom :around #'+popup*save)
+ (advice-add #'evil-window-move-very-top :around #'+popup*save)
+ (advice-add #'evil-window-move-far-left :around #'+popup*save)
+ (advice-add #'evil-window-move-far-right :around #'+popup*save))
+
+
+;; `help-mode'
+(after! help-mode
+ (defun doom--switch-from-popup (location)
+ (let (origin enable-local-variables)
+ (save-popups!
+ (switch-to-buffer (car location) nil t)
+ (if (not (cdr location))
+ (message "Unable to find location in file")
+ (goto-char (cdr location))
+ (recenter)
+ (setq origin (selected-window))))
+ (select-window origin)))
+
+ ;; Help buffers use `pop-to-window' to decide where to open followed links,
+ ;; which can be unpredictable. It should *only* replace the original buffer we
+ ;; opened the popup from. To fix this these three button types need to be
+ ;; redefined to set aside the popup before following a link.
+ (define-button-type 'help-function-def
+ :supertype 'help-xref
+ 'help-function
+ (lambda (fun file)
+ (require 'find-func)
+ (when (eq file 'C-source)
+ (setq file (help-C-file-name (indirect-function fun) 'fun)))
+ (doom--switch-from-popup (find-function-search-for-symbol fun nil file))))
+
+ (define-button-type 'help-variable-def
+ :supertype 'help-xref
+ 'help-function
+ (lambda (var &optional file)
+ (when (eq file 'C-source)
+ (setq file (help-C-file-name var 'var)))
+ (doom--switch-from-popup (find-variable-noselect var file))))
+
+ (define-button-type 'help-face-def
+ :supertype 'help-xref
+ 'help-function
+ (lambda (fun file)
+ (require 'find-func)
+ (doom--switch-from-popup (find-function-search-for-symbol fun 'defface file)))))
+
+
+;; `helpful'
+(progn
+ (defun +popup*helpful-open-in-origin-window (button)
+ "Open links in non-popup, originating window rather than helpful's window."
+ (let ((path (substring-no-properties (button-get button 'path)))
+ enable-local-variables
+ origin)
+ (save-popups!
+ (find-file path)
+ (-when-let (pos (get-text-property button 'position
+ (marker-buffer button)))
+ (goto-char pos))
+ (setq origin (selected-window))
+ (recenter))
+ (select-window origin)))
+ (advice-add #'helpful--navigate :override #'+popup*helpful-open-in-origin-window))
+
+
+;; `helm'
+(when (featurep! :completion helm)
+ (setq helm-default-display-buffer-functions '(+popup-display-buffer-stacked-side-window))
+
+ ;; Fix #897: "cannot open side window" error when TAB-completing file links
+ (defun +popup*hide-org-links-popup (orig-fn &rest args)
+ (cl-letf* ((old-org-completing-read (symbol-function 'org-completing-read))
+ ((symbol-function 'org-completing-read)
+ (lambda (&rest args)
+ (when-let* ((win (get-buffer-window "*Org Links*")))
+ ;; While helm is opened as a popup, it will mistaken the
+ ;; *Org Links* popup for the "originated window", and will
+ ;; target it for actions invoked by the user. However, since
+ ;; *Org Links* is a popup too (they're dedicated side
+ ;; windows), Emacs complains about being unable to split a
+ ;; side window. The simple fix: get rid of *Org Links*!
+ (delete-window win)
+ ;; But it must exist for org to clean up later.
+ (get-buffer-create "*Org Links*"))
+ (apply old-org-completing-read args))))
+ (apply orig-fn args)))
+ (advice-add #'org-insert-link :around #'+popup*hide-org-links-popup)
+
+ ;; Fix left-over popup window when closing persistent help for `helm-M-x'
+ (defun +popup*helm-elisp--persistent-help (candidate _fun &optional _name)
+ (let (win)
+ (when (and (helm-attr 'help-running-p)
+ (string= candidate (helm-attr 'help-current-symbol))
+ (setq win (get-buffer-window (get-buffer (help-buffer)))))
+ (delete-window win))))
+ (advice-add #'helm-elisp--persistent-help :before #'+popup*helm-elisp--persistent-help)
+
+ ;; `helm-ag'
+ (defun +helm*pop-to-buffer (orig-fn &rest args)
+ (pop-to-buffer
+ (save-window-excursion (apply orig-fn args)
+ (current-buffer))))
+ (advice-add #'helm-ag--edit :around #'+helm*pop-to-buffer))
+
+
+;; `ibuffer'
+(setq ibuffer-use-other-window t)
+
+
+;; `Info'
+(defun +popup*switch-to-info-window (&rest _)
+ (when-let* ((win (get-buffer-window "*info*")))
+ (when (+popup-window-p win)
+ (select-window win))))
+(advice-add #'info-lookup-symbol :after #'+popup*switch-to-info-window)
+
+
+;; `multi-term'
+(setq multi-term-buffer-name "doom terminal")
+
+
+;; `neotree'
+(after! neotree
+ (advice-add #'neo-util--set-window-width :override #'ignore)
+ (advice-remove #'balance-windows #'ad-Advice-balance-windows))
+
+
+;; `org'
+(after! org
+ (defvar +popup--disable-internal nil)
+ ;; Org has a scorched-earth window management system I'm not fond of. i.e. it
+ ;; kills all windows and monopolizes the frame. No thanks. We can do better
+ ;; ourselves.
+ (defun +popup*suppress-delete-other-windows (orig-fn &rest args)
+ (if +popup-mode
+ (cl-letf (((symbol-function 'delete-other-windows)
+ (symbol-function 'ignore)))
+ (apply orig-fn args))
+ (apply orig-fn args)))
+ (advice-add #'org-add-log-note :around #'+popup*suppress-delete-other-windows)
+ (advice-add #'org-capture-place-template :around #'+popup*suppress-delete-other-windows)
+ (advice-add #'org-export--dispatch-ui :around #'+popup*suppress-delete-other-windows)
+
+ (defun +popup*org-src-pop-to-buffer (orig-fn buffer context)
+ "Hand off the src-block window to the popup system by using `display-buffer'
+instead of switch-to-buffer-*."
+ (if (and (eq org-src-window-setup 'popup-window)
+ +popup-mode)
+ (pop-to-buffer buffer)
+ (funcall orig-fn buffer context)))
+ (advice-add #'org-src-switch-to-buffer :around #'+popup*org-src-pop-to-buffer)
+ (setq org-src-window-setup 'popup-window)
+
+ ;; Ensure todo, agenda, and other minor popups are delegated to the popup system.
+ (defun +popup*org-pop-to-buffer (orig-fn buf &optional norecord)
+ "Use `pop-to-buffer' instead of `switch-to-buffer' to open buffer.'"
+ (if +popup-mode
+ (pop-to-buffer buf nil norecord)
+ (funcall orig-fn buf norecord)))
+ (advice-add #'org-switch-to-buffer-other-window :around #'+popup*org-pop-to-buffer)
+
+ ;; `org-agenda'
+ (setq org-agenda-window-setup 'popup-window
+ org-agenda-restore-windows-after-quit nil)
+ ;; Don't monopolize the frame!
+ (defun +popup*org-agenda-suppress-delete-other-windows (orig-fn &rest args)
+ (cond ((not +popup-mode)
+ (apply orig-fn args))
+ ((eq org-agenda-window-setup 'popup-window)
+ (let ((org-agenda-window-setup 'other-window)
+ org-agenda-restore-windows-after-quit)
+ (cl-letf (((symbol-function 'delete-other-windows)
+ (symbol-function 'ignore)))
+ (apply orig-fn args))))
+ ((memq org-agenda-window-setup '(current-window other-window))
+ (with-popup-rules! nil
+ (cl-letf (((symbol-function 'delete-other-windows)
+ (symbol-function 'ignore)))
+ (apply orig-fn args))))
+ ((with-popup-rules! nil
+ (apply orig-fn args)))))
+ (advice-add #'org-agenda-prepare-window :around #'+popup*org-agenda-suppress-delete-other-windows))
+
+
+;; `persp-mode'
+(progn
+ (defun +popup*persp-mode-restore-popups (&rest _)
+ "Restore popup windows when loading a perspective from file."
+ (dolist (window (window-list))
+ (when (+popup-parameter 'popup window)
+ (+popup--init window nil))))
+ (advice-add #'persp-load-state-from-file :after #'+popup*persp-mode-restore-popups))
+
+
+;; `pdf-tools'
+(after! pdf-tools
+ (setq tablist-context-window-display-action
+ '((+popup-display-buffer-stacked-side-window)
+ (side . left)
+ (slot . 2)
+ (window-height . 0.3)
+ (inhibit-same-window . t))
+ pdf-annot-list-display-buffer-action
+ '((+popup-display-buffer-stacked-side-window)
+ (side . left)
+ (slot . 3)
+ (inhibit-same-window . t)))
+
+ (add-hook 'pdf-annot-list-mode-hook #'hide-mode-line-mode)
+ (set-popup-rule! "\\(^\\*Contents\\|'s annots\\*$\\)" :ignore t))
+
+
+;; `profiler'
+(defun doom*profiler-report-find-entry-in-other-window (orig-fn function)
+ (cl-letf (((symbol-function 'find-function)
+ (symbol-function 'find-function-other-window)))
+ (funcall orig-fn function)))
+(advice-add #'profiler-report-find-entry :around #'doom*profiler-report-find-entry-in-other-window)
+
+
+;; `wgrep'
+(progn
+ ;; close the popup after you're done with a wgrep buffer
+ (advice-add #'wgrep-abort-changes :after #'+popup*close)
+ (advice-add #'wgrep-finish-edit :after #'+popup*close))
+
+
+;; `which-key'
+(after! which-key
+ (when (eq which-key-popup-type 'side-window)
+ (setq which-key-popup-type 'custom
+ which-key-custom-popup-max-dimensions-function (lambda (_) (which-key--side-window-max-dimensions))
+ which-key-custom-hide-popup-function #'which-key--hide-buffer-side-window
+ which-key-custom-show-popup-function
+ (lambda (act-popup-dim)
+ (cl-letf (((symbol-function 'display-buffer-in-side-window)
+ (lambda (buffer alist)
+ (+popup-display-buffer-stacked-side-window
+ buffer (append '((vslot . -9999)) alist)))))
+ (which-key--show-buffer-side-window act-popup-dim))))))
+
+
+;; `windmove'
+(progn
+ ;; Users should be able to hop into popups easily, but Elisp shouldn't.
+ (defun doom*ignore-window-parameters (orig-fn &rest args)
+ "Allow *interactive* window moving commands to traverse popups."
+ (cl-letf (((symbol-function #'windmove-find-other-window)
+ (lambda (dir &optional arg window)
+ (window-in-direction
+ (pcase dir (`up 'above) (`down 'below) (_ dir))
+ window (bound-and-true-p +popup-mode) arg windmove-wrap-around t))))
+ (apply orig-fn args)))
+ (advice-add #'windmove-up :around #'doom*ignore-window-parameters)
+ (advice-add #'windmove-down :around #'doom*ignore-window-parameters)
+ (advice-add #'windmove-left :around #'doom*ignore-window-parameters)
+ (advice-add #'windmove-right :around #'doom*ignore-window-parameters))
diff --git a/modules/ui/popup/README.org b/modules/ui/popup/README.org
new file mode 100644
index 000000000..3545e7f76
--- /dev/null
+++ b/modules/ui/popup/README.org
@@ -0,0 +1,137 @@
+#+TITLE: :ui popup
+
+* Table of Contents :TOC:
+- [[#description][Description]]
+ - [[#module-flags][Module Flags]]
+- [[#prerequisites][Prerequisites]]
+- [[#configuration][Configuration]]
+ - [[#set-popup-rule-and-set-popup-rules][~set-popup-rule!~ and ~set-popup-rules!~]]
+ - [[#disabling-aggressive-mode-line-hiding-in-popups][Disabling aggressive mode-line hiding in popups]]
+- [[#appendix][Appendix]]
+ - [[#commands][Commands]]
+ - [[#library][Library]]
+ - [[#hacks][Hacks]]
+
+* Description
+This module provides a customizable popup window management system.
+
+Not all windows are created equally. Some are less important. Some I want gone
+once they have served their purpose, like code output or a help buffer. Others I
+want to stick around, like a scratch buffer or org-capture popup.
+
+More than that, popups ought to be be the second class citizens of my editor;
+spawned off to the side, discarded with the push of a button (e.g. =ESC= or
+=C-g=), and easily restored if I want to see them again. Of course, this system
+should clean up after itself and kill off buffers I mark as transient.
+
+** Module Flags
++ =+all= Enables fallback rules to ensure all temporary/special buffers (whose
+ name begins with a space or asterix) are treated as popups.
++ =+defaults= Enables reasonable default popup rules for a variety of buffers.
+
+* Prerequisites
+This module has no external prerequisites.
+
+* Configuration
+** ~set-popup-rule!~ and ~set-popup-rules!~
+This module has two functions for defining your own rules for popups:
+
+#+BEGIN_SRC emacs-lisp
+(set-popup-rule! PREDICATE &key IGNORE ACTIONS SIDE SIZE WIDTH HEIGHT SLOT VSLOT TTL QUIT SELECT MODELINE AUTOSAVE PARAMETERS)
+(set-popup-rules! &rest RULESETS)
+#+END_SRC
+
+~PREDICATE~ is a predicate function or regexp string to match against the
+buffer's name. To see what the other keywords do, check out the documentation
+for ~set-popup-rule!~ (=SPC h f set-popup-rule!=).
+
+#+begin_quote
+Rules are added to ~display-buffer-alist~, which instructs ~display-buffer~
+calls on how to set up windows for buffers that meet certain conditions.
+
+The ~switch-to-buffer~ command (and its ~switch-to-buffer-*~ variants) are not
+affected by ~display-buffer-alist~.
+#+end_quote
+
+e.g.
+#+BEGIN_SRC emacs-lisp
+(set-popup-rules!
+ '(("^ \\*" :slot -1) ; fallback rule for special buffers
+ ("^\\*" :select t)
+ ("^\\*Completions" :slot -1 :transient 0)
+ ("^\\*\\(?:scratch\\|Messages\\)" :transient t)
+ ("^\\*Help" :slot -1 :size 0.2 :select t)
+ ("^\\*doom:"
+ :size . 0.35 :select t :modeline t :quit t :transient t)))
+#+END_SRC
+
+Omitted parameters in a ~set-popup-rules!~ will use the defaults set in
+~+popup-defaults~.
+
+** Disabling aggressive mode-line hiding in popups
+There are two ways to go about this.
+
+1. Turn on modelines by changing the ~:modeline~ property in ~+popup-defaults~:
+
+ #+BEGIN_SRC emacs-lisp
+ ;; put in private/$USER/config.el
+ (map-put +popup-defaults :modeline t)
+ #+END_SRC
+
+ This will ensure all popups have a modeline /by default/, but allows you to
+ override this on a per-popup basis.
+
+2. Disable modeline-hiding entirely:
+
+ #+BEGIN_SRC emacs-lisp
+ ;; in ~/.doom.d/config.el
+ (remove-hook '+popup-buffer-mode-hook #'+popup|set-modeline-on-enable)
+ #+END_SRC
+
+* Appendix
+** Commands
++ ~+popup/other~ (aliased to ~other-popup~, bound to ~C-x p~)
++ ~+popup/toggle~
++ ~+popup/close~
++ ~+popup/close-all~
++ ~+popup/toggle~
++ ~+popup/restore~
++ ~+popup/raise~
+** Library
++ Functions
+ + ~+popup-window-p WINDOW~
+ + ~+popup-buffer-p BUFFER~
+ + ~+popup-buffer BUFFER &optional ALIST~
+ + ~+popup-parameter PARAMETER &optional WINDOW~
+ + ~+popup-parameter-fn PARAMETER &optional WINDOW~
+ + ~+popup-windows~
++ Macros
+ + ~without-popups!~
+ + ~save-popups!~
++ Hooks
+ + ~+popup|adjust-fringes~
+ + ~+popup|set-modeline~
+ + ~+popup|close-on-escape~
+ + ~+popup|cleanup-rules~
++ Minor modes
+ + ~+popup-mode~
+ + ~+popup-buffer-mode~
+** Hacks
++ =help-mode= has been advised to follow file links in the buffer you were in
+ before entering the popup, rather than in a new window.
++ =wgrep= buffers are advised to close themselves when aborting or committing
+ changes.
++ =persp-mode= is advised to restore popup windows when loading a session from
+ file.
++ Interactive calls to ~windmove-*~ commands (used by ~evil-window-*~ commands)
+ will ignore the ~no-other-window~ window parameter, allowing you to switch to
+ popup windows as if they're ordinary windows.
++ ~balance-windows~ has been advised to close popups while it does its business,
+ then restores them afterwards.
++ =neotree= advises ~balance-windows~, which causes major slow-downs when paired
+ with our ~balance-window~ advice, so we removes neotree's advice.
++ =org-mode= is an ongoing (and huge) effort. It has a scorched-earth window
+ management system I'm not fond of. ie. it kills all windows and monopolizes
+ the frame. On top of that, it /really/ likes to use ~switch-to-buffer~ for
+ most of its buffer management, which completely bypasses
+ ~display-buffer-alist~.
diff --git a/modules/ui/popup/autoload/popup.el b/modules/ui/popup/autoload/popup.el
new file mode 100644
index 000000000..82c64c71c
--- /dev/null
+++ b/modules/ui/popup/autoload/popup.el
@@ -0,0 +1,682 @@
+;;; ui/popup/autoload/popup.el -*- lexical-binding: t; -*-
+
+(defvar +popup--internal nil)
+
+(defun +popup--remember (windows)
+ "Remember WINDOWS (a list of windows) for later restoration."
+ (cl-assert (cl-every #'windowp windows) t)
+ (setq +popup--last
+ (cl-loop for w in windows
+ collect (cons (window-buffer w)
+ (window-state-get w)))))
+
+(defun +popup--kill-buffer (buffer ttl)
+ "Tries to kill BUFFER, as was requested by a transient timer. If it fails, eg.
+the buffer is visible, then set another timer and try again later."
+ (when (buffer-live-p buffer)
+ (let ((inhibit-quit t)
+ (kill-buffer-hook (remq '+popup|kill-buffer-hook kill-buffer-hook)))
+ (cond ((get-buffer-window buffer t)
+ (with-current-buffer buffer
+ (setq +popup--timer
+ (run-at-time ttl nil #'+popup--kill-buffer buffer ttl))))
+ ((eq ttl 0)
+ (kill-buffer buffer))
+ ((with-demoted-errors "Error killing transient buffer: %s"
+ (with-current-buffer buffer
+ (let (confirm-kill-processes)
+ (when-let* ((process (get-buffer-process buffer)))
+ (kill-process process))
+ (let (kill-buffer-hook kill-buffer-query-functions)
+ (kill-buffer buffer))))))))))
+
+(defun +popup--delete-window (window)
+ "Do housekeeping before destroying a popup window.
+
++ Disables `+popup-buffer-mode' so that any hooks attached to it get a chance to
+ run and do cleanup of its own.
++ Either kills the buffer or sets a transient timer, if the window has a
+ `transient' window parameter (see `+popup-window-parameters').
++ And finally deletes the window!"
+ (let ((buffer (window-buffer window))
+ (inhibit-quit t))
+ (and (buffer-file-name buffer)
+ (buffer-modified-p buffer)
+ (let ((autosave (+popup-parameter 'autosave window)))
+ (cond ((eq autosave 't))
+ ((null autosave)
+ (y-or-n-p "Popup buffer is modified. Save it?"))
+ ((functionp autosave)
+ (funcall autosave buffer))))
+ (with-current-buffer buffer (save-buffer)))
+ (let ((ignore-window-parameters t))
+ (if-let* ((wconf (window-parameter window 'saved-wconf)))
+ (set-window-configuration wconf)
+ (delete-window window)))
+ (unless (window-live-p window)
+ (with-current-buffer buffer
+ (set-buffer-modified-p nil)
+ (+popup-buffer-mode -1)
+ (unless +popup--inhibit-transient
+ (let ((ttl (+popup-parameter 'ttl window)))
+ (when (eq ttl 't)
+ (setq ttl (plist-get +popup-defaults :ttl)))
+ (cond ((null ttl))
+ ((functionp ttl)
+ (funcall ttl buffer))
+ ((not (integerp ttl))
+ (signal 'wrong-type-argument (list 'integerp ttl)))
+ ((= ttl 0)
+ (+popup--kill-buffer buffer 0))
+ ((add-hook 'kill-buffer-hook #'+popup|kill-buffer-hook nil t)
+ (setq +popup--timer
+ (run-at-time ttl nil #'+popup--kill-buffer
+ buffer ttl))))))))))
+
+(defun +popup--delete-other-windows (window)
+ "Fixes `delete-other-windows' when used from a popup window."
+ (when-let* ((window (ignore-errors (+popup/raise window))))
+ (let ((ignore-window-parameters t))
+ (delete-other-windows window)))
+ nil)
+
+(defun +popup--normalize-alist (alist)
+ "Merge `+popup-default-alist' and `+popup-default-parameters' with ALIST."
+ (when alist
+ (let ((alist ; handle defaults
+ (cl-remove-duplicates
+ (append alist +popup-default-alist)
+ :key #'car-safe :from-end t))
+ (parameters
+ (cl-remove-duplicates
+ (append (cdr (assq 'window-parameters alist))
+ +popup-default-parameters)
+ :key #'car-safe :from-end t)))
+ ;; handle `size'
+ (when-let* ((size (cdr (assq 'size alist)))
+ (side (or (cdr (assq 'side alist)) 'bottom))
+ (param (if (memq side '(left right))
+ 'window-width
+ 'window-height)))
+ (setq list (assq-delete-all 'size alist))
+ (setf (alist-get param alist) size))
+ (setf (alist-get 'window-parameters alist)
+ parameters)
+ alist)))
+
+(defun +popup--split-window (window size side)
+ "Ensure a non-dedicated/popup window is selected when splitting a window."
+ (unless +popup--internal
+ (cl-loop for win
+ in (cons (or window (selected-window))
+ (window-list nil 0 window))
+ unless (+popup-window-p win)
+ return (setq window win)))
+ (let ((ignore-window-parameters t))
+ (split-window window size side)))
+
+;;;###autoload
+(defun +popup--init (window &optional alist)
+ "Initializes a popup window. Run any time a popup is opened. It sets the
+default window parameters for popup windows, clears leftover transient timers
+and enables `+popup-buffer-mode'."
+ (with-selected-window window
+ (setq alist (delq (assq 'actions alist) alist))
+ (when (and alist +popup--populate-wparams)
+ ;; Emacs 26+ will automatically map the window-parameters alist entry to
+ ;; the popup window, so we need this for Emacs 25.x users
+ (dolist (param (cdr (assq 'window-parameters alist)))
+ (set-window-parameter window (car param) (cdr param))))
+ (set-window-parameter window 'popup t)
+ (set-window-parameter window 'split-window #'+popup--split-window)
+ (set-window-parameter window 'delete-window #'+popup--delete-window)
+ (set-window-parameter window 'delete-other-windows #'+popup--delete-other-windows)
+ (set-window-dedicated-p window 'popup)
+ (window-preserve-size
+ window (memq (window-parameter window 'window-side)
+ '(left right))
+ t)
+ (+popup-buffer-mode +1)
+ (run-hooks '+popup-create-window-hook)))
+
+
+;;
+;; Public library
+
+;;;###autoload
+(defun +popup-buffer-p (&optional buffer)
+ "Return non-nil if BUFFER is a popup buffer. Defaults to the current buffer."
+ (when +popup-mode
+ (let ((buffer (or buffer (current-buffer))))
+ (and (bufferp buffer)
+ (buffer-live-p buffer)
+ (buffer-local-value '+popup-buffer-mode buffer)
+ buffer))))
+
+;;;###autoload
+(defun +popup-window-p (&optional window)
+ "Return non-nil if WINDOW is a popup window. Defaults to the current window."
+ (when +popup-mode
+ (let ((window (or window (selected-window))))
+ (and (windowp window)
+ (window-live-p window)
+ (or (window-parameter window 'popup)
+ (window-parameter window 'no-other-window))
+ window))))
+
+;;;###autoload
+(defun +popup-buffer (buffer &optional alist)
+ "Open BUFFER in a popup window. ALIST describes its features."
+ (let* ((origin (selected-window))
+ (window-min-height 3)
+ (alist (+popup--normalize-alist alist))
+ (actions (or (cdr (assq 'actions alist))
+ +popup-default-display-buffer-actions)))
+ (when-let* ((popup (cl-loop for func in actions
+ if (funcall func buffer alist)
+ return it)))
+ (+popup--init popup alist)
+ (unless +popup--inhibit-select
+ (let ((select (+popup-parameter 'select popup)))
+ (if (functionp select)
+ (funcall select popup origin)
+ (select-window (if select popup origin)))))
+ popup)))
+
+;;;###autoload
+(defun +popup-parameter (parameter &optional window)
+ "Fetch the window PARAMETER (symbol) of WINDOW"
+ (window-parameter (or window (selected-window)) parameter))
+
+;;;###autoload
+(defun +popup-parameter-fn (parameter &optional window &rest args)
+ "Fetch the window PARAMETER (symbol) of WINDOW. If it is a function, run it
+with ARGS to get its return value."
+ (let ((val (+popup-parameter parameter window)))
+ (if (functionp val)
+ (apply val args)
+ val)))
+
+;;;###autoload
+(defun +popup-windows ()
+ "Returns a list of all popup windows."
+ (cl-remove-if-not #'+popup-window-p (window-list)))
+
+;;;###autoload
+(defun +popup-shrink-to-fit (&optional window)
+ "Shrinks WINDOW to fit the buffer contents, if the buffer isn't empty.
+
+Uses `shrink-window-if-larger-than-buffer'."
+ (unless window
+ (setq window (selected-window)))
+ (unless (= (- (point-max) (point-min)) 0)
+ (shrink-window-if-larger-than-buffer window)))
+
+;;;###autoload
+(defun +popup-alist-from-window-state (state)
+ "Convert window STATE (from `window-state-get') to a `display-buffer' alist."
+ (let* ((params (alist-get 'parameters state)))
+ `((side . ,(alist-get 'window-side params))
+ (window-width . ,(alist-get 'total-width state))
+ (window-height . ,(alist-get 'total-height state))
+ (window-parameters ,@params))))
+
+
+;;
+;; Hooks
+
+;;;###autoload
+(defun +popup|adjust-fringes ()
+ "Hides the fringe in popup windows, restoring them if `+popup-buffer-mode' is
+disabled."
+ (let ((f (if (bound-and-true-p +popup-buffer-mode) 0)))
+ (set-window-fringes nil f f fringes-outside-margins)))
+
+;;;###autoload
+(defun +popup|adjust-margins ()
+ "Creates padding for the popup window determined by `+popup-margin-width',
+restoring it if `+popup-buffer-mode' is disabled."
+ (when +popup-margin-width
+ (unless (memq (window-parameter nil 'window-side) '(left right))
+ (let ((m (if (bound-and-true-p +popup-buffer-mode) +popup-margin-width)))
+ (set-window-margins nil m m)))))
+
+(defvar hide-mode-line-format)
+;;;###autoload
+(defun +popup|set-modeline-on-enable ()
+ "Don't show modeline in popup windows without a `modeline' window-parameter.
+Possible values for this parameter are:
+
+ t show the mode-line as normal
+ nil hide the modeline entirely (the default)
+ a function `mode-line-format' is set to its return value
+
+Any non-nil value besides the above will be used as the raw value for
+`mode-line-format'."
+ (when (bound-and-true-p +popup-buffer-mode)
+ (let ((modeline (+popup-parameter 'modeline)))
+ (cond ((eq modeline 't))
+ ((null modeline)
+ ;; TODO use `mode-line-format' window parameter instead (emacs 26+)
+ (hide-mode-line-mode +1))
+ ((let ((hide-mode-line-format
+ (if (functionp modeline)
+ (funcall modeline)
+ modeline)))
+ (hide-mode-line-mode +1)))))))
+(put '+popup|set-modeline-on-enable 'permanent-local-hook t)
+
+;;;###autoload
+(defun +popup|unset-modeline-on-disable ()
+ "Restore the modeline when `+popup-buffer-mode' is deactivated."
+ (when (and (not (bound-and-true-p +popup-buffer-mode))
+ (bound-and-true-p hide-mode-line-mode))
+ (hide-mode-line-mode -1)))
+
+;;;###autoload
+(defun +popup|close-on-escape ()
+ "If called inside a popup, try to close that popup window (see
+`+popup/close'). If called outside, try to close all popup windows (see
+`+popup/close-all')."
+ (if (+popup-window-p)
+ (+popup/close)
+ (+popup/close-all)))
+
+;;;###autoload
+(defun +popup|cleanup-rules ()
+ "Cleans up any duplicate popup rules."
+ (interactive)
+ (setq +popup--display-buffer-alist
+ (cl-delete-duplicates +popup--display-buffer-alist
+ :key #'car :test #'equal :from-end t))
+ (when +popup-mode
+ (setq display-buffer-alist +popup--display-buffer-alist)))
+
+;;;###autoload
+(defun +popup|kill-buffer-hook ()
+ "TODO"
+ (when-let* ((window (get-buffer-window)))
+ (when (+popup-window-p window)
+ (let ((+popup--inhibit-transient t))
+ (+popup--delete-window window)))))
+
+
+;;
+;; Commands
+
+;;;###autoload
+(defalias 'other-popup #'+popup/other)
+
+;;;###autoload
+(defun +popup/buffer ()
+ "Open this buffer in a popup window."
+ (interactive)
+ (let ((+popup-default-display-buffer-actions
+ '(+popup-display-buffer-stacked-side-window))
+ (display-buffer-alist +popup--display-buffer-alist)
+ (buffer (current-buffer)))
+ (push (+popup--make "." +popup-defaults) display-buffer-alist)
+ (bury-buffer)
+ (pop-to-buffer buffer)))
+
+;;;###autoload
+(defun +popup/other ()
+ "Cycle through popup windows, like `other-window'. Ignores regular windows."
+ (interactive)
+ (let ((popups (+popup-windows))
+ (window (selected-window)))
+ (unless popups
+ (user-error "No popups are open"))
+ (select-window (if (+popup-window-p)
+ (or (car-safe (cdr (memq window popups)))
+ (car (delq window popups))
+ (car popups))
+ (car popups)))))
+
+;;;###autoload
+(defun +popup/close (&optional window force-p)
+ "Close WINDOW, if it's a popup window.
+
+This will do nothing if the popup's `quit' window parameter is either nil or
+'other. This window parameter is ignored if FORCE-P is non-nil."
+ (interactive
+ (list (selected-window)
+ current-prefix-arg))
+ (let ((window (or window (selected-window))))
+ (when (and (+popup-window-p window)
+ (or force-p
+ (memq (+popup-parameter-fn 'quit window window)
+ '(t current))))
+ (when +popup--remember-last
+ (+popup--remember (list window)))
+ (delete-window window)
+ t)))
+
+;;;###autoload
+(defun +popup/close-all (&optional force-p)
+ "Close all open popup windows.
+
+This will ignore popups with an `quit' parameter that is either nil or 'current.
+This window parameter is ignored if FORCE-P is non-nil."
+ (interactive "P")
+ (let (targets +popup--remember-last)
+ (dolist (window (+popup-windows))
+ (when (or force-p
+ (memq (+popup-parameter-fn 'quit window window)
+ '(t other)))
+ (push window targets)))
+ (when targets
+ (+popup--remember targets)
+ (mapc #'delete-window targets)
+ t)))
+
+;;;###autoload
+(defun +popup/toggle ()
+ "If popups are open, close them. If they aren't, restore the last one or open
+the message buffer in a popup window."
+ (interactive)
+ (let ((+popup--inhibit-transient t))
+ (cond ((+popup-windows) (+popup/close-all t))
+ ((ignore-errors (+popup/restore)))
+ ((display-buffer (get-buffer "*Messages*"))))))
+
+;;;###autoload
+(defun +popup/restore ()
+ "Restore the last popups that were closed, if any."
+ (interactive)
+ (unless +popup--last
+ (error "No popups to restore"))
+ (cl-loop for (buffer . state) in +popup--last
+ if (buffer-live-p buffer)
+ do (+popup-buffer buffer (+popup-alist-from-window-state state)))
+ (setq +popup--last nil)
+ t)
+
+;;;###autoload
+(defun +popup/raise (window)
+ "Raise the current popup window into a regular window."
+ (interactive (list (selected-window)))
+ (cl-check-type window window)
+ (unless (+popup-window-p window)
+ (user-error "Cannot raise a non-popup window"))
+ (let ((buffer (current-buffer))
+ (+popup--inhibit-transient t)
+ +popup--remember-last)
+ (+popup/close window 'force)
+ (display-buffer-pop-up-window buffer nil)))
+
+;;;###autoload
+(defun +popup/diagnose ()
+ "Reveal what popup rule will be used for the current buffer."
+ (interactive)
+ (or (cl-loop with bname = (buffer-name)
+ for (pred . action) in display-buffer-alist
+ if (and (functionp pred) (funcall pred bname action))
+ return (cons pred action)
+ else if (and (stringp pred) (string-match-p pred bname))
+ return (cons pred action))
+ (message "No popup rule for this buffer")))
+
+
+;;
+;; Advice
+
+;;;###autoload
+(defun +popup*close (&rest _)
+ "TODO"
+ (+popup/close nil t))
+
+;;;###autoload
+(defun +popup*save (orig-fn &rest args)
+ "Sets aside all popups before executing the original function, usually to
+prevent the popup(s) from messing up the UI (or vice versa)."
+ (save-popups! (apply orig-fn args)))
+
+;;;###autoload
+(defun +popup-display-buffer-fullframe (buffer alist)
+ "Displays the buffer fullscreen."
+ (let ((wconf (current-window-configuration)))
+ (when-let (window (or (display-buffer-reuse-window buffer alist)
+ (display-buffer-same-window buffer alist)
+ (display-buffer-pop-up-window buffer alist)
+ (display-buffer-use-some-window buffer alist)))
+ (set-window-parameter window 'saved-wconf wconf)
+ (add-to-list 'window-persistent-parameters '(saved-wconf . t))
+ (delete-other-windows window)
+ window)))
+
+;;;###autoload
+(defun +popup-display-buffer-stacked-side-window (buffer alist)
+ "A `display-buffer' action that serves as an alternative to
+`display-buffer-in-side-window', but allows for stacking popups with the `vslot'
+alist entry.
+
+Accepts the same arguments as `display-buffer-in-side-window'. You must set
+`window--sides-inhibit-check' to non-nil for this work properly."
+ (let* ((side (or (cdr (assq 'side alist)) 'bottom))
+ (slot (or (cdr (assq 'slot alist)) 0))
+ (vslot (or (cdr (assq 'vslot alist)) 0))
+ (left-or-right (memq side '(left right)))
+ (display-buffer-mark-dedicated (or display-buffer-mark-dedicated 'popup)))
+
+ (cond ((not (memq side '(top bottom left right)))
+ (error "Invalid side %s specified" side))
+ ((not (numberp slot))
+ (error "Invalid slot %s specified" slot))
+ ((not (numberp vslot))
+ (error "Invalid vslot %s specified" vslot)))
+
+ (let* ((major (get-window-with-predicate
+ (lambda (window)
+ (and (eq (window-parameter window 'window-side) side)
+ (eq (window-parameter window 'window-vslot) vslot)))
+ nil))
+ (reversed (window--sides-reverse-on-frame-p (selected-frame)))
+ (windows
+ (cond ((window-live-p major)
+ (list major))
+ ((window-valid-p major)
+ (let* ((first (window-child major))
+ (next (window-next-sibling first))
+ (windows (list next first)))
+ (setq reversed (> (window-parameter first 'window-slot)
+ (window-parameter next 'window-slot)))
+ (while (setq next (window-next-sibling next))
+ (setq windows (cons next windows)))
+ (if reversed windows (nreverse windows))))))
+ (slots (if major (max 1 (window-child-count major))))
+ (max-slots
+ (nth (plist-get '(left 0 top 1 right 2 bottom 3) side)
+ window-sides-slots))
+ (window--sides-inhibit-check t)
+ window this-window this-slot prev-window next-window
+ best-window best-slot abs-slot)
+
+ (cond ((and (numberp max-slots) (<= max-slots 0))
+ nil)
+ ((not windows)
+ (cl-letf (((symbol-function 'window--make-major-side-window-next-to)
+ (lambda (_side) (frame-root-window (selected-frame)))))
+ (when-let* ((window (window--make-major-side-window buffer side slot alist)))
+ (set-window-parameter window 'window-vslot vslot)
+ (add-to-list 'window-persistent-parameters '(window-vslot . writable))
+ window)))
+ (t
+ ;; Scan windows on SIDE.
+ (catch 'found
+ (dolist (window windows)
+ (setq this-slot (window-parameter window 'window-slot))
+ (cond ((not (numberp this-slot)))
+ ((= this-slot slot) ; A window with a matching slot found
+ (setq this-window window)
+ (throw 'found t))
+ (t
+ ;; Check if this window has a better slot value wrt the
+ ;; slot of the window we want.
+ (setq abs-slot
+ (if (or (and (> this-slot 0) (> slot 0))
+ (and (< this-slot 0) (< slot 0)))
+ (abs (- slot this-slot))
+ (+ (abs slot) (abs this-slot))))
+ (unless (and best-slot (<= best-slot abs-slot))
+ (setq best-window window)
+ (setq best-slot abs-slot))
+ (if reversed
+ (cond
+ ((<= this-slot slot)
+ (setq next-window window))
+ ((not prev-window)
+ (setq prev-window window)))
+ (cond
+ ((<= this-slot slot)
+ (setq prev-window window))
+ ((not next-window)
+ (setq next-window window))))))))
+
+ ;; `this-window' is the first window with the same SLOT.
+ ;; `prev-window' is the window with the largest slot < SLOT. A new
+ ;; window will be created after it.
+ ;; `next-window' is the window with the smallest slot > SLOT. A new
+ ;; window will be created before it.
+ ;; `best-window' is the window with the smallest absolute
+ ;; difference of its slot and SLOT.
+ (or (and this-window
+ ;; Reuse `this-window'.
+ (with-current-buffer buffer
+ (setq window--sides-shown t))
+ (window--display-buffer
+ buffer this-window 'reuse alist))
+ (and (or (not max-slots) (< slots max-slots))
+ (or (and next-window
+ ;; Make new window before `next-window'.
+ (let ((next-side (if left-or-right 'above 'left))
+ (+popup--internal t)
+ (window-combination-resize 'side))
+ (setq window
+ (ignore-errors (split-window next-window nil next-side)))))
+ (and prev-window
+ ;; Make new window after `prev-window'.
+ (let ((prev-side (if left-or-right 'below 'right))
+ (+popup--internal t)
+ (window-combination-resize 'side))
+ (setq window
+ (ignore-errors (split-window prev-window nil prev-side))))))
+ (set-window-parameter window 'window-slot slot)
+ (with-current-buffer buffer
+ (setq window--sides-shown t))
+ (window--display-buffer
+ buffer window 'window alist))
+ (and best-window
+ ;; Reuse `best-window'.
+ (progn
+ ;; Give best-window the new slot value.
+ (set-window-parameter best-window 'window-slot slot)
+ (with-current-buffer buffer
+ (setq window--sides-shown t))
+ (window--display-buffer
+ buffer best-window 'reuse alist)))))))))
+
+
+;;
+;; Emacs backwards compatibility
+
+(unless EMACS27+
+ (defun +popup*set-window-dedicated (window)
+ "Ensure `window--dispaly-buffer' respects `display-buffer-mark-dedicated'.
+
+This was not so until recent Emacs 27 builds, where it causes breaking errors.
+This advice ensures backwards compatibility for Emacs <= 26 users."
+ (when (and (windowp window) display-buffer-mark-dedicated)
+ (set-window-dedicated-p window display-buffer-mark-dedicated))
+ window)
+ (advice-add #'window--display-buffer :filter-return #'+popup*set-window-dedicated))
+
+(unless EMACS26+
+ (defvar window-sides-reversed nil)
+
+ (defun window--sides-reverse-on-frame-p (frame)
+ "Return non-nil when side windows should appear reversed on FRAME.
+This uses some heuristics to guess the user's intentions when the
+selected window of FRAME is a side window ."
+ (cond
+ ;; Reverse when `window-sides-reversed' is t. Do not reverse when
+ ;; `window-sides-reversed' is nil.
+ ((memq window-sides-reversed '(nil t))
+ window-sides-reversed)
+ ;; Reverse when FRAME's selected window shows a right-to-left buffer.
+ ((let ((window (frame-selected-window frame)))
+ (when (and (not (window-parameter window 'window-side))
+ (or (not (window-minibuffer-p window))
+ (setq window (minibuffer-selected-window))))
+ (with-current-buffer (window-buffer window)
+ (eq bidi-paragraph-direction 'right-to-left)))))
+ ;; Reverse when FRAME's `window-sides-main-selected-window' parameter
+ ;; specifies a live window showing a right-to-left buffer.
+ ((let ((window (frame-parameter
+ frame 'window-sides-main-selected-window)))
+ (when (window-live-p window)
+ (with-current-buffer (window-buffer window)
+ (eq bidi-paragraph-direction 'right-to-left)))))
+ ;; Reverse when all windows in FRAME's main window show right-to-left
+ ;; buffers.
+ (t
+ (catch 'found
+ (walk-window-subtree
+ (lambda (window)
+ (with-current-buffer (window-buffer window)
+ (when (eq bidi-paragraph-direction 'left-to-right)
+ (throw 'found nil))))
+ (window-main-window frame))
+ t))))
+
+ (defun window--make-major-side-window (buffer side slot &optional alist)
+ "Display BUFFER in a new major side window on the selected frame.
+SIDE must be one of `left', `top', `right' or `bottom'. SLOT
+specifies the slot to use. ALIST is an association list of
+symbols and values as passed to `display-buffer-in-side-window'.
+Return the new window, nil if its creation failed.
+
+This is an auxiliary function of `display-buffer-in-side-window'
+and may be called only if no window on SIDE exists yet."
+ (let* ((left-or-right (memq side '(left right)))
+ (next-to (window--make-major-side-window-next-to side))
+ (on-side (cond
+ ((eq side 'top) 'above)
+ ((eq side 'bottom) 'below)
+ (t side)))
+ (window--sides-inhibit-check t)
+ ;; The following two bindings will tell `split-window' to take
+ ;; the space for the new window from the selected frame's main
+ ;; window and not make a new parent window unless needed.
+ (window-combination-resize 'side)
+ (window-combination-limit nil)
+ (window (ignore-errors (split-window next-to nil on-side))))
+ (when window
+ ;; Initialize `window-side' parameter of new window to SIDE and
+ ;; make that parameter persistent.
+ (set-window-parameter window 'window-side side)
+ (add-to-list 'window-persistent-parameters '(window-side . writable))
+ ;; Install `window-slot' parameter of new window and make that
+ ;; parameter persistent.
+ (set-window-parameter window 'window-slot slot)
+ (add-to-list 'window-persistent-parameters '(window-slot . writable))
+ ;; Auto-adjust height/width of new window unless a size has been
+ ;; explicitly requested.
+ (unless (if left-or-right
+ (cdr (assq 'window-width alist))
+ (cdr (assq 'window-height alist)))
+ (setq alist
+ (cons
+ (cons
+ (if left-or-right 'window-width 'window-height)
+ (/ (window-total-size (frame-root-window) left-or-right)
+ ;; By default use a fourth of the size of the frame's
+ ;; root window.
+ 4))
+ alist)))
+ (with-current-buffer buffer
+ (setq window--sides-shown t))
+ ;; Install BUFFER in new window and return WINDOW.
+ (window--display-buffer buffer window 'window alist 'side))))
+
+ (advice-add #'window--sides-check :override #'ignore))
diff --git a/modules/ui/popup/autoload/settings.el b/modules/ui/popup/autoload/settings.el
new file mode 100644
index 000000000..ad4bead4f
--- /dev/null
+++ b/modules/ui/popup/autoload/settings.el
@@ -0,0 +1,200 @@
+;;; ui/popup/autoload/settings.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defvar +popup--display-buffer-alist nil)
+
+;;;###autoload
+(defvar +popup-defaults
+ (list :side 'bottom
+ :height 0.16
+ :width 40
+ :quit t
+ :select #'ignore
+ :ttl 5)
+ "Default properties for popup rules defined with `set-popup-rule!'.")
+
+;;;###autoload
+(defun +popup--make (predicate plist)
+ (cond ((and plist (not (keywordp (car plist))))
+ ;; FIXME deprecated popup rule support
+ (message "Warning: the old usage of `set-popup-rule!' is deprecated; update the rule for '%s'"
+ predicate)
+ (cl-destructuring-bind (condition &optional alist parameters)
+ (list predicate (car plist) (cadr plist))
+ (if (eq alist :ignore)
+ (list condition nil)
+ `(,condition (+popup-buffer)
+ ,@alist
+ (window-parameters ,@parameters)))))
+ ((plist-get plist :ignore)
+ (list predicate nil))
+ ((let* ((plist (append plist +popup-defaults))
+ (alist
+ `((actions . ,(plist-get plist :actions))
+ (side . ,(plist-get plist :side))
+ (size . ,(plist-get plist :size))
+ (window-width . ,(plist-get plist :width))
+ (window-height . ,(plist-get plist :height))
+ (slot . ,(plist-get plist :slot))
+ (vslot . ,(plist-get plist :vslot))))
+ (params
+ `((ttl . ,(plist-get plist :ttl))
+ (quit . ,(plist-get plist :quit))
+ (select . ,(plist-get plist :select))
+ (modeline . ,(plist-get plist :modeline))
+ (autosave . ,(plist-get plist :autosave))
+ ,@(plist-get plist :parameters))))
+ `(,predicate (+popup-buffer)
+ ,@alist
+ (window-parameters ,@params))))))
+
+;;;###autodef
+(defun set-popup-rule! (predicate &rest plist)
+ "Define a popup rule.
+
+These rules affect buffers displayed with `pop-to-buffer' and `display-buffer'
+(or their siblings). Buffers displayed with `switch-to-buffer' (and its
+variants) will not be affected by these rules (as they are unaffected by
+`display-buffer-alist', which powers the popup management system).
+
+PREDICATE can be either a) a regexp string (matched against the buffer's name)
+or b) a function that takes no arguments and returns a boolean.
+
+PLIST can be made up of any of the following properties:
+
+:ignore BOOL
+ If BOOL is non-nil, popups matching PREDICATE will not be handled by the popup
+ system. Use this for buffers that have their own window management system like
+ magit or helm.
+
+:actions ACTIONS
+ ACTIONS is a list of functions or an alist containing (FUNCTION . ALIST). See
+ `display-buffer''s second argument for more information on its format and what
+ it accepts. If omitted, `+popup-default-display-buffer-actions' is used.
+
+:side 'bottom|'top|'left|'right
+ Which side of the frame to open the popup on. This is only respected if
+ `+popup-display-buffer-stacked-side-window' or `display-buffer-in-side-window'
+ is in :actions or `+popup-default-display-buffer-actions'.
+
+:size/:width/:height FLOAT|INT|FN
+ Determines the size of the popup. If more tha one of these size properties are
+ given :size always takes precedence, and is mapped with window-width or
+ window-height depending on what :side the popup is opened. Setting a height
+ for a popup that opens on the left or right is harmless, but comes into play
+ if two popups occupy the same :vslot.
+
+ If a FLOAT (0 < x < 1), the number represents how much of the window will be
+ consumed by the popup (a percentage).
+ If an INT, the number determines the size in lines (height) or units of
+ character width (width).
+ If a function, it takes one argument: the popup window, and can do whatever it
+ wants with it, typically resize it, like `+popup-shrink-to-fit'.
+
+:slot/:vslot INT
+ (This only applies to popups with a :side and only if :actions is blank or
+ contains the `+popup-display-buffer-stacked-side-window' action) These control
+ how multiple popups are laid out. INT can be any integer, positive and
+ negative.
+
+ :slot controls lateral positioning (e.g. the horizontal positioning for
+ top/bottom popups, or vertical positioning for left/right popups).
+ :vslot controls popup stacking (from the edge of the frame toward the center).
+
+ Let's assume popup A and B are opened with :side 'bottom, in that order.
+ If they possess the same :slot and :vslot, popup B will replace popup A.
+ If popup B has a higher :slot, it will open to the right of popup A.
+ If popup B has a lower :slot, it will open to the left of popup A.
+ If popup B has a higher :vslot, it will open above popup A.
+ If popup B has a lower :vslot, it will open below popup A.
+
+:ttl INT|BOOL|FN
+ Stands for time-to-live. It can be t, an integer, nil or a function. This
+ controls how (and if) the popup system will clean up after the popup.
+
+ If any non-zero integer, wait that many seconds before killing the buffer (and
+ any associated processes).
+ If 0, the buffer is immediately killed.
+ If nil, the buffer won't be killed and is left to its own devices.
+ If t, resort to the default :ttl in `+popup-defaults'. If none exists, this is
+ the same as nil.
+ If a function, it takes one argument: the target popup buffer. The popup
+ system does nothing else and ignores the function's return value.
+
+:quit FN|BOOL|'other|'current
+ Can be t, 'other, 'current, nil, or a function. This determines the behavior
+ of the ESC/C-g keys in or outside of popup windows.
+
+ If t, close the popup if ESC/C-g is pressed anywhere.
+ If 'other, close this popup if ESC/C-g is pressed outside of any popup. This
+ is great for popups you may press ESC/C-g a lot in.
+ If 'current, close the current popup if ESC/C-g is pressed from inside of the
+ popup. This makes it harder to accidentally close a popup until you really
+ want to.
+ If nil, pressing ESC/C-g will never close this popup.
+ If a function, it takes one argument: the to-be-closed popup window, and is
+ run when ESC/C-g is pressed while that popup is open. It must return one of
+ the other values to determine the fate of the popup.
+
+:select BOOL|FN
+ Can be a boolean or function. The boolean determines whether to focus the
+ popup window after it opens (non-nil) or focus the origin window (nil).
+
+ If a function, it takes two arguments: the popup window and originating window
+ (where you were before the popup opened). The popup system does nothing else
+ and ignores the function's return value.
+
+:modeline BOOL|FN|LIST
+ Can be t (show the default modeline), nil (show no modeline), a function that
+ returns a modeline format or a valid value for `mode-line-format' to be used
+ verbatim. The function takes no arguments and is run in the context of the
+ popup buffer.
+
+:autosave BOOL|FN
+ This parameter determines what to do with modified buffers when closing popup
+ windows. It accepts t, 'ignore, a function or nil.
+
+ If t, no prompts. Just save them automatically (if they're file-visiting
+ buffers). Same as 'ignore for non-file-visiting buffers.
+ If nil (the default), prompt the user what to do if the buffer is
+ file-visiting and modified.
+ If 'ignore, no prompts, no saving. Just silently kill it.
+ If a function, it is run with one argument: the popup buffer, and must return
+ non-nil to save or nil to do nothing (but no prompts).
+
+:parameters ALIST
+ An alist of custom window parameters. See `(elisp)Window Parameters'.
+
+If any of these are omitted, defaults derived from `+popup-defaults' will be
+used.
+
+\(fn PREDICATE &key IGNORE ACTIONS SIDE SIZE WIDTH HEIGHT SLOT VSLOT TTL QUIT SELECT MODELINE AUTOSAVE PARAMETERS)"
+ (declare (indent defun))
+ (push (+popup--make predicate plist) +popup--display-buffer-alist)
+ (when (bound-and-true-p +popup-mode)
+ (setq display-buffer-alist +popup--display-buffer-alist))
+ +popup--display-buffer-alist)
+
+;;;###autodef
+(defun set-popup-rules! (&rest rulesets)
+ "Defines multiple popup rules.
+
+Every entry in RULESETS should be a list of alists where the CAR is the
+predicate and CDR is a plist. See `set-popup-rule!' for details on the predicate
+and plist.
+
+Example:
+
+ (set-popup-rules!
+ '((\"^ \\*\" :slot 1 :vslot -1 :size #'+popup-shrink-to-fit)
+ (\"^\\*\" :slot 1 :vslot -1 :select t))
+ '((\"^\\*Completions\" :slot -1 :vslot -2 :ttl 0)
+ (\"^\\*Compil\\(?:ation\\|e-Log\\)\" :size 0.3 :ttl 0 :quit t)))"
+ (declare (indent 0))
+ (dolist (rules rulesets)
+ (dolist (rule rules)
+ (push (+popup--make (car rule) (cdr rule))
+ +popup--display-buffer-alist)))
+ (when (bound-and-true-p +popup-mode)
+ (setq display-buffer-alist +popup--display-buffer-alist))
+ +popup--display-buffer-alist)
diff --git a/modules/ui/popup/config.el b/modules/ui/popup/config.el
new file mode 100644
index 000000000..6f7b308f5
--- /dev/null
+++ b/modules/ui/popup/config.el
@@ -0,0 +1,178 @@
+;;; ui/popup/config.el -*- lexical-binding: t; -*-
+
+(defconst +popup-window-parameters '(ttl quit select modeline popup)
+ "A list of custom parameters to be added to `window-persistent-parameters'.
+Modifying this has no effect, unless done before ui/popup loads.")
+
+(defvar +popup-default-display-buffer-actions
+ '(+popup-display-buffer-stacked-side-window)
+ "The functions to use to display the popup buffer.")
+
+(defvar +popup-default-alist
+ '((window-height . 0.16) ; remove later
+ (reusable-frames . visible))
+ "The default alist for `display-buffer-alist' rules.")
+
+(defvar +popup-default-parameters
+ '((transient . t) ; remove later
+ (quit . t) ; remove later
+ (select . ignore) ; remove later
+ (no-other-window . t))
+ "The default window parameters.")
+
+(defvar +popup-margin-width 1
+ "Size of the margins to give popup windows. Set this to nil to disable margin
+adjustment.")
+
+(defvar +popup--populate-wparams (not EMACS26+))
+(defvar +popup--inhibit-transient nil)
+(defvar +popup--inhibit-select nil)
+(defvar +popup--old-display-buffer-alist nil)
+(defvar +popup--remember-last t)
+(defvar +popup--last nil)
+(defvar-local +popup--timer nil)
+
+
+;;
+;; Global modes
+
+(defvar +popup-mode-map (make-sparse-keymap)
+ "Active keymap in a session with the popup system enabled. See
+`+popup-mode'.")
+
+(defvar +popup-buffer-mode-map
+ (let ((map (make-sparse-keymap)))
+ (when (featurep! :feature evil)
+ ;; For maximum escape coverage in emacs state buffers; this only works in
+ ;; GUI Emacs, in tty Emacs use C-g instead
+ (define-key map [escape] #'doom/escape))
+ map)
+ "Active keymap in popup windows. See `+popup-buffer-mode'.")
+
+(define-minor-mode +popup-mode
+ "Global minor mode representing Doom's popup management system."
+ :init-value nil
+ :global t
+ :keymap +popup-mode-map
+ (cond (+popup-mode
+ (add-hook 'doom-escape-hook #'+popup|close-on-escape t)
+ (setq +popup--old-display-buffer-alist display-buffer-alist
+ display-buffer-alist +popup--display-buffer-alist
+ window--sides-inhibit-check t)
+ (dolist (prop +popup-window-parameters)
+ (push (cons prop 'writable) window-persistent-parameters)))
+ (t
+ (remove-hook 'doom-escape-hook #'+popup|close-on-escape)
+ (setq display-buffer-alist +popup--old-display-buffer-alist
+ window--sides-inhibit-check nil)
+ (+popup|cleanup-rules)
+ (dolist (prop +popup-window-parameters)
+ (delq (assq prop window-persistent-parameters)
+ window-persistent-parameters)))))
+
+(define-minor-mode +popup-buffer-mode
+ "Minor mode for individual popup windows.
+
+It is enabled when a buffer is displayed in a popup window and disabled when
+that window has been changed or closed."
+ :init-value nil
+ :keymap +popup-buffer-mode-map
+ (if (not +popup-buffer-mode)
+ (remove-hook 'after-change-major-mode-hook #'+popup|set-modeline-on-enable t)
+ (add-hook 'after-change-major-mode-hook #'+popup|set-modeline-on-enable nil t)
+ (when (timerp +popup--timer)
+ (remove-hook 'kill-buffer-hook #'+popup|kill-buffer-hook t)
+ (cancel-timer +popup--timer)
+ (setq +popup--timer nil))))
+
+(put '+popup-buffer-mode 'permanent-local t)
+(put '+popup-buffer-mode 'permanent-local-hook t)
+(put '+popup|set-modeline-on-enable 'permanent-local-hook t)
+
+
+;;
+;; Macros
+
+(defmacro with-popup-rules! (rules &rest body)
+ "Evaluate BODY with popup RULES. RULES is a list of popup rules. Each rule
+should match the arguments of `+popup-define' or the :popup setting."
+ (declare (indent defun))
+ `(let ((+popup--display-buffer-alist +popup--old-display-buffer-alist)
+ display-buffer-alist)
+ (set-popup-rules! ,rules)
+ (when (bound-and-true-p +popup-mode)
+ (setq display-buffer-alist +popup--display-buffer-alist))
+ ,@body))
+
+(defmacro save-popups! (&rest body)
+ "Sets aside all popups before executing the original function, usually to
+prevent the popup(s) from messing up the UI (or vice versa)."
+ `(let* ((in-popup-p (+popup-buffer-p))
+ (popups (+popup-windows))
+ (+popup--inhibit-transient t)
+ +popup--last)
+ (dolist (p popups)
+ (+popup/close p 'force))
+ (unwind-protect
+ (progn ,@body)
+ (when popups
+ (let ((origin (selected-window)))
+ (+popup/restore)
+ (unless in-popup-p
+ (select-window origin)))))))
+
+
+;;
+;; Default popup rules & bootstrap
+
+(set-popup-rules!
+ (when (featurep! +all)
+ '(("^\\*" :slot 1 :vslot -1 :select t)
+ ("^ \\*" :slot 1 :vslot -1 :size +popup-shrink-to-fit)))
+ (when (featurep! +defaults)
+ '(("^\\*bin/doom\\*$"
+ :vslot 9999 :size 0.75 :quit 'current :select t :ttl 0)
+ ("^\\*Completions"
+ :slot -1 :vslot -2 :ttl 0)
+ ("^\\*Compil\\(?:ation\\|e-Log\\)"
+ :vslot -2 :size 0.3 :ttl nil :quit t)
+ ("^\\*\\(?:scratch\\|Messages\\)"
+ :autosave t :ttl nil)
+ ("^\\*Man "
+ :size 0.45 :vslot -3 :ttl 0 :quit t :select t)
+ ("^\\*doom \\(?:term\\|eshell\\)"
+ :size 0.25 :vslot -4 :select t :quit nil :ttl 0)
+ ("^\\*doom:"
+ :vslot -5 :size 0.35 :size bottom :autosave t :select t :modeline t :quit nil)
+ ("^\\*\\(?:\\(?:Pp E\\|doom e\\)val\\)"
+ :size +popup-shrink-to-fit :ttl 0 :select ignore)
+ ("^\\*Customize"
+ :slot 2 :side right :select t :quit t)
+ ("^ \\*undo-tree\\*"
+ :slot 2 :side left :size 20 :select t :quit t)
+ ;; `help-mode', `helpful-mode'
+ ("^\\*[Hh]elp"
+ :slot 2 :vslot -2 :size 0.35 :select t)
+ ;; `eww' (and used by dash docsets)
+ ("^\\*eww\\*"
+ :vslot -11 :size 0.35 :select t)
+ ;; `Info-mode'
+ ("^\\*info\\*$"
+ :slot 2 :vslot 2 :size 0.45 :select t)))
+ '(("^\\*Backtrace" :vslot 99 :size 0.4 :quit nil)
+ ("^\\*CPU-Profiler-Report " :side bottom :vslot 100 :slot 1 :height 0.4 :width 0.5 :quit nil)
+ ("^\\*Memory-Profiler-Report " :side bottom :vslot 100 :slot 2 :height 0.4 :width 0.5 :quit nil)))
+
+(add-hook 'doom-init-ui-hook #'+popup-mode :append)
+
+(add-hook! '+popup-buffer-mode-hook
+ #'(+popup|adjust-fringes
+ +popup|adjust-margins
+ +popup|set-modeline-on-enable
+ +popup|unset-modeline-on-disable))
+
+
+;;
+;; Hacks
+
+(load! "+hacks")
diff --git a/modules/ui/popup/test/test-popup.el b/modules/ui/popup/test/test-popup.el
new file mode 100644
index 000000000..7a0b2327c
--- /dev/null
+++ b/modules/ui/popup/test/test-popup.el
@@ -0,0 +1,212 @@
+;; -*- no-byte-compile: t; -*-
+;;; ui/popup/test/test-popup.el
+
+(require! :ui popup)
+
+(describe "ui/popup"
+ :var (display-buffer-alist
+ +popup-default-display-buffer-actions
+ +popup--display-buffer-alist
+ +popup-defaults
+ wconf)
+
+ (before-all
+ (delete-other-windows)
+ (switch-to-buffer "*scratch*")
+ (setq wconf (current-window-configuration))
+ (+popup-mode +1))
+ (after-all
+ (+popup-mode -1))
+
+ (before-each
+ (setq display-buffer-alist nil
+ +popup--display-buffer-alist nil
+ +popup-default-display-buffer-actions '(+popup-display-buffer-stacked-side-window)
+ +popup-defaults '(:side bottom :select ignore :ttl nil :slot 1 :vslot 1)))
+ (after-each
+ (set-window-configuration wconf))
+
+ (describe "set-popup-rule!"
+ (it "sets popup rules"
+ (set-popup-rule! "does-not-exist" :size 10)
+ (let ((rule (cdr (assoc "does-not-exist" display-buffer-alist))))
+ (expect rule :to-contain '(+popup-buffer))
+ (expect rule :to-contain '(size . 10))))
+ (it "shadows old rules"
+ (set-popup-rule! "a" :size 10)
+ (set-popup-rule! "a" :size 20)
+ (expect (cdr (assoc "a" display-buffer-alist))
+ :to-contain '(size . 20)))
+ (it "resolves to defaults"
+ (let ((+popup-defaults '(:size 5)))
+ (set-popup-rule! "a")
+ (expect (cdr (assoc "a" display-buffer-alist))
+ :to-contain '(size . 5)))))
+
+ (describe "popup rules"
+ :var (origin a b c d e f g)
+ (before-all (setq origin (current-buffer)))
+ (before-each
+ (dolist (name '(a b c d e f g))
+ (set name (get-buffer-create (symbol-name name)))))
+ (after-each
+ (let (kill-buffer-query-functions kill-buffer-hook)
+ (dolist (x (list a b c d e f g))
+ (ignore-errors (delete-window (get-buffer-window x)))
+ (kill-buffer x))))
+
+ (describe "slot positioning"
+ (before-each
+ (set-popup-rules!
+ '(("a" :slot 1 :vslot 1)
+ ("b" :slot 2 :vslot 1)
+ ("c" :slot 1 :vslot 2)
+ ("d" :slot 2 :vslot 2)
+ ("e" :slot 1 :vslot 3)
+ ("f" :slot 1 :vslot 3)
+ ("g"))))
+
+ (it "replaces popups with the same slots"
+ (mapc #'display-buffer (list e f))
+ (expect (length (+popup-windows)) :to-be 1))
+
+ (it "replaces popups among multiple that have the same slots"
+ (let ((first (display-buffer a))
+ (second (display-buffer b))
+ (third (display-buffer e))
+ (fourth (display-buffer f)))
+ (expect (+popup-windows) :to-have-same-items-as
+ (list first second fourth))))
+
+ (describe ":slot"
+ (it "opens left of others if lower"
+ (let ((first (display-buffer b))
+ (second (display-buffer a)))
+ (expect (length (+popup-windows)) :to-be 2)
+ (expect (window-in-direction 'left first t)
+ :to-equal second)))
+ (it "opens right of others if higher"
+ (let ((first (display-buffer a))
+ (second (display-buffer b)))
+ (expect (length (+popup-windows)) :to-be 2)
+ (expect (window-in-direction 'right first t)
+ :to-equal second)))
+ (it "obeys default :slot"
+ (let ((window (display-buffer g)))
+ (expect (window-parameter window 'window-slot) :to-be 1)
+ (expect (window-parameter window 'window-vslot) :to-be 1))))
+
+ (describe ":vslot"
+ ;; TODO Implement this, somehow
+ (xit "opens lower :vslot popups above others"
+ (let ((first (display-buffer c))
+ (second (display-buffer a)))
+ (expect (length (+popup-windows)) :to-be 2)
+ (expect (window-in-direction 'above first t)
+ :to-equal second)))
+ (it "opens higher :vslot popups below others"
+ (let ((first (display-buffer c))
+ (second (display-buffer e)))
+ (expect (length (+popup-windows)) :to-be 2)
+ (expect (window-in-direction 'below first t)
+ :to-equal second)))))
+
+ (describe ":select"
+ (it "selects the popup if non-nil"
+ (set-popup-rule! "^a$" :select t)
+ (display-buffer a)
+ (expect (current-buffer) :to-equal a))
+ (it "selects the originating window if nil"
+ (set-popup-rule! "^a$" :select nil)
+ (display-buffer a)
+ (expect (current-buffer) :to-equal origin))
+ (it "fall back to base selection if passed #'ignore"
+ (spy-on 'ignore)
+ (set-popup-rule! "^a$" :select #'ignore)
+ (save-window-excursion
+ (display-buffer a)
+ (expect (current-buffer) :to-equal origin))
+ (save-window-excursion
+ (pop-to-buffer a)
+ (expect (current-buffer) :to-equal a))
+ (expect 'ignore :to-have-been-called-times 2)))
+
+ (describe ":modeline"
+ (it "disables the mode-line if nil"
+ (set-popup-rule! "a" :modeline nil :select t)
+ (display-buffer a)
+ (expect mode-line-format :to-be nil))
+ (it "uses the default mode-line if t"
+ (set-popup-rule! "a" :modeline t :select t)
+ (display-buffer a)
+ (expect mode-line-format :to-equal (default-value 'mode-line-format)))
+ (it "uses a predefined mode-line if passed a symbol"
+ (set-popup-rule! "a" :modeline '("x") :select t)
+ (display-buffer a)
+ (expect mode-line-format :to-equal '("x")))
+ (it "runs the handler if passed a function"
+ (set-popup-rule! "a" :modeline (lambda () (setq mode-line-format '("x"))) :select t)
+ (display-buffer a)
+ (expect mode-line-format :to-equal '("x"))))
+
+ ;; TODO
+ (xdescribe ":autosave")
+
+ (describe ":quit"
+ (it "will close from anywhere if :quit = t"
+ (set-popup-rule! "a" :quit t)
+ (save-window-excursion
+ (display-buffer a)
+ (call-interactively #'+popup/close-all)
+ (expect (get-buffer-window a) :to-be nil))
+ (save-window-excursion
+ (pop-to-buffer a)
+ (call-interactively #'+popup/close)
+ (expect (get-buffer-window a) :to-be nil)))
+ (it "will only close from outside if :quit = 'other"
+ (set-popup-rule! "a" :quit 'other)
+ (save-window-excursion
+ (display-buffer a)
+ (call-interactively #'+popup/close-all)
+ (expect (get-buffer-window a) :to-be nil))
+ (save-window-excursion
+ (pop-to-buffer a)
+ (call-interactively #'+popup/close)
+ (expect (get-buffer-window a))))
+ (it "will only close from inside if :quit = 'current"
+ (set-popup-rule! "a" :quit 'current)
+ (save-window-excursion
+ (display-buffer a)
+ (call-interactively #'+popup/close-all)
+ (expect (get-buffer-window a)))
+ (save-window-excursion
+ (pop-to-buffer a)
+ (call-interactively #'+popup/close)
+ (expect (get-buffer-window a) :to-be nil)))
+ (it "never close a if :quit = nil"
+ (set-popup-rule! "a" :quit nil)
+ (save-window-excursion
+ (display-buffer a)
+ (call-interactively #'+popup/close-all)
+ (expect (get-buffer-window a)))
+ (save-window-excursion
+ (pop-to-buffer a)
+ (call-interactively #'+popup/close)
+ (expect (get-buffer-window a)))))
+
+ ;; TODO
+ (xdescribe ":ttl")
+ (xdescribe ":size")
+ (xdescribe ":width")
+ (xdescribe ":height")
+ (xdescribe ":side")
+ (xdescribe ":actions"))
+
+ ;; TODO
+ (xdescribe "predicate functions"
+ (describe "buffer-p")
+ (describe "window-p"))
+
+ ;; TODO
+ (xdescribe "save-popups!")
+ (xdescribe "with-popup-rules!"))
diff --git a/modules/ui/pretty-code/+fira.el b/modules/ui/pretty-code/+fira.el
new file mode 100644
index 000000000..24b2982af
--- /dev/null
+++ b/modules/ui/pretty-code/+fira.el
@@ -0,0 +1,122 @@
+;;; ui/pretty-code/+fira.el -*- lexical-binding: t; -*-
+
+(defvar +pretty-code-fira-code-font-name "Fira Code Symbol"
+ "Name of the fira code ligature font.")
+
+(defvar +pretty-code-fira-code-font-ligatures
+ '(("www" . #Xe100)
+ ("**" . #Xe101)
+ ("***" . #Xe102)
+ ("**/" . #Xe103)
+ ("*>" . #Xe104)
+ ("*/" . #Xe105)
+ ("\\\\" . #Xe106)
+ ("\\\\\\" . #Xe107)
+ ("{-" . #Xe108)
+ ("[]" . #Xe109)
+ ("::" . #Xe10a)
+ (":::" . #Xe10b)
+ (":=" . #Xe10c)
+ ("!!" . #Xe10d)
+ ("!=" . #Xe10e)
+ ("!==" . #Xe10f)
+ ("-}" . #Xe110)
+ ("--" . #Xe111)
+ ("---" . #Xe112)
+ ("-->" . #Xe113)
+ ("->" . #Xe114)
+ ("->>" . #Xe115)
+ ("-<" . #Xe116)
+ ("-<<" . #Xe117)
+ ("-~" . #Xe118)
+ ("#{" . #Xe119)
+ ("#[" . #Xe11a)
+ ("##" . #Xe11b)
+ ("###" . #Xe11c)
+ ("####" . #Xe11d)
+ ("#(" . #Xe11e)
+ ("#?" . #Xe11f)
+ ("#_" . #Xe120)
+ ("#_(" . #Xe121)
+ (".-" . #Xe122)
+ (".=" . #Xe123)
+ (".." . #Xe124)
+ ("..<" . #Xe125)
+ ("..." . #Xe126)
+ ("?=" . #Xe127)
+ ("??" . #Xe128)
+ (";;" . #Xe129)
+ ("/*" . #Xe12a)
+ ("/**" . #Xe12b)
+ ("/=" . #Xe12c)
+ ("/==" . #Xe12d)
+ ("/>" . #Xe12e)
+ ("//" . #Xe12f)
+ ("///" . #Xe130)
+ ("&&" . #Xe131)
+ ("||" . #Xe132)
+ ("||=" . #Xe133)
+ ("|=" . #Xe134)
+ ("|>" . #Xe135)
+ ("^=" . #Xe136)
+ ("$>" . #Xe137)
+ ("++" . #Xe138)
+ ("+++" . #Xe139)
+ ("+>" . #Xe13a)
+ ("=:=" . #Xe13b)
+ ("==" . #Xe13c)
+ ("===" . #Xe13d)
+ ("==>" . #Xe13e)
+ ("=>" . #Xe13f)
+ ("=>>" . #Xe140)
+ ("<=" . #Xe141)
+ ("=<<" . #Xe142)
+ ("=/=" . #Xe143)
+ (">-" . #Xe144)
+ (">=" . #Xe145)
+ (">=>" . #Xe146)
+ (">>" . #Xe147)
+ (">>-" . #Xe148)
+ (">>=" . #Xe149)
+ (">>>" . #Xe14a)
+ ("<*" . #Xe14b)
+ ("<*>" . #Xe14c)
+ ("<|" . #Xe14d)
+ ("<|>" . #Xe14e)
+ ("<$" . #Xe14f)
+ ("<$>" . #Xe150)
+ ("" . #Xe101)
+ ("<--->" . #Xe102)
+ ("<---->" . #Xe103)
+ ("<----->" . #Xe104)
+ ;; Double-ended equals arrows
+ ("<=>" . #Xe105)
+ ("<==>" . #Xe106)
+ ("<===>" . #Xe107)
+ ("<====>" . #Xe108)
+ ("<=====>" . #Xe109)
+ ;; Double-ended asterisk operators
+ ("<**>" . #Xe10a)
+ ("<***>" . #Xe10b)
+ ("<****>" . #Xe10c)
+ ("<*****>" . #Xe10d)
+ ;; HTML comments
+ ("" . #Xe152)
+ ("-->-" . #Xe153)
+ ("-->--" . #Xe154)
+ ("-->>" . #Xe155)
+ ("-->>-" . #Xe156)
+ ("-->>--" . #Xe157)
+ ("-->>>" . #Xe158)
+ ("-->>>-" . #Xe159)
+ ("-->>>--" . #Xe15a)
+ (">-" . #Xe15b)
+ (">--" . #Xe15c)
+ (">>-" . #Xe15d)
+ (">>--" . #Xe15e)
+ (">>>-" . #Xe15f)
+ (">>>--" . #Xe160)
+ ("=>" . #Xe161)
+ ("=>=" . #Xe162)
+ ("=>==" . #Xe163)
+ ("=>>" . #Xe164)
+ ("=>>=" . #Xe165)
+ ("=>>==" . #Xe166)
+ ("=>>>" . #Xe167)
+ ("=>>>=" . #Xe168)
+ ("=>>>==" . #Xe169)
+ ("==>" . #Xe16a)
+ ("==>=" . #Xe16b)
+ ("==>==" . #Xe16c)
+ ("==>>" . #Xe16d)
+ ("==>>=" . #Xe16e)
+ ("==>>==" . #Xe16f)
+ ("==>>>" . #Xe170)
+ ("==>>>=" . #Xe171)
+ ("==>>>==" . #Xe172)
+ (">=" . #Xe173)
+ (">==" . #Xe174)
+ (">>=" . #Xe175)
+ (">>==" . #Xe176)
+ (">>>=" . #Xe177)
+ (">>>==" . #Xe178)
+ ("<-" . #Xe179)
+ ("-<-" . #Xe17a)
+ ("--<-" . #Xe17b)
+ ("<<-" . #Xe17c)
+ ("-<<-" . #Xe17d)
+ ("--<<-" . #Xe17e)
+ ("<<<-" . #Xe17f)
+ ("-<<<-" . #Xe180)
+ ("--<<<-" . #Xe181)
+ ("<--" . #Xe182)
+ ("-<--" . #Xe183)
+ ("--<--" . #Xe184)
+ ("<<--" . #Xe185)
+ ("-<<--" . #Xe186)
+ ("--<<--" . #Xe187)
+ ("<<<--" . #Xe188)
+ ("-<<<--" . #Xe189)
+ ("--<<<--" . #Xe18a)
+ ("-<" . #Xe18b)
+ ("--<" . #Xe18c)
+ ("-<<" . #Xe18d)
+ ("--<<" . #Xe18e)
+ ("-<<<" . #Xe18f)
+ ("--<<<" . #Xe190)
+ ("<=" . #Xe191)
+ ("=<=" . #Xe192)
+ ("==<=" . #Xe193)
+ ("<<=" . #Xe194)
+ ("=<<=" . #Xe195)
+ ("==<<=" . #Xe196)
+ ("<<<=" . #Xe197)
+ ("=<<<=" . #Xe198)
+ ("==<<<=" . #Xe199)
+ ("<==" . #Xe19a)
+ ("=<==" . #Xe19b)
+ ("==<==" . #Xe19c)
+ ("<<==" . #Xe19d)
+ ("=<<==" . #Xe19e)
+ ("==<<==" . #Xe19f)
+ ("<<<==" . #Xe1a0)
+ ("=<<<==" . #Xe1a1)
+ ("==<<<==" . #Xe1a2)
+ ("=<" . #Xe1a3)
+ ("==<" . #Xe1a4)
+ ("=<<" . #Xe1a5)
+ ("==<<" . #Xe1a6)
+ ("=<<<" . #Xe1a7)
+ ("==<<<" . #Xe1a8)
+ ;; Monadic operators
+ (">=>" . #Xe1a9)
+ (">->" . #Xe1aa)
+ (">-->" . #Xe1ab)
+ (">==>" . #Xe1ac)
+ ("<=<" . #Xe1ad)
+ ("<-<" . #Xe1ae)
+ ("<--<" . #Xe1af)
+ ("<==<" . #Xe1b0)
+ ;; Composition operators
+ (">>" . #Xe1b1)
+ (">>>" . #Xe1b2)
+ ("<<" . #Xe1b3)
+ ("<<<" . #Xe1b4)
+ ;; Lens operators
+ (":+" . #Xe1b5)
+ (":-" . #Xe1b6)
+ (":=" . #Xe1b7)
+ ("+:" . #Xe1b8)
+ ("-:" . #Xe1b9)
+ ("=:" . #Xe1ba)
+ ("=^" . #Xe1bb)
+ ("=+" . #Xe1bc)
+ ("=-" . #Xe1bd)
+ ("=*" . #Xe1be)
+ ("=/" . #Xe1bf)
+ ("=%" . #Xe1c0)
+ ("^=" . #Xe1c1)
+ ("+=" . #Xe1c2)
+ ("-=" . #Xe1c3)
+ ("*=" . #Xe1c4)
+ ("/=" . #Xe1c5)
+ ("%=" . #Xe1c6)
+ ;; Logical
+ ("/\\" . #Xe1c7)
+ ("\\/" . #Xe1c8)
+ ;; Semigroup/monoid operators
+ ("<>" . #Xe1c9)
+ ("<+" . #Xe1ca)
+ ("<+>" . #Xe1cb)
+ ("+>" . #Xe1cc))
+ "Defines the character mappings for ligatures for Iosevka.")
+
+(defun +pretty-code|setup-iosevka-ligatures ()
+ (set-fontset-font t '(#Xe100 . #Xe16f) +pretty-code-iosevka-font-name)
+ (setq-default prettify-symbols-alist
+ (append prettify-symbols-alist
+ +pretty-code-iosevka-font-ligatures)))
+
+(add-hook 'doom-init-ui-hook #'+pretty-code|setup-iosevka-ligatures)
+
diff --git a/modules/ui/pretty-code/+pragmata-pro.el b/modules/ui/pretty-code/+pragmata-pro.el
new file mode 100644
index 000000000..eafa4cedd
--- /dev/null
+++ b/modules/ui/pretty-code/+pragmata-pro.el
@@ -0,0 +1,207 @@
+;;; ui/pretty-code/+pragmata-pro.el -*- lexical-binding: t; -*-
+
+(defvar +pretty-code-pragmata-pro-font-name "PragmataPro"
+ "Name of the Pragmata Pro ligature font.")
+
+(defvar +pretty-code-pragmata-pro-font-ligatures
+ '(("!!" . #XE900)
+ ("!=" . #XE901)
+ ("!==" . #XE902)
+ ("!!!" . #XE903)
+ ("!≡" . #XE904)
+ ("!≡≡" . #XE905)
+ ("!>" . #XE906)
+ ("!=<" . #XE907)
+ ("#(" . #XE920)
+ ("#_" . #XE921)
+ ("#{" . #XE922)
+ ("#?" . #XE923)
+ ("#>" . #XE924)
+ ("##" . #XE925)
+ ("#_(" . #XE926)
+ ("%=" . #XE930)
+ ("%>" . #XE931)
+ ("%>%" . #XE932)
+ ("%<%" . #XE933)
+ ("&%" . #XE940)
+ ("&&" . #XE941)
+ ("&*" . #XE942)
+ ("&+" . #XE943)
+ ("&-" . #XE944)
+ ("&/" . #XE945)
+ ("&=" . #XE946)
+ ("&&&" . #XE947)
+ ("&>" . #XE948)
+ ("$>" . #XE955)
+ ("***" . #XE960)
+ ("*=" . #XE961)
+ ("*/" . #XE962)
+ ("*>" . #XE963)
+ ("++" . #XE970)
+ ("+++" . #XE971)
+ ("+=" . #XE972)
+ ("+>" . #XE973)
+ ("++=" . #XE974)
+ ("--" . #XE980)
+ ("-<" . #XE981)
+ ("-<<" . #XE982)
+ ("-=" . #XE983)
+ ("->" . #XE984)
+ ("->>" . #XE985)
+ ("---" . #XE986)
+ ("-->" . #XE987)
+ ("-+-" . #XE988)
+ ("-\\/" . #XE989)
+ ("-|>" . #XE98A)
+ ("-<|" . #XE98B)
+ (".." . #XE990)
+ ("..." . #XE991)
+ ("..<" . #XE992)
+ (".>" . #XE993)
+ (".~" . #XE994)
+ (".=" . #XE995)
+ ("/*" . #XE9A0)
+ ("//" . #XE9A1)
+ ("/>" . #XE9A2)
+ ("/=" . #XE9A3)
+ ("/==" . #XE9A4)
+ ("///" . #XE9A5)
+ ("/**" . #XE9A6)
+ (":::" . #XE9AF)
+ ("::" . #XE9B0)
+ (":=" . #XE9B1)
+ (":≡" . #XE9B2)
+ (":>" . #XE9B3)
+ (":=>" . #XE9B4)
+ (":(" . #XE9B5)
+ (":-(" . #XE9B6)
+ (":)" . #XE9B7)
+ (":-)" . #XE9B8)
+ (":/" . #XE9B9)
+ (":\\" . #XE9BA)
+ (":3" . #XE9BB)
+ (":D" . #XE9BC)
+ (":P" . #XE9BD)
+ (":>:" . #XE9BE)
+ (":<:" . #XE9BF)
+ ("<$>" . #XE9C0)
+ ("<*" . #XE9C1)
+ ("<*>" . #XE9C2)
+ ("<+>" . #XE9C3)
+ ("<-" . #XE9C4)
+ ("<<" . #XE9C5)
+ ("<<<" . #XE9C6)
+ ("<<=" . #XE9C7)
+ ("<=" . #XE9C8)
+ ("<=>" . #XE9C9)
+ ("<>" . #XE9CA)
+ ("<|>" . #XE9CB)
+ ("<<-" . #XE9CC)
+ ("<|" . #XE9CD)
+ ("<=<" . #XE9CE)
+ ("<~" . #XE9CF)
+ ("<~~" . #XE9D0)
+ ("<<~" . #XE9D1)
+ ("<$" . #XE9D2)
+ ("<+" . #XE9D3)
+ ("" . #XE9D4)
+ ("<@>" . #XE9D5)
+ ("<#>" . #XE9D6)
+ ("<%>" . #XE9D7)
+ ("<^>" . #XE9D8)
+ ("<&>" . #XE9D9)
+ (">" . #XE9DA)
+ ("<.>" . #XE9DB)
+ (">" . #XE9DC)
+ ("<\\>" . #XE9DD)
+ ("<\">" . #XE9DE)
+ ("<:>" . #XE9DF)
+ ("<~>" . #XE9E0)
+ ("<**>" . #XE9E1)
+ ("<<^" . #XE9E2)
+ ("" . #XE9EF)
+ ("" . #XEA66)
+ ("|=>" . #XEA67)
+ ("|==>" . #XEA68)
+ ("|>-" . #XEA69)
+ ("|<<" . #XEA6A)
+ ("||>" . #XEA6B)
+ ("|>>" . #XEA6C)
+ ("~=" . #XEA70)
+ ("~>" . #XEA71)
+ ("~~>" . #XEA72)
+ ("~>>" . #XEA73)
+ ("[[" . #XEA80)
+ ("]]" . #XEA81)
+ ("\">" . #XEA90))
+ "Defines the character mappings for ligatures for Pragmata Pro.")
+
+(defun +pretty-code|setup-pragmata-pro-ligatures ()
+ (setq-default prettify-symbols-alist
+ (append prettify-symbols-alist
+ (mapcar #'+pretty-code--correct-symbol-bounds
+ +pretty-code-pragmata-pro-font-ligatures))))
+
+(add-hook 'doom-init-ui-hook #'+pretty-code|setup-pragmata-pro-ligatures)
diff --git a/modules/ui/pretty-code/autoload.el b/modules/ui/pretty-code/autoload.el
new file mode 100644
index 000000000..4ad18fe69
--- /dev/null
+++ b/modules/ui/pretty-code/autoload.el
@@ -0,0 +1,107 @@
+;;; ui/pretty-code/settings.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(defvar +pretty-code-symbols
+ '(;; org
+ :name "»"
+ :src_block "»"
+ :src_block_end "«"
+ ;; Functional
+ :lambda "λ"
+ :def "ƒ"
+ :composition "∘"
+ :map "↦"
+ ;; Types
+ :null "∅"
+ :true "𝕋"
+ :false "𝔽"
+ :int "ℤ"
+ :float "ℝ"
+ :str "𝕊"
+ :bool "𝔹"
+ ;; Flow
+ :not "¬"
+ :in "∈"
+ :not-in "∉"
+ :and "∧"
+ :or "∨"
+ :for "∀"
+ :some "∃"
+ :return "⟼"
+ :yield "⟻"
+ ;; Other
+ :tuple "⨂"
+ :pipe "" ;; FIXME: find a non-private char
+ :dot "•")
+ "Options plist for `set-pretty-symbols!'.
+
+This should not contain any symbols from the Unicode Private Area! There is no
+universal way of getting the correct symbol as that area varies from font to
+font.")
+
+;;;###autoload
+(defvar +pretty-code-symbols-alist '((t))
+ "An alist containing a mapping of major modes to its value for
+`prettify-symbols-alist'.")
+
+;;;###autodef
+(defun +pretty-code--correct-symbol-bounds (ligature-alist)
+ "Prepend non-breaking spaces to a ligature.
+
+This way `compose-region' (called by `prettify-symbols-mode') will use the
+correct width of the symbols instead of the width measured by `char-width'."
+ (let ((len (length (car ligature-alist)))
+ (acc (list (cdr ligature-alist))))
+ (while (> len 1)
+ (setq acc (cons #X00a0 (cons '(Br . Bl) acc))
+ len (1- len)))
+ (cons (car ligature-alist) acc)))
+
+;;;###autodef
+(defun set-pretty-symbols! (modes &rest plist)
+ "Associates string patterns with icons in certain major-modes.
+
+ MODES is a major mode symbol or a list of them.
+ PLIST is a property list whose keys must match keys in `+pretty-code-symbols',
+and whose values are strings representing the text to be replaced with that
+symbol. If the car of PLIST is nil, then unset any pretty symbols previously
+defined for MODES.
+
+The following properties are special:
+
+ :alist ALIST
+ Appends ALIST to `prettify-symbols-alist' literally, without mapping text to
+ `+pretty-code-symbols'.
+ :merge BOOL
+ If non-nil, merge with previously defined `prettify-symbols-alist',
+ otherwise overwrite it.
+
+For example, the rule for emacs-lisp-mode is very simple:
+
+ (set-pretty-symbols! 'emacs-lisp-mode
+ :lambda \"lambda\")
+
+This will replace any instances of \"lambda\" in emacs-lisp-mode with the symbol
+assicated with :lambda in `+pretty-code-symbols'.
+
+Pretty symbols can be unset for emacs-lisp-mode with:
+
+ (set-pretty-symbols! 'emacs-lisp-mode nil)"
+ (declare (indent defun))
+ (if (null (car-safe plist))
+ (dolist (mode (doom-enlist modes))
+ (delq (assq mode +pretty-code-symbols-alist)
+ +pretty-code-symbols-alist))
+ (let (results merge key)
+ (while plist
+ (pcase (setq key (pop plist))
+ (:merge (setq merge (pop plist)))
+ (:alist (setq results (append (pop plist) results)))
+ (_
+ (when-let* ((char (plist-get +pretty-code-symbols key)))
+ (push (cons (pop plist) char) results)))))
+ (dolist (mode (doom-enlist modes))
+ (unless merge
+ (delq (assq mode +pretty-code-symbols-alist)
+ +pretty-code-symbols-alist))
+ (push (cons mode results) +pretty-code-symbols-alist)))))
diff --git a/modules/ui/pretty-code/config.el b/modules/ui/pretty-code/config.el
new file mode 100644
index 000000000..4583f0863
--- /dev/null
+++ b/modules/ui/pretty-code/config.el
@@ -0,0 +1,40 @@
+;;; ui/pretty-code/config.el -*- lexical-binding: t; -*-
+
+(cond ((featurep! +fira)
+ (load! "+fira"))
+ ((featurep! +iosevka)
+ (load! "+iosevka"))
+ ((featurep! +pragmata-pro)
+ (load! "+pragmata-pro")))
+
+(defvar +pretty-code-enabled-modes t
+ "List of major modes in which `prettify-symbols-mode' should be enabled.
+If t, enable it everywhere. If the first element is 'not, enable it in any mode
+besides what is listed.")
+
+;; When you get to the right edge, it goes back to how it normally prints
+(setq prettify-symbols-unprettify-at-point 'right-edge)
+
+(defun +pretty-code|init-pretty-symbols ()
+ "Enable `prettify-symbols-mode'.
+
+If in fundamental-mode, or a mode derived from special, comint, eshell or term
+modes, this function does nothing.
+
+Otherwise it builds `prettify-code-symbols-alist' according to
+`+pretty-code-symbols-alist' for the current major-mode."
+ (unless (or (eq major-mode 'fundamental-mode)
+ (eq (get major-mode 'mode-class) 'special)
+ (derived-mode-p 'comint-mode 'eshell-mode 'term-mode))
+ (when (or (eq +pretty-code-enabled-modes t)
+ (if (eq (car +pretty-code-enabled-modes) 'not)
+ (not (memq major-mode (cdr +pretty-code-enabled-modes)))
+ (memq major-mode +pretty-code-enabled-modes)))
+ (setq prettify-symbols-alist
+ (append (cdr (assq major-mode +pretty-code-symbols-alist))
+ (default-value 'prettify-symbols-alist)))
+ (when prettify-symbols-mode
+ (prettify-symbols-mode -1))
+ (prettify-symbols-mode +1))))
+
+(add-hook 'after-change-major-mode-hook #'+pretty-code|init-pretty-symbols)
diff --git a/modules/ui/tabbar/config.el b/modules/ui/tabbar/config.el
index bb694c5c4..6d3eb5111 100644
--- a/modules/ui/tabbar/config.el
+++ b/modules/ui/tabbar/config.el
@@ -5,15 +5,15 @@
;; find ivy (or helm) or even `buffer-menu' is better suited for this purpose.
(def-package! tabbar
+ :hook (doom-init-ui . tabbar-mode)
:config
(setq tabbar-use-images nil)
- (tabbar-mode)
(defun +tabbar|disable-in-popups ()
- (when tabbar-mode
+ (when (and +popup-buffer-mode tabbar-mode)
(tabbar-local-mode -1)
(setq-local header-line-format nil)))
- (add-hook 'doom-popup-mode-hook #'+tabbar|disable-in-popups)
+ (add-hook '+popup-buffer-mode-hook #'+tabbar|disable-in-popups)
(defun +tabbar-display-tab (tab)
"Return a label for TAB that resembles tabs in Atom."
diff --git a/modules/ui/treemacs/autoload.el b/modules/ui/treemacs/autoload.el
new file mode 100644
index 000000000..6f8b75a9c
--- /dev/null
+++ b/modules/ui/treemacs/autoload.el
@@ -0,0 +1,41 @@
+;;; ui/treemacs/autoload.el -*- lexical-binding: t; -*-
+
+(defun +treemacs--init ()
+ (require 'treemacs)
+ (let ((origin-buffer (current-buffer)))
+ (cl-letf (((symbol-function 'treemacs-workspace->is-empty?)
+ (symbol-function 'ignore)))
+ (treemacs--init))
+ (dolist (project (treemacs-workspace->projects (treemacs-current-workspace)))
+ (treemacs-do-remove-project-from-workspace project))
+ (with-current-buffer origin-buffer
+ (let ((project-root (or (doom-project-root) default-directory)))
+ (treemacs-do-add-project-to-workspace
+ (treemacs--canonical-path project-root)
+ (doom-project-name project-root)))
+ (setq treemacs--ready-to-follow t)
+ (when (or treemacs-follow-after-init treemacs-follow-mode)
+ (treemacs--follow)))))
+
+;;;###autoload
+(defun +treemacs/toggle ()
+ "Initialize or toggle treemacs.
+
+Ensures that only the current project is present and all other projects have
+been removed.
+
+Use `treemacs' command for old functionality."
+ (interactive)
+ (require 'treemacs)
+ (pcase (treemacs-current-visibility)
+ (`visible (delete-window (treemacs-get-local-window)))
+ (_ (+treemacs--init))))
+
+;;;###autoload
+(defun +treemacs/find-file (arg)
+ "Open treemacs (if necessary) and find current file."
+ (interactive "P")
+ (let ((origin-buffer (current-buffer)))
+ (+treemacs--init)
+ (with-current-buffer origin-buffer
+ (treemacs-find-file arg))))
diff --git a/modules/ui/treemacs/config.el b/modules/ui/treemacs/config.el
new file mode 100644
index 000000000..b45958886
--- /dev/null
+++ b/modules/ui/treemacs/config.el
@@ -0,0 +1,41 @@
+;;; ui/treemacs/config.el -*- lexical-binding: t; -*-
+
+(setq treemacs-follow-after-init t
+ treemacs-is-never-other-window t
+ treemacs-sorting 'alphabetic-case-insensitive-desc
+ treemacs-persist-file (concat doom-cache-dir "treemacs-persist"))
+
+(after! treemacs-persistence
+ ;; This variable is defined with defconst, so we must wait to change it until
+ ;; it has loaded.
+ (setq treemacs--last-error-persist-file
+ (concat doom-cache-dir
+ "treemacs-persist-at-last-error")))
+
+
+(after! treemacs
+ (set-popup-rule! "^ \\*Treemacs"
+ :side treemacs-position
+ :size treemacs-width
+ :quit nil
+ :ttl 0)
+
+ ;; Don't follow the cursor
+ (treemacs-follow-mode -1)
+
+ (after! ace-window
+ (setq aw-ignored-buffers (delq 'treemacs-mode aw-ignored-buffers))))
+
+
+(def-package! treemacs-evil
+ :when (featurep! :feature evil +everywhere)
+ :after treemacs
+ :config
+ (define-key! evil-treemacs-state-map
+ [return] #'treemacs-RET-action
+ [tab] #'treemacs-TAB-action
+ "TAB" #'treemacs-TAB-action))
+
+
+(def-package! treemacs-projectile
+ :after treemacs)
diff --git a/modules/ui/treemacs/packages.el b/modules/ui/treemacs/packages.el
new file mode 100644
index 000000000..8d203580f
--- /dev/null
+++ b/modules/ui/treemacs/packages.el
@@ -0,0 +1,7 @@
+;; -*- no-byte-compile: t; -*-
+;;; ui/treemacs/packages.el
+
+(package! treemacs)
+(when (featurep! :feature evil +everywhere)
+ (package! treemacs-evil))
+(package! treemacs-projectile)
diff --git a/modules/ui/unicode/autoload.el b/modules/ui/unicode/autoload.el
new file mode 100644
index 000000000..0be6bd27e
--- /dev/null
+++ b/modules/ui/unicode/autoload.el
@@ -0,0 +1,23 @@
+;;; ui/unicode/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(add-hook 'doom-init-ui-hook #'+unicode|init-fonts)
+
+;;;###autoload
+(defun +unicode|init-fonts ()
+ "Set up `unicode-fonts' to eventually run; accomodating the daemon, if
+necessary."
+ (setq-default bidi-display-reordering t
+ doom-unicode-font nil)
+ (if initial-window-system
+ (+unicode|setup-fonts (selected-frame))
+ (add-hook 'after-make-frame-functions #'+unicode|setup-fonts)))
+
+;;;###autoload
+(defun +unicode|setup-fonts (&optional frame)
+ "Initialize `unicode-fonts', if in a GUI session."
+ (when (and frame (display-graphic-p frame))
+ (with-selected-frame frame
+ (require 'unicode-fonts)
+ ;; NOTE will impact startup time on first run
+ (unicode-fonts-setup))))
diff --git a/modules/ui/unicode/config.el b/modules/ui/unicode/config.el
deleted file mode 100644
index 06636b3e9..000000000
--- a/modules/ui/unicode/config.el
+++ /dev/null
@@ -1,19 +0,0 @@
-;;; ui/unicode/config.el -*- lexical-binding: t; -*-
-
-(def-package! unicode-fonts
- :init
- (setq-default bidi-display-reordering t
- doom-unicode-font nil)
-
- (defun +unicode|init-fonts (&optional frame)
- "Initialize `unicode-fonts', if in a GUI session."
- (when (and frame (display-graphic-p frame))
- (with-selected-frame frame
- (require 'unicode-fonts)
- ;; NOTE will impact startup time on first run
- (unicode-fonts-setup))))
-
- (add-hook! 'after-init-hook
- (if initial-window-system
- (+unicode|init-fonts)
- (add-hook 'after-make-frame-functions #'+unicode|init-fonts))))
diff --git a/modules/ui/vc-gutter/autoload.el b/modules/ui/vc-gutter/autoload.el
new file mode 100644
index 000000000..bac2c9e90
--- /dev/null
+++ b/modules/ui/vc-gutter/autoload.el
@@ -0,0 +1,26 @@
+;;; ui/vc-gutter/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload (autoload '+vc-gutter-hydra/body "ui/vc-gutter/autoload" nil t)
+(defhydra +vc-gutter-hydra
+ (:body-pre (git-gutter-mode 1) :hint nil)
+ "
+ [git gutter]
+ Movement Hunk Actions Misc. +%-4s(car (git-gutter:statistic))/ -%-4s(cdr (git-gutter:statistic))
+ ╭──────────────────────────────────┴────────────────╯
+ ^_g_^ [_s_] stage [_R_] set start Rev
+ ^_k_^ [_r_] revert
+ ^↑ ^ [_m_] mark
+ ^↓ ^ [_p_] popup ╭─────────────────────
+ ^_j_^ │[_q_] quit
+ ^_G_^ │[_Q_] Quit and disable"
+ ("j" (progn (git-gutter:next-hunk 1) (recenter)))
+ ("k" (progn (git-gutter:previous-hunk 1) (recenter)))
+ ("g" (progn (goto-char (point-min)) (git-gutter:next-hunk 1)))
+ ("G" (progn (goto-char (point-min)) (git-gutter:previous-hunk 1)))
+ ("s" git-gutter:stage-hunk)
+ ("r" git-gutter:revert-hunk)
+ ("m" git-gutter:mark-hunk)
+ ("p" git-gutter:popup-hunk)
+ ("R" git-gutter:set-start-revision)
+ ("q" nil :color blue)
+ ("Q" (git-gutter-mode -1) :color blue))
diff --git a/modules/ui/vc-gutter/config.el b/modules/ui/vc-gutter/config.el
new file mode 100644
index 000000000..daed7ec42
--- /dev/null
+++ b/modules/ui/vc-gutter/config.el
@@ -0,0 +1,82 @@
+;;; ui/vc-gutter/config.el -*- lexical-binding: t; -*-
+
+(defvar +vc-gutter-in-margin nil
+ "If non-nil, use the margin for diffs instead of the fringe.")
+
+(defvar +vc-gutter-in-remote-files nil
+ "If non-nil, enable the vc gutter in remote files (e.g. open through TRAMP).")
+
+(defvar +vc-gutter-diff-unsaved-buffer nil
+ "If non-nil, `diff-hl-flydiff-mode' will be activated. This allows on-the-fly
+diffing, even for unsaved buffers.")
+
+(defvar +vc-gutter-default-style t
+ "If non-nil, enable the default look of the vc gutter. This means subtle thin
+bitmaps on the left, an arrow bitmap for flycheck, and flycheck indicators moved
+to the right fringe.")
+
+
+;;
+;; Packages
+
+(def-package! git-gutter
+ :commands (git-gutter:revert-hunk git-gutter:stage-hunk)
+ :init
+ (defun +version-control|git-gutter-maybe ()
+ "Enable `git-gutter-mode' in non-remote buffers."
+ (when (and buffer-file-name
+ (or +vc-gutter-in-remote-files
+ (not (file-remote-p buffer-file-name)))
+ (vc-backend buffer-file-name))
+ (if (display-graphic-p)
+ (progn
+ (require 'git-gutter-fringe)
+ (setq-local git-gutter:init-function #'git-gutter-fr:init)
+ (setq-local git-gutter:view-diff-function #'git-gutter-fr:view-diff-infos)
+ (setq-local git-gutter:clear-function #'git-gutter-fr:clear)
+ (setq-local git-gutter:window-width -1))
+ (setq-local git-gutter:init-function 'nil)
+ (setq-local git-gutter:view-diff-function #'git-gutter:view-diff-infos)
+ (setq-local git-gutter:clear-function #'git-gutter:clear-diff-infos)
+ (setq-local git-gutter:window-width 1))
+ (git-gutter-mode +1)))
+ (add-hook! (text-mode prog-mode conf-mode)
+ #'+version-control|git-gutter-maybe)
+ ;; standardize default fringe width
+ (if (fboundp 'fringe-mode) (fringe-mode '4))
+ :config
+ (set-popup-rule! "^\\*git-gutter" :select nil)
+
+ ;; Update git-gutter on focus (in case I was using git externally)
+ (add-hook 'focus-in-hook #'git-gutter:update-all-windows)
+
+ (defun +version-control|update-git-gutter (&rest _)
+ "Refresh git-gutter on ESC. Return nil to prevent shadowing other
+`doom-escape-hook' hooks."
+ (when git-gutter-mode
+ (ignore (git-gutter))))
+ (add-hook 'doom-escape-hook #'+version-control|update-git-gutter t)
+
+ ;; update git-gutter when using magit commands
+ (advice-add #'magit-stage-file :after #'+version-control|update-git-gutter)
+ (advice-add #'magit-unstage-file :after #'+version-control|update-git-gutter))
+
+
+;; subtle diff indicators in the fringe
+(when +vc-gutter-default-style
+ (after! git-gutter-fringe
+ ;; places the git gutter outside the margins.
+ (setq-default fringes-outside-margins t)
+ ;; thin fringe bitmaps
+ (define-fringe-bitmap 'git-gutter-fr:added [224]
+ nil nil '(center repeated))
+ (define-fringe-bitmap 'git-gutter-fr:modified [224]
+ nil nil '(center repeated))
+ (define-fringe-bitmap 'git-gutter-fr:deleted [128 192 224 240]
+ nil nil 'bottom)
+ ;; let diff have left fringe, flycheck can have right fringe
+ (after! flycheck
+ (setq flycheck-indication-mode 'right-fringe)
+ ;; A non-descript, left-pointing arrow
+ (define-fringe-bitmap 'flycheck-fringe-bitmap-double-arrow
+ [16 48 112 240 112 48 16] nil nil 'center))))
diff --git a/modules/ui/vc-gutter/packages.el b/modules/ui/vc-gutter/packages.el
new file mode 100644
index 000000000..a12b41b8f
--- /dev/null
+++ b/modules/ui/vc-gutter/packages.el
@@ -0,0 +1,4 @@
+;; -*- no-byte-compile: t; -*-
+;;; ui/vc-gutter/packages.el
+
+(package! git-gutter-fringe)
diff --git a/modules/ui/vi-tilde-fringe/autoload.el b/modules/ui/vi-tilde-fringe/autoload.el
new file mode 100644
index 000000000..ab9029cfc
--- /dev/null
+++ b/modules/ui/vi-tilde-fringe/autoload.el
@@ -0,0 +1,4 @@
+;;; ui/vi-tilde-fringe/autoload.el -*- lexical-binding: t; -*-
+
+;;;###autoload
+(add-hook! (prog-mode text-mode conf-mode) #'vi-tilde-fringe-mode)
diff --git a/modules/ui/vi-tilde-fringe/config.el b/modules/ui/vi-tilde-fringe/config.el
deleted file mode 100644
index 25ec00771..000000000
--- a/modules/ui/vi-tilde-fringe/config.el
+++ /dev/null
@@ -1,11 +0,0 @@
-;;; ui/vi-tilde-fringe/config.el -*- lexical-binding: t; -*-
-
-;; indicators for empty lines past EOF
-(def-package! vi-tilde-fringe
- :commands vi-tilde-fringe-mode
- :hook (doom-init-ui . global-vi-tilde-fringe-mode)
- :config
- (add-hook! +doom-dashboard-mode
- (when (bound-and-true-p vi-tilde-fringe-mode)
- (vi-tilde-fringe-mode -1))))
-
diff --git a/modules/ui/window-select/README.org b/modules/ui/window-select/README.org
index 57cba1db9..9d70524b2 100644
--- a/modules/ui/window-select/README.org
+++ b/modules/ui/window-select/README.org
@@ -2,22 +2,44 @@
This module provides the user with a visual way to switch windows. By default, the module only kicks in if there are more than two windows open at the same time.
-* Keybindings
-The default function to switch windows is automatically changed to work with this module. More specifically, the function ~other-window~ is automatically remapped. This function is bounded by default to ~C-x o~.
+* Table of Contents :TOC:
+- [[#install][Install]]
+ - [[#module-flags][Module flags]]
+- [[#usage][Usage]]
+- [[#configuration][Configuration]]
+ - [[#ace-window][ace-window]]
+ - [[#switch-window][switch-window]]
-* Customization
-There are two packages that accomplish the same but with a different visual queue.
+* Install
+Add =:feature lookup= to your init.el.
+
+** Module flags
+This module provides two flags:
+
++ ~+ace-window~ Use avy (and ace-window) as the backend.
++ ~+switch-window~ Use switch-window as the backend.
+
+If neither flag is specified, ~+ace-window~ will be used.
+
+* Usage
+This module remaps the function ~other-window~ to either ~switch-window~ or
+~ace-window~, depending on which backend you've enabled.
+
+~other-window~ is bound to ~C-x o~ and ~SPC w o~.
+
+* Configuration
+This module provides two backends, both providing the same functionality, but
+with different visual cues. They are =ace-window= and =switch-window=.
** ace-window
-The first character of the buffers change to a user selectable character.
+The first character of the buffers changes to a highlighted, user-selectable
+character.
+
+ Pros: The content of the buffers are always visible.
+ Cons: The displayed characters are small and difficult to see.
-This is the module's default if no module flag is specified. To explicitly enable this, write in *init.el*: ~(doom! :ui (window-select +ace-window))~
-
** switch-window
-The entire buffer is replaced by big letters than enable the user to easily select a desired window.
- + Pros: The displayed characters are easy to see. They will never be missed.
- + Cons: The contents of the buffers is entirely replaced by a big character.
+Replaces the entire buffer with large letters.
-To enable this, write in *init.el*: ~(doom! :ui (window-select +switch-window))~
+ + Pros: The displayed characters are /really/ easy to see.
+ + Cons: You can't see the contents of the buffers.
diff --git a/modules/ui/window-select/config.el b/modules/ui/window-select/config.el
index 4436b4fa6..3bc5c032c 100644
--- a/modules/ui/window-select/config.el
+++ b/modules/ui/window-select/config.el
@@ -1,24 +1,21 @@
;;; ui/window-select/config.el -*- lexical-binding: t; -*-
-(cond ((featurep! +switch-window)
- ;; Configure switch-window if that flag is found
- (def-package! switch-window
- :commands (switch-window switch-window-then-maximize switch-window-then-split-below
- switch-window-then-split-right switch-window-then-delete
- switch-window-then-swap-buffer)
- :init
- (setq switch-window-shortcut-style 'qwerty
- switch-window-qwerty-shortcuts '("a" "s" "d" "f" "g" "h" "j" "k" "l"))
- ;; Redefine how we switch windows with switch-window
- (define-key global-map [remap other-window] #'switch-window)))
- ((or (featurep! +ace-window) t)
- ;; Configure ace-window if that flag or no flag is found
- (def-package! ace-window
- :commands (ace-window ace-swap-window ace-delete-window
- ace-select-window ace-delete-other-windows)
- :init
- (define-key global-map [remap other-window] #'ace-window)
- :config
- (setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l)
- aw-scope 'frame
- aw-background t))))
+(def-package! switch-window
+ :when (featurep! +switch-window)
+ :defer t
+ :init
+ (global-set-key [remap other-window] #'switch-window)
+ :config
+ (setq switch-window-shortcut-style 'qwerty
+ switch-window-qwerty-shortcuts '("a" "s" "d" "f" "g" "h" "j" "k" "l")))
+
+
+(def-package! ace-window
+ :unless (featurep! +switch-window)
+ :defer t
+ :init
+ (global-set-key [remap other-window] #'ace-window)
+ :config
+ (setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l)
+ aw-scope 'frame
+ aw-background t))
diff --git a/modules/ui/window-select/packages.el b/modules/ui/window-select/packages.el
index fc1c5fd9f..30a4d7a92 100644
--- a/modules/ui/window-select/packages.el
+++ b/modules/ui/window-select/packages.el
@@ -2,10 +2,11 @@
;;; ui/window-select/packages.el
(cond ((featurep! +switch-window)
- ;; Install switch-window if the user indicated the '+switch-window' module flag
+ ;; Install switch-window if the user indicated the '+switch-window'
+ ;; module flag
(package! switch-window))
((or (featurep! +ace-window) t)
- ;; Install ace-window if the user selects the flag '+ace-window' or by default
- ;; ... if the user did not specify a module flag
+ ;; Install ace-window if the user selects the flag '+ace-window' or by
+ ;; default ... if the user did not specify a module flag
(package! ace-window)))