diff options
-rw-r--r-- | .gitmodules | 3 | ||||
m--------- | catppuccin/emacs | 0 | ||||
-rw-r--r-- | emacs-init.el | 461 | ||||
-rw-r--r-- | home-configuration.scm | 80 |
4 files changed, 539 insertions, 5 deletions
diff --git a/.gitmodules b/.gitmodules index acaec0d0..89e8ef65 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "catppuccin/polybar"] path = catppuccin/polybar url = https://github.com/catppuccin/polybar +[submodule "catppuccin/emacs"] + path = catppuccin/emacs + url = https://github.com/catppuccin/emacs diff --git a/catppuccin/emacs b/catppuccin/emacs new file mode 160000 +Subproject b4be30de73aa295ab56a20c21dff07fba3dec05 diff --git a/emacs-init.el b/emacs-init.el new file mode 100644 index 00000000..5b31082e --- /dev/null +++ b/emacs-init.el @@ -0,0 +1,461 @@ +;;; init.el --- Emacs configuration. -*- lexical-binding: t -*- +;;; Commentary: +;;; Code: + +(defun tw/xdg-emacs-subdir (type name) + "Get the name of a subdirectory called NAME under $XDG_<TYPE>_HOME/emacs." + (expand-file-name + (concat (string-trim-right name "/") "/") + (expand-file-name + "emacs/" (pcase type + ('cache (or (getenv "XDG_CACHE_HOME") "~/.cache/")) + ('config (or (getenv "XDG_CONFIG_HOME") "~/.config/")) + ('data (or (getenv "XDG_DATA_HOME") "~/.local/share/")) + ;; The following two are Guix/GuixSD extensions. + ('log (or (getenv "XDG_LOG_HOME") "~/.local/var/log/")) + ('state (or (getenv "XDG_STATE_HOME") "~/.local/var/lib/")) + (_ (error "Unknown XDG directory type: %S" type)))))) + +;; Global/built-in Custom settings +;; Apply these as early as possible so that e.g. the native-comp files go to the right place. +(mapc (apply-partially #'apply #'customize-set-variable) + `((native-comp-async-report-warnings-errors silent "Don't pop up Warnings buffer for native compilation.") + (native-compile-target-directory ,(tw/xdg-emacs-subdir 'cache "eln") "Put native-compiled binaries in the cache.") + (inhibit-startup-screen t "Don't show the startup screen with help links.") + (indent-tabs-mode nil "Always use spaces to indent.") + (menu-bar-mode nil "Hide the menu bar globally.") + (tool-bar-mode nil "Hide the tool bar globally.") + (tooltip-mode nil "Show tooltips in the echo area instead.") + (global-hl-line-mode t "Highlight the current line in all buffers.") + (column-number-mode t "Show the column number in the statusline.") + (backup-directory-alist (("." . ,(tw/xdg-emacs-subdir 'data "backup"))) "Save all backup files in one place to avoid clutter.") + (scroll-up-aggressively 0.0 "Don't recenter the window if the point moves off the page.") + (scroll-down-aggressively 0.0 "Don't recenter the window if the point moves off the page.") + (package-archives nil "Don't fetch packages from the internet; only get them from Guix.") + (recentf-max-saved-items 10000 "Save lots of recently-opened files."))) + +(defalias 'yes-or-no-p #'y-or-n-p + "Always use `y-or-n-p' when asking for confirmation.") + +;; Custom modes depending on file names. +(mapc (apply-partially #'add-to-list 'auto-mode-alist) + `((,(rx (or bos "/") "PKGBUILD" eos) . sh-mode) + (,(rx ".install" eos) . sh-mode) + (,(rx bos "/tmp/neomutt-") . mail-mode) + (,(rx "." (1+ anything) "rc" eos) . conf-unix-mode) + (,(rx ".gnuplot" eos) . gnuplot-mode) + (,(rx ".aurora" eos) . python-mode))) + +;; Load settings set through Custom. +;; (setq custom-file (locate-user-emacs-file "custom.el")) +;; (when (file-readable-p custom-file) +;; (load custom-file)) + +;; `use-package' requirements. +(require 'package) +(package-initialize t) +(eval-when-compile + (require 'use-package)) +(use-package diminish) ; for using :diminish later +(use-package bind-key) ; for using :bind later + +;; Look and feel +(set-face-attribute 'default nil :family "Hermit" :height 100) + +(use-package autothemer + ;; The "catppuccin" theme is linked to the appropriate variant by guix home. + :config (load-theme 'catppuccin-mocha t)) + +(defun tw/get-catppuccin-color (name) + "Get the hex code of the Catppuccin color named NAME." + (cl-loop for color in (autothemer--theme-colors autothemer-current-theme) + when (string= (autothemer--color-name color) name) + return (autothemer--color-value color))) + +(custom-set-faces + `(cursor ((default :background ,(tw/get-catppuccin-color "rosewater"))) t + "Make the cursor \"rosewater\", as recommended by Catppuccin upstream, overriding catppuccin/emacs.")) + +;; Some packages below have `:commands (...) :demand t'. +;; We need :commands for the byte-compiler, but we want to load the package immediately. + +;; General editor behaviour. +(use-package ivy + :commands (ivy-mode) :demand t + :config (ivy-mode +1) + :diminish ivy-mode) + +(use-package counsel ; extra niceties for `ivy-mode' + :after (ivy evil) ; evil for :bind-ing to <leader> + :bind (("<leader>SPC" . counsel-M-x) ; <leader><leader> doesn't work + ("<leader>fr" . counsel-buffer-or-recentf) + :map evil-visual-state-map + ("<leader>SPC" . counsel-M-x)) + :commands (counsel-mode) :demand t + :config (counsel-mode +1) + :diminish counsel-mode) + +(use-package rainbow-mode + :after (evil) + :bind (("<leader>tR" . rainbow-mode))) + +(use-package linum + :hook (prog-mode . linum-mode)) + +(use-package which-key + :commands (which-key-mode) :demand t + :config (which-key-mode +1) + :diminish which-key-mode) + +(use-package undo-tree + :after (evil) ; for our :bind-ing + :bind (("<leader>U" . undo-tree-visualize)) + :custom + (undo-tree-history-directory-alist + `(("." . ,(tw/xdg-emacs-subdir 'data "undo-tree-history"))) + "Store all `undo-tree' history in a single directory, instead of next to the associated file.") + :commands (global-undo-tree-mode) + :demand t ; this is required so that the :config stanza is actually run asap despite :bind + :config (global-undo-tree-mode +1)) + +;; IDE-like features. +(use-package vc + :after (which-key evil) + :init (which-key-add-key-based-replacements + "<leader>g" '("vc" . "Version control") + "<leader>gM" '("merge" . "Version control merging")) + :bind-keymap ("<leader>g" . vc-prefix-map)) + +(use-package company + :hook (prog-mode . company-mode) + ;; Use TAB for selecting completions. Config from: + ;; https://github.com/company-mode/company-mode/blob/master/company-tng.el + :bind (:map company-active-map + ([tab] . company-select-next) + ([backtab] . company-select-previous)) + :custom + (company-minimum-prefix-length 2 "Start showing completion candidates slightly earlier.") + (company-idle-delay (lambda () (if (company-in-string-or-comment) nil 0.3)) + "Automatically show completions when editing non-comment parts of code.") + :diminish company-mode) + +(use-package company-quickhelp + :after (company autothemer) ; autothemer needed for `tw/get-catppuccin-color' + :hook (company-mode . company-quickhelp-mode) + :custom + (company-quickhelp-delay 0.2 "Automatically show quickhelp for completions.") + (company-quickhelp-color-background (tw/get-catppuccin-color "surface0") "Give the quickhelp tooltip a nicer background colour.") + (company-quickhelp-color-foreground (tw/get-catppuccin-color "text") "Give the quickhelp tooltip the default text colour.")) + +(use-package company-posframe + :after (company company-quickhelp) + :hook (company-mode . company-posframe-mode) + :diminish company-posframe-mode) + +(use-package flymake + :after (evil which-key) + :hook (prog-mode . flymake-mode) + :init (which-key-add-key-based-replacements + "<leader>e" '("errors" . "Flymake")) + :bind (("<leader>el" . flymake-show-buffer-diagnostics) + ("<leader>ep" . flymake-show-project-diagnostics)) + :custom + (flymake-suppress-zero-counters t "Don't show severity counters that are zero at all.")) + +(use-package flymake-collection + :after (flymake) + ;; This needs to be called in `after-init-hook' so that all other + ;; packages' `:flymake-hook's are processed before f-c-hook-setup is + ;; called. See https://github.com/mohkale/flymake-collection. + :hook (after-init . flymake-collection-hook-setup)) + +;; Language Server Protocol. +(defun tw/help-is-eldoc (&rest _) + "Set up `evil-lookup-func' to display the `eldoc' buffer." + (setq-local evil-lookup-func #'eldoc-doc-buffer)) + +(use-package eglot + ;; I have clang (for clangd) and python-lsp-server installed. + :hook (python-mode c-mode c++-mode) + :custom + (eglot-autoshutdown t "Shut down language servers after deleting their last associated buffer.") + (eglot-sync-connect 0.1 "Wait for the language server in the background if it takes longer than 100ms.") + :config + (advice-add 'eglot :after #'tw/help-is-eldoc)) + +;; Non-LSP language modes. +(use-package mmm-mode + :commands (mmm-mode)) +(use-package ledger-mode + :mode (rx ".journal" eos)) +(use-package cmake-mode + :mode (rx (or (: (or bos "/") "CMakeLists.txt") ".cmake") eos)) +(use-package puppet-mode + :mode (rx ".pp" eos)) +(use-package web-mode + :mode (rx ".htm" (? "l") eos)) +(use-package yaml-mode + :mode (rx ".y" (? "a") "ml" eos)) +(use-package rec-mode + :mode (rx ".rec" eos)) +(use-package hcl-mode + :mode (rx "." (or "hcl" "nomad") eos)) +(use-package graphviz-dot-mode + :mode (rx ".dot" eos) + :custom (graphviz-dot-view-command "xdot %s" "Use xdot for previewing graphviz files.")) + +(use-package geiser + :after (evil) + :commands (geiser geiser-eval-buffer geiser-eval-definition geiser-eval-region geiser-eval-last-sexp) + :hook (scheme-mode . geiser-autodoc-mode) + :config + (evil-define-key '(normal visual) scheme-mode-map + (kbd "<localleader>i") #'geiser + (kbd "<localleader>eb") #'geiser-eval-buffer + (kbd "<localleader>ef") #'geiser-eval-definition + (kbd "<localleader>er") #'geiser-eval-region + (kbd "<localleader>el") #'geiser-eval-last-sexp) + :defines scheme-mode-map) + +(use-package geiser-racket + :after (geiser)) +(use-package geiser-guile + :after (geiser)) + +;; My own custom packages, and stuff that isn't on MELPA. +;; (use-package actionlint ; TODO: port to flymake +;; :after (flycheck) +;; :load-path "include/") +;; (use-package alidist-mode ; TODO: port to flymake +;; :after (flycheck mmm-mode) +;; :mode (rx (or bos "/") "alidist/" (1+ anything) ".sh" eos) +;; :load-path "include/") + +(use-package bemscript-mode + :load-path "include/" + :mode (rx ".bem" eos)) + +(use-package ifm-mode + :load-path "include/" + :mode (rx ".ifm" eos)) + +(use-package pam-env-mode + :load-path "include/" + :mode (rx (or bos "/") (or "pam_env.conf" ".pam_environment") eos)) + +(use-package environmentd-mode + :load-path "include/" + :mode (rx (or bos "/") + (or (: (? "etc/") "environment") + (: ".environment.d/" (1+ (not ?\/)) ".conf")) + eos)) + +(use-package org-latex-classes + :after (ox-latex) + :load-path "include/") + +(use-package ob-rec + ;; `org-babel' hooks for `rec-mode' + :after (org org-babel) + :load-path "include/") + +(use-package vcard-mode + :load-path "include/vcard-mode/" + :mode (rx "." (or "vcf" "vcard") eos)) + +;; Vim keybindings. +(defun tw/switch-to-other-buffer () + "Switch to the last-used buffer." + (switch-to-buffer (other-buffer))) + +(defun tw/new-buffer () + "Open a new, empty buffer." + (interactive) + (switch-to-buffer (generate-new-buffer "untitled"))) + +(use-package evil + :after (which-key) + :commands (evil-mode evil-ex-nohighlight) + :init (setq evil-want-keybinding nil) ; evil-collection needs this + :custom + (evil-undo-system 'undo-tree "Use `undo-tree' for evil's undo-redo function.") + (evil-want-minibuffer t "Use evil bindings in the minibuffer too.") + (evil-want-C-u-scroll t "Scroll on C-u in normal mode, not `universal-argument'.") + (evil-want-C-u-delete t "Delete line on C-u in insert mode, not `universal-argument'.") + (evil-want-Y-yank-to-eol t "Yank from point to end-of-line on Y.") + :config + (evil-mode +1) + (evil-set-leader '(normal visual) (kbd "SPC")) ; <leader> + (evil-set-leader '(normal visual) (kbd "\\") t) ; <localleader> + (evil-define-key '(normal insert visual replace) 'global + (kbd "C-s") #'save-buffer) + ;; Global major-mode-independent keys should be defined here. Major + ;; mode-dependent keys (e.g. for launching a REPL) should go under + ;; <localleader> instead. Use `use-package' `:bind' for those. + (evil-define-key '(normal visual) 'global + ;; These keybindings mirror the default Spacemacs ones because I have + ;; muscle memory of those. + (kbd "<leader>:") #'eval-expression + (kbd "<leader>TAB") #'tw/switch-to-other-buffer + (kbd "<leader>bb") #'switch-to-buffer + (kbd "<leader>bd") #'kill-current-buffer + (kbd "<leader>bn") #'tw/new-buffer + (kbd "<leader>br") #'revert-buffer-quick + (kbd "<leader>bx") #'kill-buffer-and-window + (kbd "<leader>ff") #'find-file + (kbd "<leader>fs") #'save-buffer + (kbd "<leader>h") help-map + (kbd "<leader>sc") #'evil-ex-nohighlight + (kbd "<leader>tl") #'toggle-truncate-lines + (kbd "<leader>u") #'universal-argument + (kbd "<leader>w") evil-window-map + (kbd "<leader>wd") #'evil-window-delete ; analogous to "<leader>bd" + (kbd "<leader>wx") #'kill-buffer-and-window) ; analogous to "<leader>bx" + (which-key-add-key-based-replacements + ;; Names are a `cons' of a short name and a long name. + ;; E.g. for <leader>b, "buffer" is shown under "b" in the "<leader>" menu, + ;; while "Buffers" is shown as the title in the "<leader>b" menu. + "<leader>b" '("buffer" . "Buffers") + "<leader>f" '("file" . "Files") + "<leader>h" '("help" . "General help and documentation") + "<leader>q" '("quit" . "Finish editing the current buffer in emacsclient") + "<leader>s" '("search" . "Search operations and options") + "<leader>t" '("toggle" . "Toggles and quick settings") + "<leader>w" '("window" . "Windows")) + :functions (evil-define-key evil-set-leader + evil-define-key* evil-window-delete evil-delay) + :defines (evil-visual-state-map)) + +(use-package evil-collection + :after (evil) + :commands (evil-collection-init) :demand t + :config (evil-collection-init)) + +(use-package evil-org + :after (evil org) + :hook (org-mode . evil-org-mode) + :config + (evil-define-key '(normal visual) org-mode-map + (kbd "<localleader>\\") #'org-ctrl-c-ctrl-c + (kbd "<localleader>'") #'org-edit-src-code + (kbd "<localleader>e") #'org-export-dispatch) + (evil-define-key '(normal visual) org-src-mode-map + (kbd "<localleader>'") #'org-edit-src-exit + (kbd "<localleader>\\") #'org-edit-src-save + (kbd "<localleader>a") #'org-edit-src-abort)) + +(use-package evil-replace-with-register + :after (evil) + :commands (evil-replace-with-register-install) :demand t + :custom ; (evil-replace-with-register-key "gR" "Use the default key.") + :config (evil-replace-with-register-install)) + +(use-package evil-commentary + :after (evil) + :commands (evil-commentary-mode) :demand t + :config (evil-commentary-mode +1)) + +(use-package evil-expat + :after (evil)) + +(use-package evil-surround + :after (evil) + :commands (global-evil-surround-mode) :demand t + :config (global-evil-surround-mode +1)) + +(use-package evil-smartparens + :after (evil smartparens) + :hook (smartparens-enabled-hook . evil-smartparens-mode)) + +(use-package evil-multiedit + ;; See: https://github.com/hlissner/evil-multiedit#usage + :commands (evil-multiedit-default-keybinds) :demand t + :config (evil-multiedit-default-keybinds)) + +(use-package evil-args + :after (evil) + :config + ;; Bind evil-args text objects only. + ;; See https://github.com/wcsmith/evil-args for more bindings. + (define-key evil-inner-text-objects-map "a" 'evil-inner-arg) + (define-key evil-outer-text-objects-map "a" 'evil-outer-arg)) + +(use-package evil-numbers + :after (evil) + :bind (("<leader>+" . evil-numbers/inc-at-pt) + ("<leader>-" . evil-numbers/dec-at-pt))) + +(use-package evil-goggles ; visual previews for edit operations + :after (evil) + :commands (evil-goggles-mode evil-goggles-use-diff-faces) :demand t + :config + (evil-goggles-mode +1) + (evil-goggles-use-diff-faces) + :diminish evil-goggles-mode) + +(use-package evil-traces ; visual previews for :ex commands + :after (evil) + :commands (evil-traces-mode evil-traces-use-diff-faces) :demand t + :config + (evil-traces-mode +1) + (evil-traces-use-diff-faces) + :diminish evil-traces-mode) + +(use-package evil-markdown + :after (evil) + :hook (markdown-mode . evil-markdown-mode)) + +;; Lisp features +(use-package smartparens + :commands (smartparens-global-mode) :demand t + :config + (smartparens-global-mode +1) + (defun tw/sp-no-pair-single-quotes-p (id action context) + "Return whether ` and ' shouldn't be auto-paired in the current `major-mode'." + (memq major-mode (cons 'minibuffer-mode sp-lisp-modes))) + ;; Don't auto-pair ` and ' chars in Lisp modes or the minibuffer. + ;; Modify the global pair to not apply there. + ;; The CLOSE parameter is not used in this case, so it is nil. + (sp-pair "`" nil :unless '(tw/sp-no-pair-single-quotes-p)) + (sp-pair "'" nil :unless '(tw/sp-no-pair-single-quotes-p + sp-in-comment-p sp-in-string-p)) + (sp-local-pair 'lisp-mode "`" "'" + :actions '(wrap insert autoskip) + :when '(sp-in-comment-p sp-in-string-p)) + :functions (sp-pair sp-local-pair)) + +(use-package aggressive-indent + :hook (; (scheme-mode . aggressive-indent-mode) + ((emacs-lisp-mode common-lisp-mode) . aggressive-indent-mode))) + +(defun tw/lisp-evil-setup () + "Set up evil in general `lisp-mode' buffers." + (setq-local evil-symbol-word-search t + ;; https://github.com/wcsmith/evil-args#customization + evil-args-delimiters '(" "))) + +(add-hook 'lisp-mode-hook #'tw/lisp-evil-setup) + +;; buffer-locally set `evil-lookup-func' (used on K keys) for +;; languages where something better than man pages is available +;; (e.g. `describe-symbol' for elisp). +(defun tw/elisp-lookup-func () + "Show help in `emacs-lisp-mode' buffers." + (let ((sym (symbol-at-point))) + (if sym (describe-symbol sym) + (call-interactively #'describe-symbol)))) + +(defun tw/emacs-lisp-evil-setup () + "Set up evil in `emacs-lisp-mode' buffers." + (setq-local evil-lookup-func #'tw/elisp-lookup-func)) + +(add-hook 'emacs-lisp-mode-hook #'tw/emacs-lisp-evil-setup) + +(evil-define-key '(normal visual) emacs-lisp-mode-map + (kbd "<localleader>eb") #'eval-buffer + (kbd "<localleader>ef") #'eval-defun + (kbd "<localleader>er") #'eval-region + (kbd "<localleader>el") #'eval-last-sexp + (kbd "<localleader>ep") #'eval-print-last-sexp) + +;;; init.el ends here diff --git a/home-configuration.scm b/home-configuration.scm index 4c88248b..9e9a3590 100644 --- a/home-configuration.scm +++ b/home-configuration.scm @@ -204,6 +204,10 @@ replacement spec (to which `regexp-substitute/global' is applied)." ,(local-file #.(string-append "catppuccin/dunst/src/" catppuccin-theme-variant ".conf") "dunst-theme.conf")) + ("emacs/init.el" ,(local-file "emacs-init.el")) + (#.(string-append "emacs/catppuccin-" catppuccin-theme-variant "-theme.el") + ,(local-file #.(string-append "catppuccin/emacs/catppuccin-" + catppuccin-theme-variant "-theme.el"))) ("gtk-2.0/gtkrc" ,(local-file "gtk2.ini")) ("gtk-3.0/settings.ini" ,(local-file "gtk3.ini")) ("i3/config" ,(local-file "i3.conf")) @@ -338,12 +342,78 @@ replacement spec (to which `regexp-substitute/global' is applied)." ,@(specifications->packages gui-packages) ,@(specifications->packages '(;; CLI tools - "adb" "dos2unix" "file" "fzf" "git" "graphviz" "imagemagick" "jq" - "lesspipe" "nvme-cli" "openssh" "powertop" "pulsemixer" "pv" - "python" "rsync" "sbcl" "smartmontools" "source-highlight" - "tk" "tmux" "tree" "xxd" "zip" "unzip" + "adb" "dos2unix" "file" "fzf" "git" "graphviz" "hledger" + "imagemagick" "jq" "lesspipe" "nvme-cli" "openssh" "powertop" + "pulsemixer" "pv" "python" "rsync" "sbcl" "smartmontools" + "source-highlight" "tk" "tmux" "tree" "xxd" "zip" "unzip" + + ;; Language servers + "clang" ; for clangd + "python-lsp-server" + ;; Supported OotB by eglot, but not packaged by guix: + ;; https://github.com/mads-hartmann/bash-language-server + ;; https://github.com/regen100/cmake-language-server + ;; https://github.com/hrsh7th/vscode-langservers-extracted ; {html,css,json}-languageserver + ;; https://github.com/golang/tools/tree/master/gopls ; maybe? + ;; https://github.com/artempyanykh/marksman ; Markdown + ;; https://github.com/jeapostrophe/racket-langserver + ;; https://github.com/astoff/digestif ; (La)TeX + ;; https://github.com/redhat-developer/yaml-language-server + ;; Needs eglot config + not packaged (from lsp-mode): + ;; https://github.com/graphql/graphiql/tree/main/packages/graphql-language-service-cli#readme + ;; https://github.com/haskell/haskell-language-server / https://github.com/haskell/ghcide + ;; https://github.com/eclipse/lemminx + ;; Emacs - "emacs" "emacs-use-package" "emacs-editorconfig" ; TODO: set up EditorConfig + "emacs" + "emacs-use-package" + "emacs-eglot" + "emacs-counsel" + "emacs-ivy" "emacs-ivy-posframe" + "emacs-company" "emacs-company-quickhelp" "emacs-company-posframe" + "emacs-autothemer" ; for catppuccin/emacs + "emacs-undo-tree" + "emacs-aggressive-indent" + "emacs-which-key" + "emacs-smart-mode-line" + "emacs-diminish" + "emacs-rainbow-mode" + "emacs-guix" + ;; TODO: "emacs-editorconfig" "emacs-sly" "emacs-sly-macrostep" + "emacs-org" ; "emacs-org-modern" theme? + + "emacs-evil" + "emacs-evil-collection" + "emacs-evil-expat" ; for :reverse, :remove, :rename, :colo, :g*, ... ex commands + "emacs-evil-surround" + ;; "emacs-evil-owl" ; tests failing + "emacs-evil-args" + "emacs-evil-smartparens" + "emacs-evil-numbers" + "emacs-evil-multiedit" + "emacs-evil-goggles" + "emacs-evil-traces" + "emacs-evil-commentary" + "emacs-evil-replace-with-register" + "emacs-evil-org" + "emacs-evil-markdown" + "emacs-evil-tex" + "emacs-evil-text-object-python" + + "emacs-geiser" "emacs-geiser-racket" "emacs-geiser-guile" + "emacs-ac-geiser" "emacs-macrostep-geiser" + "emacs-ledger-mode" ; "emacs-hledger-mode" + "emacs-flymake-collection" ; "emacs-flymake-flycheck" ; if needed + "emacs-cmake-mode" + "emacs-graphviz-dot-mode" + "emacs-hcl-mode" + "emacs-mmm-mode" + "emacs-puppet-mode" + "emacs-web-mode" + "emacs-rec-mode" + "emacs-yaml-mode" + + "shellcheck" ;; Shell "zsh" "zsh-autosuggestions" "zsh-syntax-highlighting" "zsh-autopair" ;; Ranger can do code highlighting using python-pygments and |