alexpeits / emacs.d

My emacs configuration using org-mode

Home Page:https://alexpeits.github.io/emacs.d

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Variables and PATH

(add-to-list 'load-path (expand-file-name (expand-file-name "lisp" user-emacs-directory)))
(add-to-list 'custom-theme-load-path
             (expand-file-name (expand-file-name "custom-themes/" user-emacs-directory)))

(setq user-full-name "Alex Peitsinis"
      user-mail-address "alexpeitsinis@gmail.com")

(dolist (pth '(
               "/usr/local/bin"
               "~/bin"
               "~/.local/bin"
               "~/.ghcup/bin"
               ))

  (add-to-list 'exec-path (expand-file-name pth))
  (setenv "PATH" (concat (expand-file-name pth)
                         path-separator
                         (getenv "PATH"))))

(defvar is-mac (eq system-type 'darwin)
  "Whether emacs is running in mac or not")

(defvar is-windows (eq window-system 'w32)
  "Whether emacs is running in windows or not")

(defvar is-linux (not (or is-mac is-windows))
  "Whether emacs is running in linux or not")

(defvar is-gui (display-graphic-p)
  "Whether emacs is running in gui mode or not")

(defvar is-term (not is-gui)
  "Whether emacs is running in a terminal or not")

(defvar my/dropbox-dir
  (if is-windows
      "c:/Users/alexp/Dropbox"
    (expand-file-name "~/Dropbox"))
  "Private directory synced with dropbox")

(defvar my/dropbox-emacs-dir (expand-file-name "emacs/" my/dropbox-dir)
  "Private directory synced with dropbox")

(defvar my/org-directory (expand-file-name "org/" my/dropbox-emacs-dir)
  "Org directory")

(defvar my/xmonad-emacs-sp-name "emacs-sp")

Package management

package.el

(require 'package)
(add-to-list 'package-archives
             '("melpa" . "https://melpa.org/packages/")
             ;; '("MELPA Stable" . "https://stable.melpa.org/packages/")
             )

(unless (bound-and-true-p package--initialized)
  (package-initialize)
  (setq package-enable-at-startup nil))

use-package

(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

;; Can be used to debug slow packages
;; (setq use-package-minimum-reported-time 0.05 use-package-verbose t)

(eval-when-compile
  (require 'use-package))

load required packages

(use-package diminish :ensure t)

;; commonly used packages
(use-package dash :ensure t)
(use-package s :ensure t)

;; normally the PATH in nixos contains everything we'd like (e.g. rg, direnv),
;; but on mac that's not true, so load the PATH that the shell would have and
;; rely on bash/zsh/fishrc
(use-package exec-path-from-shell
  :ensure t
  :if is-mac
  :init
  (exec-path-from-shell-initialize))

Various utility functions

(defmacro my/add-hooks (hooks &rest body)
  `(dolist (hook ,hooks)
     (add-hook hook (lambda () ,@body))))

(defmacro my/execute-f-with-hook (f winf)
  `(lambda (&rest args)
     (interactive)
     (,winf)
     (apply (quote ,f) args)))

(defmacro my/control-function-window-split (f height width)
  `(lambda (&rest args)
     (interactive)
     (let ((split-height-threshold ,height)
           (split-width-threshold ,width))
       (apply (quote ,f) args))))

;; what it says
(defun my/revert-all-buffers (also-git)
  "Refresh all open file buffers without confirmation.

Buffers in modified \(not yet saved) state in EMACS will not be reverted. They
will be reverted though if they were modified outside EMACS. Buffers visiting
files which do not exist any more or are no longer readable will be killed.

With prefix argument ALSO-GIT, refresh the git state as well \(branch status on
modeline)."
  (interactive "P")
  (dolist (buf (buffer-list))
    (let ((filename (buffer-file-name buf)))
      ;; Revert only buffers containing files, which are not modified;
      ;; do not try to revert non-file buffers like *Messages*.
      (when (and filename
                 (not (buffer-modified-p buf)))
        (if (file-readable-p filename)
            ;; If the file exists and is readable, revert the buffer.
            (with-current-buffer buf
              (revert-buffer :ignore-auto :noconfirm :preserve-modes)
              (when also-git (vc-refresh-state)))
          ;; Otherwise, kill the buffer.
          (let (kill-buffer-query-functions) ; No query done when killing buffer
            (kill-buffer buf)
            (message "Killed non-existing/unreadable file buffer: %s" filename))))))
  (let ((msg-end (if also-git ", and their git state." ".")))
    (message
     (format "Finished reverting buffers containing unmodified files%s" msg-end))))

(defalias 'rb  'revert-buffer)
(defalias 'rab 'my/revert-all-buffers)

(defun my/indent-region-or-buffer ()
  "Indent a region if selected, otherwise the whole buffer."
  (interactive)
  (save-excursion
    (if (region-active-p)
        (progn
          (indent-region (region-beginning) (region-end))
          (message "Indented selected region."))
      (progn
        (indent-region (point-min) (point-max))
        (message "Indented buffer.")))))

(global-set-key (kbd "C-M-\\") #'my/indent-region-or-buffer)

(defun my/line-length (&optional line)
  "Length of the Nth line."
  (let ((ln (if line line (line-number-at-pos))))
    (save-excursion
      (goto-char (point-min))
      (if (zerop (forward-line (1- ln)))
          (- (line-end-position)
             (line-beginning-position))
        0))))

(defun my/format-region-or-buffer (cmd &rest args)
  (interactive)
  (let ((buf (current-buffer))
        (cur-point (point))
        (cur-line (line-number-at-pos))
        (cur-col (current-column))
        (cur-rel-line (- (line-number-at-pos) (line-number-at-pos (window-start)))))
    (with-current-buffer (get-buffer-create "*codefmt*")
      (erase-buffer)
      (insert-buffer-substring buf)
      (if (zerop (apply 'call-process-region `(,(point-min) ,(point-max) ,cmd t (t nil) nil ,@args)))
          (progn
            (if (not (string= (buffer-string) (with-current-buffer buf (buffer-string))))
                (copy-to-buffer buf (point-min) (point-max)))
            (kill-buffer))
        (error (format "%s failed, see *codefmt* for details" cmd))))
    (goto-line cur-line)
    (when (< cur-col (my/line-length cur-line))
      (forward-char cur-col))
    (recenter cur-rel-line)
    (message (format "Formatted with %s" cmd))))

(defun my/format-and-save (cmd &rest args)
  (interactive)
  (apply 'my/format-region-or-buffer `(,cmd ,@args))
  (save-buffer))

(defvar my/select-a-major-mode-last-selected nil)
(defun my/select-a-major-mode (&optional default)
  "Interactively select a major mode and return it as a string."
  (let* ((def (or
               default
               my/select-a-major-mode-last-selected
               (symbol-name initial-major-mode)))
         (choice (completing-read "major mode: "
                                  (apropos-internal "-mode$")
                                  nil nil nil nil
                                  def)))
    (setq my/select-a-major-mode-last-selected choice)))

(defun my/create-scratch-buffer-with-mode (other-window)
  "Create a new scratch buffer and select major mode to use.
With a prefix argument, open the buffer using `switch-to-buffer-other-window'."
  (interactive "P")
  (let* ((mmode (my/select-a-major-mode "markdown-mode"))
         (buf (generate-new-buffer (concat "*scratch" "-" mmode "*")))
         (switch-func (if other-window 'switch-to-buffer-other-window 'switch-to-buffer)))
    (funcall switch-func buf)
    (funcall (intern mmode))
    (setq buffer-offer-save nil)))

;; https://www.reddit.com/r/emacs/comments/ac9gsf/question_emacs_way_of_using_windows/
(defun my/window-dedicated (&optional window)
  "Toggle the dedicated flag on a window."
  (interactive)
  (let* ((window (or window (selected-window)))
         (dedicated (not (window-dedicated-p window))))
    (when (called-interactively-p)
      (message (format "%s %sdedicated"
                       (buffer-name (window-buffer window))
                       (if dedicated "" "un"))))
    (set-window-dedicated-p window dedicated)
    dedicated))

(defun my/window-fixed (&optional window)
  "Make a window non-resizable."
  (interactive)
  (let* ((window (or window (selected-window)))
         (new-status (with-selected-window window (not window-size-fixed))))
    (when (called-interactively-p)
      (message (format "%s %sfixed"
                       (buffer-name (window-buffer window))
                       (if new-status "" "un"))))
    (with-selected-window window
      (setq window-size-fixed new-status))
    new-status))

(defun my/copy-file-path (include-line-number)
  (interactive "P")
  (let* ((full-fp (buffer-file-name))
         (prefix (read-directory-name "prefix to strip: " (projectile-project-root)))
         (suffix (if include-line-number (format ":%s" (number-to-string (line-number-at-pos))) ""))
         (fp (concat (string-remove-prefix prefix full-fp) suffix)))
    (kill-new fp)
    (message fp)
    t))

(defvar my/useful-files
  '(
    ;; nix
    "default.nix"
    "shell.nix"
    ;; haskell
    "package.yaml"
    "stack.yaml"
    ".hlint.yaml"
    ;; python
    "requirements.txt"
    "pyproject.toml"
    "setup.cfg"
    ;; ruby
    "Gemfile"
    ;; js
    "package.json"
    ;; docker
    "docker-compose.yml"
    "Dockerfile"
    ;; bazel
    "BUILD.bazel"
    ;; drone
    ".drone.star"
    ".drone.yml"
    ;; make
    "Makefile"
    ;; git repo
    "README.md"
    ".pre-commit-config.yaml"
    ;; writing
    ".markdownlint.yml"
    ".vale.ini"
    ;; emacs
    ".dir-locals.el"
    ))

(defun my/try-open-dominating-file (other-window)
  (interactive "P")
  (let* ((cur-file (or (buffer-file-name) (user-error "Not a file")))
         (paths (seq-filter
                 #'(lambda (pair) (not (null (cdr pair))))
                 (mapcar #'(lambda (fn)
                             (cons fn (locate-dominating-file cur-file fn)))
                         my/useful-files)))
         (file (completing-read "File name: "
                                paths
                                nil nil nil nil nil))
         (dir (cdr (assoc file paths)))
         (find-file-func (if other-window 'find-file-other-window 'find-file)))
    (funcall find-file-func (expand-file-name file (file-name-as-directory dir)))))

(with-eval-after-load 'ivy
  (defun my/try-open-dominating-file-display-transformer (fn)
    (let ((dir (locate-dominating-file (buffer-file-name) fn))
          (max-length (apply 'max (mapcar 'length my/useful-files))))
      (format (format "%%-%ds (in %%s)" max-length)
              fn
              (propertize dir 'face 'font-lock-type-face))))
  (ivy-configure 'my/try-open-dominating-file
    :display-transformer-fn #'my/try-open-dominating-file-display-transformer))

(defun my/line-numbers (relative)
  (interactive "P")
  (if display-line-numbers
      (setq display-line-numbers nil)
    (if relative
        (setq display-line-numbers 'relative)
      (setq display-line-numbers t))))

(defun my/shell-command-on-buffer-or-region (cmd)
  (save-excursion
    (unless (region-active-p)
      (mark-whole-buffer))
    (shell-command-on-region (region-beginning)
                             (region-end)
                             cmd
                             nil
                             t)))

Various configurations

disable custom file

(use-package cus-edit
  :defer t
  :init
  (setq custom-file (expand-file-name "custom.el" user-emacs-directory)))

basic editing

;; remember last position
(use-package saveplace
  :hook (after-init . save-place-mode))

;; undo tree
(use-package undo-tree
  :ensure t
  :bind ("C-x u" . undo-tree-visualize)
  :diminish undo-tree-mode
  :hook (after-init . global-undo-tree-mode)
  :init
  (setq undo-tree-visualizer-relative-timestamps t
        undo-tree-visualizer-diff t
        undo-tree-history-directory-alist `(("." . ,(expand-file-name "undo" user-emacs-directory)))))

;; use column width 80 to fill (e.g. with `M-q'/`gq')
(setq-default fill-column 80)
(setq fill-indent-according-to-mode t)

(use-package autorevert
  :hook (after-init . global-auto-revert-mode)
  :diminish auto-revert-mode
  :init
  (setq auto-revert-verbose nil))

(use-package eldoc :diminish eldoc-mode)

(use-package files
  :init
  ;; add trailing newline if missing
  (setq require-final-newline t)
  ;; store all backup and autosave files in
  ;; one dir
  (setq backup-directory-alist
        `((".*" . ,temporary-file-directory)))
  (setq auto-save-file-name-transforms
        `((".*" ,temporary-file-directory t))))

(use-package simple
  :diminish visual-line-mode
  :init
  (defalias 'dw #'delete-trailing-whitespace))

;; only with this set to nil can org-mode export & open too
;; ... but it also breaks some stuff so it's disabled
;; (setq process-connection-type nil)

;; yesss
(defalias 'yes-or-no-p #'y-or-n-p)

;; Always confirm before closing because I'm stupid
(add-hook
 'kill-emacs-query-functions
 (lambda () (y-or-n-p "Do you really want to exit Emacs? "))
 'append)

;; use spaces
(setq-default indent-tabs-mode nil)

;; always scroll to the end of compilation buffers
;; (setq compilation-scroll-output t)

;; vim-like scrolling (emacs=0)
(setq scroll-conservatively 101)

;; Supress "ad-handle-definition: x got redefined" warnings
(setq ad-redefinition-action 'accept)

;; smooth mouse scrolling
(setq mouse-wheel-scroll-amount '(1 ((shift) . 1)) ;; one line at a time
      mouse-wheel-progressive-speed t ;; don't accelerate scrolling
      mouse-wheel-follow-mouse 't) ;; scroll window under mouse

;; turn off because it causes delays in some modes (e.g. coq-mode)
;; TODO: not sure if this makes a difference
(setq smie-blink-matching-inners nil)
;; (setq blink-matching-paren nil)

;; who in their right mind ends sentences with 2 spaces?
(setq sentence-end-double-space nil)

;; Don't autofill when pressing RET
(aset auto-fill-chars ?\n nil)

;; always trim whitespace before saving
;; (add-hook 'before-save-hook 'delete-trailing-whitespace)

;; some keymaps
(global-set-key (kbd "M-o") 'other-window)
(global-set-key (kbd "C-c j") 'previous-buffer)
(global-set-key (kbd "C-c k") 'next-buffer)
;; I use that to switch to Greek layout
(global-set-key (kbd "M-SPC") nil)
;; Bind M-\ to just-one-space instead of delete-horizontal-space
(global-set-key (kbd "M-\\") 'just-one-space)
;; proper count-words keybinding
(global-set-key (kbd "M-=") 'count-words)

(use-package newcomment
  :commands (comment-indent comment-kill)
  :bind (("C-;" . my/comment-end-of-line)
         ("C-:" . comment-kill))
  :init
  (setq-default comment-indent-function nil)
  (defvar-local my/comment-offset 2)
  (defun my/comment-end-of-line ()
    "Add an inline comment, 2 spaces after EOL."
    (interactive)
    (let* ((len (- (line-end-position)
                   (line-beginning-position)))
           (comment-column (+ my/comment-offset len)))
      (funcall-interactively 'comment-indent))))

;; DocView
(setq doc-view-continuous t)

;; shr (html rendering)
(make-variable-buffer-local 'shr-width)

(use-package expand-region
  :ensure t
  :bind (("C-=" . er/expand-region)
         ("C-M-=" . er/contract-region)))

;; M-x is zap-to-char
(use-package misc
  :bind ("M-Z" . zap-up-to-char))

(use-package subword
  :diminish subword-mode
  :commands (subword-mode)
  :init
  (advice-add 'subword-mode
              :after
              #'(lambda (&optional arg)
                  (setq evil-symbol-word-search subword-mode))))

(use-package outline
  :defer t
  :bind (:map outline-minor-mode-map
              ("<tab>" . my/outline-toggle-heading))
  :diminish outline-minor-mode
  :init
  (defun my/outline-toggle-heading ()
    (interactive)
    (when (outline-on-heading-p)
      (funcall-interactively 'outline-toggle-children))))

;; elisp: ;; -*- eval: (outshine-mode) -*-
(use-package outshine
  :ensure t
  :after outline
  :bind (:map outline-minor-mode-map
              ("<S-iso-lefttab>" . outshine-cycle-buffer))
  :commands (outshine-mode))

(use-package rainbow-mode
  :ensure t
  :commands (rainbow-mode)
  :init
  (setq rainbow-ansi-colors nil
        rainbow-html-colors nil
        rainbow-latex-colors nil
        rainbow-r-colors nil
        rainbow-x-colors nil))

(use-package rainbow-delimiters
  :ensure t
  :hook ((lisp-mode emacs-lisp-mode clojure-mode) . rainbow-delimiters-mode)
  :commands (rainbow-delimiters-mode)
  :diminish)

advise raise-frame with wmctrl (linux only)

(defun my/wmctrl-raise-frame (&optional frame)
  (when (executable-find "wmctrl")
    (let* ((fr (or frame (selected-frame)))
           (name (frame-parameter fr 'name))
           (flag (if (string-equal name my/xmonad-emacs-sp-name) "-R" "-a")))
      ;; catch any exception, otherwise might interfere with terminal emacsclients
      (condition-case ex
          (call-process
           "wmctrl" nil nil nil "-i" flag
           (frame-parameter fr 'outer-window-id))
        ('error nil)))))

(when is-linux
  (advice-add 'raise-frame :after 'my/wmctrl-raise-frame))

compilation

(defvar my/fast-recompile-mode-map (make-sparse-keymap))

(define-minor-mode my/fast-recompile-mode
  "Minor mode for fast recompilation using C-c C-c"
  :lighter " rc"
  :global t
  :keymap my/fast-recompile-mode-map
  (if my/fast-recompile-mode
      (progn
        (put 'my/-old-compilation-ask-about-save 'state compilation-ask-about-save)
        (setq compilation-ask-about-save nil))
    (setq compilation-ask-about-save (get 'my/-old-compilation-ask-about-save 'state))))

(define-key my/fast-recompile-mode-map (kbd "C-c C-c") #'recompile)

(use-package ansi-color
  :commands (ansi-color-apply-on-region)
  :init
  ;; http://endlessparentheses.com/ansi-colors-in-the-compilation-buffer-output.html
  (defun my/compilation-mode-colorize ()
    "Colorize from `compilation-filter-start' to `point'."
    (let ((inhibit-read-only t))
      (ansi-color-apply-on-region
       compilation-filter-start (point)))))

(use-package compile
  :commands (compile recompile)
  :init
  (defun my/compile-in-dir ()
    (interactive)
    (let ((default-directory (read-directory-name "Run command in: ")))
      (call-interactively 'compile)))
  (setq compilation-scroll-output 'first-error)
  (add-hook 'compilation-filter-hook #'my/compilation-mode-colorize))

Smartparens

Paredit keys:

keyoppositedescriptionexample
C-M-fC-M-bforward/backward sexp_(...)(...) <-> (...)_(...)
C-M-dC-M-udown-up sexp_(...) <-> (_...)
C-M-nC-M-pup-down sexp (end)(..._) <-> (...)_
(use-package smartparens-config
  :after smartparens
  :config
  ;; don't create a pair with single quote in minibuffer
  (sp-local-pair 'minibuffer-inactive-mode "'" nil :actions nil)

  ;; because DataKinds
  ;;(with-eval-after-load 'haskell-mode
  ;;  (sp-local-pair 'haskell-mode "'" nil :actions nil))

  ;; indent after inserting any kinds of parens
  (defun my/smartparens-pair-newline-and-indent (id action context)
    (save-excursion
      (newline)
      (indent-according-to-mode))
    (indent-according-to-mode))
  (sp-pair "(" nil :post-handlers
           '(:add (my/smartparens-pair-newline-and-indent "RET")))
  (sp-pair "{" nil :post-handlers
           '(:add (my/smartparens-pair-newline-and-indent "RET")))
  (sp-pair "[" nil :post-handlers
           '(:add (my/smartparens-pair-newline-and-indent "RET")))
  )

(use-package smartparens
  :ensure t
  :hook (after-init . show-smartparens-global-mode)
  :bind (:map smartparens-mode-map
              ;; paredit bindings
              ("C-M-f" . sp-forward-sexp)
              ("C-M-b" . sp-backward-sexp)
              ("C-M-d" . sp-down-sexp)
              ("C-M-u" . sp-backward-up-sexp)
              ("C-M-n" . sp-up-sexp)
              ("C-M-p" . sp-backward-down-sexp)
              ("M-s" . sp-splice-sexp)
              ("M-<up>" . sp-splice-sexp-killing-backward)
              ("M-<down>" . sp-splice-sexp-killing-forward)
              ("M-r" . sp-splice-sexp-killing-around)
              ("M-(" . sp-wrap-round)
              ("M-{" . sp-wrap-curly)
              ("C-)" . sp-forward-slurp-sexp)
              ("C-<right>" . sp-forward-slurp-sexp)
              ("C-}" . sp-forward-barf-sexp)
              ("C-<left>" . sp-forward-barf-sexp)
              ("C-(" . sp-backward-slurp-sexp)
              ("C-M-<left>" . sp-backward-slurp-sexp)
              ("C-{" . sp-backward-barf-sexp)
              ("C-M-<right>" . sp-backward-barf-sexp)
              ("M-S" . sp-split-sexp)
              ;; mine
              ("C-M-k" . sp-kill-sexp)
              ("C-M-w" . sp-copy-sexp)
              ("M-@" . sp-mark-sexp)
              )
  :diminish smartparens-mode
  :init
  (setq sp-show-pair-delay 0.2
        ;; avoid slowness when editing inside a comment for modes with
        ;; parenthesized comments (e.g. coq)
        sp-show-pair-from-inside nil
        sp-cancel-autoskip-on-backward-movement nil
        sp-highlight-pair-overlay nil
        sp-highlight-wrap-overlay nil
        sp-highlight-wrap-tag-overlay nil
        sp-python-insert-colon-in-function-definitions nil)

  (my/add-hooks '(emacs-lisp-mode-hook clojure-mode-hook)
                (smartparens-strict-mode)
                (evil-smartparens-mode))
  (my/add-hooks '(prog-mode-hook coq-mode-hook comint-mode-hook css-mode-hook)
                (smartparens-mode))
  :config
  (when is-gui
    ;; interferes in terminal
    (define-key smartparens-mode-map (kbd "M-[") 'sp-wrap-square)))

(use-package evil-smartparens
  :ensure t
  :after smartparens
  :diminish evil-smartparens-mode)

Documentation & help

(use-package which-key
  :ensure t
  :hook (after-init . which-key-mode)
  :diminish which-key-mode)

mark

(defun my/goto-line-show ()
  "Show line numbers temporarily, while prompting for the line number input."
  (interactive)
  (let ((cur display-line-numbers))
    (unwind-protect
        (progn
          (setq display-line-numbers t)
          (call-interactively #'goto-line))
      (setq display-line-numbers cur))))

(global-set-key (kbd "M-g M-g") 'my/goto-line-show)

(define-key prog-mode-map (kbd "M-a") 'beginning-of-defun)
(define-key prog-mode-map (kbd "M-e") 'end-of-defun)

(defun my/push-mark-no-activate ()
  "Pushes `point' to `mark-ring' and does not activate the region
   Equivalent to \\[set-mark-command] when \\[transient-mark-mode] is disabled"
  (interactive)
  (push-mark (point) t nil)
  (message "Pushed mark to ring"))

(global-set-key (kbd "C-`") 'my/push-mark-no-activate)

(defun my/jump-to-mark ()
  "Jumps to the local mark, respecting the `mark-ring' order.
  This is the same as using \\[set-mark-command] with the prefix argument."
  (interactive)
  (set-mark-command 1))

(global-set-key (kbd "M-`") 'my/jump-to-mark)

abbrev etc

(use-package dabbrev
  :commands (dabbrev-expand)
  :init
  ;; Don't consider punctuation part of word for completion, helps complete
  ;; qualified symbols
  (my/add-hooks
   '(prog-mode-hook)
   (setq dabbrev-abbrev-char-regexp "\\sw\\|\\s_\\|\\sw\\s.")))

(use-package abbrev
  :commands (abbrev-mode abbrev-prefix-mark)
  :diminish)

;; Testing it out
(use-package hippie-exp
  :bind (("M-/" . hippie-expand))
  :init
  (setq hippie-expand-verbose nil)
  (setq hippie-expand-try-functions-list
        '(try-expand-dabbrev
          try-expand-dabbrev-all-buffers
          try-expand-dabbrev-from-kill
          try-complete-file-name-partially
          try-complete-file-name
          try-expand-all-abbrevs
          try-expand-list
          try-expand-line
          try-complete-lisp-symbol-partially
          try-complete-lisp-symbol)))

engine-mode

(use-package engine-mode
  :ensure t
  :hook (after-init . engine-mode)
  :bind-keymap ("C-x /" . engine-mode-map)
  :config
  (defengine google
    "http://www.google.com/search?ie=utf-8&oe=utf-8&q=%s"
    :keybinding "g")

  (defengine google-images
    "http://www.google.com/images?hl=en&source=hp&biw=1440&bih=795&gbv=2&aq=f&aqi=&aql=&oq=&q=%s"
    :keybinding "i")

  (defengine google-maps
    "http://maps.google.com/maps?q=%s")

  (defengine wikipedia
    "http://www.wikipedia.org/search-redirect.php?language=en&go=Go&search=%s"
    :keybinding "w")

  (defengine wiktionary
    "https://www.wikipedia.org/search-redirect.php?family=wiktionary&language=en&go=Go&search=%s"
    :keybinding "d")

  (defengine wolfram-alpha
    "http://www.wolframalpha.com/input/?i=%s"
    :keybinding "m")

  (defengine youtube
    "http://www.youtube.com/results?aq=f&oq=&search_query=%s"
    :keybinding "v")

  (defengine hoogle
    "https://hoogle.haskell.org/?hoogle=%s"
    :keybinding "h")

  (defengine stackage
    "https://www.stackage.org/lts/hoogle?q=%s"
    :keybinding "s")

  (defengine haskell-language-extensions
    "https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-%s"
    :keybinding "#")
  )

browser

(use-package browse-url
  :init
  (setq
   browse-url-browser-function
   (cond ((or (executable-find "google-chrome-stable")
              (executable-find "google-chrome")) 'browse-url-chrome)
         ((executable-find "firefox") 'browse-url-firefox)
         (t 'browse-url-default-browser))))

prettify symbols

;; show original symbol when cursor is on it, or right next to it
(setq prettify-symbols-unprettify-at-point 'right-edge)

recentf

(use-package recentf
  :hook (after-init . recentf-mode)
  :init
  (setq recentf-max-saved-items 100))

hi-lock & symbol overlay

(use-package hi-lock
  :hook (after-init . global-hi-lock-mode)
  :init
  (setq hi-lock-face-defaults
        '(
          "hi-black-b"
          "hi-red-b"
          "hi-green-b"
          "hi-blue-b"
          "hi-green"
          "hi-blue"
          "hi-pink"
          "hi-yellow"
          ))
  (setq hi-lock-auto-select-face t)
  :config
  (define-key hi-lock-map (kbd "M-H") (lookup-key hi-lock-map (kbd "C-x w")))
  ;; TODO: find out why I can't just `define-key'
  (substitute-key-definition
   'highlight-regexp 'my/highlight-regexp hi-lock-map)

  (defun my/highlight-regexp (regexp &optional face)
    (interactive
     (list
      (hi-lock-regexp-okay
       (read-regexp "Regexp to highlight" 'regexp-history-last))
      (hi-lock-read-face-name)))
    (or (facep face) (setq face 'hi-yellow))
    (unless hi-lock-mode (hi-lock-mode 1))
    (hi-lock-set-pattern regexp face nil)))

(use-package symbol-overlay
  :ensure t
  :commands (symbol-overlay-mode)
  :diminish)

highlight keywords in some modes

(defface my/special-keyword-face
  '((t (:inherit font-lock-keyword-face)))
  "Face for highlighting special keywords"
  :group 'my/faces)

(defface my/special-comment-keyword-face
  '((t (:inherit font-lock-preprocessor-face)))
  "Face for highlighting special keywords in comments"
  :group 'my/faces)

(defun my/highlight-keyword-in-mode (mode kw &optional in-comment face)
  (let ((fc (or face (if in-comment 'my/special-comment-keyword-face 'my/special-keyword-face)))
        (str (format "\\<\\(%s\\)\\>" kw)))
    (font-lock-add-keywords
     mode
     (if in-comment
         `((,str 1 ,`(quote ,fc) prepend))
       `((,str . ,`(quote ,fc)))))))

(defvar my/comment-keywords
  '("TODO" "NOTE" "FIXME" "WARNING" "HACK" "XXX" "DONE"))

(defun my/highlight-comment-keywords (mode &optional face)
  (dolist (kw my/comment-keywords)
    (my/highlight-keyword-in-mode mode kw t face)))

(dolist
    (mode '(haskell-mode
            literate-haskell-mode
            purescript-mode
            js2-mode
            html-mode
            python-mode
            idris-mode
            agda-mode
            rust-mode
            c-mode
            emacs-lisp-mode
            coq-mode
            enh-ruby-mode
            ))
  (my/highlight-comment-keywords mode))

alignment

(use-package align
  :bind ("C-c \\" . align-regexp))

temp project roots

(defvar my/temp-project-root nil)

(defun my/get-or-set-temp-root (reset)
  (let* ((reset-root (if reset my/temp-project-root nil))
         (root
          (if (or reset
                  (null my/temp-project-root)
                  (not (file-directory-p my/temp-project-root)))
              (read-directory-name "Temp root dir: " reset-root)
            my/temp-project-root)))
    (setq my/temp-project-root root)))

edit-indirect

(use-package edit-indirect
  :ensure t
  :commands (edit-indirect-region)
  :bind ("C-c C-'" . my/edit-indirect-region)
  :config
  (add-hook 'edit-indirect-after-creation-hook 'my/edit-indirect-dedent)
  (add-hook 'edit-indirect-before-commit-hook 'my/edit-indirect-indent))

(defun my/edit-indirect-region ()
  (interactive)
  (unless (region-active-p) (user-error "No region selected"))
  (save-excursion
    (let* ((begin (region-beginning))
           (end (region-end))
           (mode (my/select-a-major-mode))
           (edit-indirect-guess-mode-function
            (lambda (_parent _beg _end)
              (funcall (intern mode)))))
      (edit-indirect-region begin end 'display-buffer))))

(defun my/get-buffer-min-leading-spaces (&optional buffer)
  (let* ((buf (or buffer (current-buffer)))
         (ind nil))
    (save-excursion
      (goto-char (point-min))
      (setq ind (org-get-indentation))
      (while (not (or (evil-eobp) (eobp)))
        (unless (string-match-p "\\`\\s-*$" (thing-at-point 'line))
          (setq ind (min ind (org-get-indentation))))
        (ignore-errors (next-line))
        ))
    ind))

(defun my/edit-indirect-dedent ()
  (let ((amount (my/get-buffer-min-leading-spaces)))
    (setq-local my/edit-indirect-dedented-amount amount)
    (save-excursion
      (indent-rigidly (point-min) (point-max) (- amount)))))

(defun my/edit-indirect-indent ()
  (when (boundp 'my/edit-indirect-dedented-amount)
    (save-excursion
      (indent-rigidly (point-min) (point-max) my/edit-indirect-dedented-amount))))

term & eshell

terms

(use-package term
  :defer t
  :config
  (my/add-hooks
   '(term-mode-hook)
   (define-key term-raw-map (kbd "M-o") nil)
   (define-key term-raw-map (kbd "M-+") nil))

  ;; automatically close term buffers on EOF
  (defun my/term-exec-hook ()
    (let* ((buff (current-buffer))
           (proc (get-buffer-process buff)))
      (set-process-sentinel
       proc
       `(lambda (process event)
          (if (string= event "finished\n")
              (kill-buffer ,buff))))))
  (add-hook 'term-exec-hook 'my/term-exec-hook))

(use-package comint
  :defer t
  :init
  (setq comint-prompt-read-only t)
  :config
  (defun my/comint-clear-buffer ()
    (interactive)
    (let ((comint-buffer-maximum-size 0))
      (comint-truncate-buffer)))
  (add-hook 'comint-mode-hook
            (lambda ()
              (define-key comint-mode-map (kbd "C-l") 'my/comint-clear-buffer))))

eshell

(use-package em-hist :after eshell)

(use-package eshell
  :commands (eshell)
  :bind (("C-!" . my/eshell)
         ("<f2>" . my/eshell))
  :init
  ;; eshell/clear doesn't work anymore because eshell has its own clear function
  (defun my/eshell-clear ()
    (interactive)
    "Clear the eshell buffer."
    (let ((eshell-buffer-maximum-lines 0))
      (eshell-truncate-buffer)))

  ;; eshell bug prevents using eshell-mode-map so this is run in the mode hook
  (defun my/eshell-define-keys ()
    (let ((map eshell-mode-map))
      (define-key map (kbd "C-l") #'my/eshell-clear)))

  (defalias 'eshell/x 'eshell/exit)
  (defalias 'eshell/e 'find-file)
  (defalias 'eshell/ff 'find-file)
  (defalias 'eshell/gc 'magit-commit-create)

  (setq eshell-destroy-buffer-when-process-dies t
        eshell-history-size 1024
        eshell-prompt-regexp "^[^#$]* [#$] ")

  (setq eshell-prompt-function
        (lambda ()
          (concat
           (propertize
            ((lambda (p-lst)
               (if (> (length p-lst) 3)
                   (concat
                    (mapconcat (lambda (elm) (if (zerop (length elm)) ""
                                               (substring elm 0 1)))
                               (butlast p-lst 3)
                               "/")
                    "/"
                    (mapconcat (lambda (elm) elm)
                               (last p-lst 3)
                               "/"))
                 (mapconcat (lambda (elm) elm)
                            p-lst
                            "/")))
             (split-string (my/eshell-prompt-dir (eshell/pwd)) "/"))
            'face
            'font-lock-type-face)
           (or (my/eshell-prompt-git (eshell/pwd)))
           " "
           (propertize "$" 'face 'font-lock-function-name-face)
           (propertize " " 'face 'default))))
  :config
  (add-hook 'eshell-mode-hook #'my/eshell-define-keys)

  (add-hook 'eshell-exit-hook 'delete-window)
  ;; Don't ask, just save
  (if (boundp 'eshell-save-history-on-exit)
      (setq eshell-save-history-on-exit t))
  ;; For older(?) version
  (if (boundp 'eshell-ask-to-save-history)
      (setq eshell-ask-to-save-history 'always)))

(use-package em-smart
  :after eshell
  :init
  (setq eshell-where-to-jump 'begin
        eshell-review-quick-commands nil
        eshell-smart-space-goes-to-end t))

(defun my/eshell (&optional dir prompt)
  "Open up a new shell in the directory associated with the current buffer.

The shell is renamed to match that directory to make multiple
eshell windows easier. If DIR is provided, open the shell there. If PROMPT is
non-nil, prompt for the directory instead. With a prefix argument, prompt for
directory."
  (interactive (list nil current-prefix-arg))
  (let* ((parent (if prompt
                     (read-directory-name "Open eshell in: ")
                   (if dir
                       dir
                     (if (buffer-file-name)
                         (file-name-directory (buffer-file-name))
                       default-directory))))
         (height (/ (window-total-height) 3))
         (name (car (last (split-string parent "/" t))))
         (bufname (format "*eshell:%s*" name))
         (default-directory parent))
    (split-window-vertically (- height))
    (other-window 1)
    (let ((eshell-banner-message
           (format "eshell in %s\n\n"
                   (propertize (abbreviate-file-name parent)
                               'face
                               'font-lock-keyword-face))))
      (eshell :new))
    (rename-buffer (generate-new-buffer-name bufname))))

(defun my/eshell-prompt-dir (pwd)
  (interactive)
  (let* ((home (expand-file-name (getenv "HOME")))
         (home-len (length home)))
    (if (and
         (>= (length pwd) home-len)
         (equal home (substring pwd 0 home-len)))
        (concat "~" (substring pwd home-len))
      pwd)))

(defun my/eshell-prompt-git (cwd)
  "Returns current git branch as a string, or the empty string if
CWD is not in a git repo (or the git command is not found)."
  (interactive)
  (when (and (eshell-search-path "git")
             (locate-dominating-file cwd ".git"))
    (let ((git-output
           (shell-command-to-string
            (format "git -C %s branch | grep '\\*' | sed -e 's/^\\* //'" cwd))))
      (concat
       (propertize
        (concat "["
                (if (> (length git-output) 0)
                    (substring git-output 0 -1)
                  "(no branch)")
                )
        'face 'font-lock-string-face)
       (my/git-collect-status cwd)
       (propertize "]" 'face 'font-lock-string-face)
       )
      )))

;; TODO
;; https://github.com/xuchunyang/eshell-git-prompt/blob/master/eshell-git-prompt.el
(defun my/git-collect-status (cwd)
  (when (and (eshell-search-path "git")
             (locate-dominating-file cwd ".git"))
    (let ((git-output
           (split-string
            (shell-command-to-string
             (format "git -C %S status --porcelain" cwd))
            "\n" t))
          (untracked 0)
          (modified 0)
          (modified-updated 0)
          (new-added 0)
          (deleted 0)
          (deleted-updated 0)
          (renamed-updated 0)
          (commits-ahead 0) ;; TODO
          (commits-behind 0) ;; TODO
          )
      (dolist (x git-output)
        (pcase (substring x 0 2)
          ("??" (cl-incf untracked))
          ("MM" (progn (cl-incf modified)
                       (cl-incf modified-updated)))
          (" M" (cl-incf modified))
          ("M " (cl-incf modified-updated))
          ("A " (cl-incf new-added))
          (" D" (cl-incf deleted))
          ("D " (cl-incf deleted-updated))
          ("R " (cl-incf renamed-updated))
          ))
      (concat
       (propertize (if (> (+ untracked deleted) 0) "" "") 'face '(:foreground "salmon3"))
       (propertize (if (> modified 0) "" "") 'face '(:foreground "goldenrod3"))
       (propertize (if (> modified-updated 0) "" "") 'face '(:foreground "SeaGreen4"))))))

vterm

;; NOTE: on NixOS this is managed by the OS, not melpa
(use-package vterm
  :ensure t
  :commands (vterm)
  :bind (("C-@" . my/vterm)
         ("<S-f2>" . my/vterm))
  :init
  (defun my/vterm ()
    (interactive)
    (let* ((height (/ (window-total-height) 3))
           (parent (if (buffer-file-name)
                       (file-name-directory (buffer-file-name))
                     default-directory))
           (name (car (last (split-string parent "/" t))))
           (bufname (format "*vterm:%s*" name)))
      (split-window-vertically (- height))
      (other-window 1)
      (vterm (generate-new-buffer-name bufname))))
  ;; kill vterm buffers when exiting with C-d
  (defun my/vterm-exit-kill-buffer (buffer event)
    (kill-buffer buffer))
  (setq vterm-exit-functions '(my/vterm-exit-kill-buffer))
  :config
  (add-to-list 'vterm-eval-cmds '("magit-commit-create" magit-commit-create)))

UI

various

;; highlight numbers
(use-package highlight-numbers
  :ensure t
  :hook ((prog-mode haskell-cabal-mode css-mode) . highlight-numbers-mode))

;; show column in modeline
(setq column-number-mode t)

;; disable annoying stuff
(setq ring-bell-function 'ignore
      inhibit-startup-message t
      inhibit-splash-screen t
      initial-scratch-message nil)
(menu-bar-mode -1)
(scroll-bar-mode -1)
(set-window-scroll-bars (minibuffer-window) nil nil)
(tool-bar-mode -1)

(use-package hl-line
  :hook (prog-mode . hl-line-mode)
  :commands (hl-line-mode global-hl-line-mode)
  :init
  (setq hl-line-sticky-flag nil))

(use-package display-fill-column-indicator
  :commands (display-fill-column-indicator-mode)
  :hook ((python-mode markdown-mode) . display-fill-column-indicator-mode))

(use-package visual-fill-column
  :ensure t
  :commands (visual-fill-column-mode)
  :init
  (defun my/visual-fill-column-mode-hook ()
    (if visual-fill-column-mode
        (visual-line-mode)
      (visual-line-mode -1)))
  (add-hook 'visual-fill-column-mode-hook #'my/visual-fill-column-mode-hook))

highlight trailing whitespace

(use-package whitespace
  :diminish whitespace-mode
  :diminish global-whitespace-mode
  :hook ((prog-mode . whitespace-mode))
  :init
  (setq whitespace-line-column 80
        whitespace-style '(face trailing)))

Theme

theme loading

(setq custom--inhibit-theme-enable nil)

(defvar my/avail-themes '(deeper-blue adwaita))
(defvar my/current-theme 0)

(defvar my/after-set-theme-hook nil
  "Hook called after setting a theme")

(defun my/set-theme (&optional theme)
  (let ((theme (or theme (elt my/avail-themes my/current-theme))))
    (mapc 'disable-theme custom-enabled-themes)
    (if (functionp theme)
        (funcall theme)
      (load-theme theme t))
    (run-hooks 'my/after-set-theme-hook)))

(defun my/toggle-theme ()
  (interactive)
  (let* ((next-theme (mod (1+ my/current-theme) (length my/avail-themes)))
         (theme (elt my/avail-themes next-theme)))
    (setq my/current-theme next-theme)
    (my/set-theme)))

(defun my/refresh-theme ()
  (interactive)
  (my/set-theme))

(use-package color
  :commands (color-darken-name color-lighten-name))

modus themes

(use-package modus-themes
  :ensure t
  :defer t
  :init
  (setq modus-themes-org-blocks 'greyscale)
  (setq modus-themes-common-palette-overrides
        '((bg-mode-line-active bg-cyan-subtle)
          (fg-mode-line-active fg-main)
          (border-mode-line-active slate)
          (fg-region nil)
          (bg-region bg-cyan-subtle)
          (fg-heading-1 green-cooler)
          ))
  (setq modus-themes-headings '((1 . (1.2))))
  (setq modus-vivendi-palette-overrides
        '((bg-main "#090909")
          (fg-main "#e7e7e7")
          (bg-hl-line "#272727")
          (type cyan-warmer)))
  (setq modus-operandi-palette-overrides
        '((bg-hl-line "#eef3f6")))
  (setq modus-operandi-palette-overrides nil))

Fonts

(defvar my/font-variant "default")
(defvar my/fonts
  '(("default" . (:fixed ("Monospace" . 12) :variable ("sans-serif" . 12)))))

(defvar my/after-set-font-hook nil
  "Hook called after updating fonts")

(defun my/all-font-variants ()
  (mapcar 'car my/fonts))

(defun my/set-font (&optional variant)
  (let* ((variant (or variant my/font-variant))
         (spec (cdr (assoc variant my/fonts)))
         (fixed (plist-get spec :fixed))
         (variable (plist-get spec :variable))
         (spacing (or (plist-get spec :spacing) 0)))
    (dolist (face '(default fixed-pitch))
      (set-face-attribute
       face nil :font (format
                       "%s-%s"
                       (car fixed)
                       (cdr fixed))))
    (set-face-attribute
     'variable-pitch nil :font (format
                                "%s-%s"
                                (car variable)
                                (cdr variable)))
    (setq line-spacing spacing)
    (setq-default line-spacing spacing)
    (run-hooks 'my/after-set-font-hook)))

(defun my/select-font-variant (&optional new-variant)
  (interactive)
  (let* ((variants (my/all-font-variants))
         (new-variant (or new-variant (completing-read "Font variant: "
                                                       variants
                                                       nil nil nil nil
                                                       my/font-variant))))
    (setq my/font-variant new-variant)
    (my/set-font)))

(defun my/toggle-font ()
  (interactive)
  (let* ((variants (my/all-font-variants))
         (cur-idx (cl-position my/font-variant variants :test 'string-equal))
         (next-idx (mod (1+ cur-idx) (length variants)))
         (new-variant (elt variants next-idx)))
    (my/select-font-variant new-variant)))

(defun my/refresh-font ()
  (interactive)
  (my/set-font))

;; size & scaling
(setq text-scale-mode-step 1.05)
(define-key global-map (kbd "C-+") 'text-scale-increase)
(define-key global-map (kbd "C--") 'text-scale-decrease)

VCS

vc

Common prefix is C-x v

Some useful commands:

keynamedescription
C-x v C-h-show help for vc-related actions
C-x v pmy/vc-projectrun vc-dir in repo root
C-x v vvc-next-actionnext logical action in a repo (init, add, commit)
C-x v d or =vc-diffshow diff for current file
C-x v Dvc-root-diffshow diff for whole repo
C-x v avc-annotateshow history, color-coded
C-x v hvc-region-historyshow history (buffer or region)
C-x v lvc-print-logshow log for current file
C-x v +vc-updatepull
C-x v Pvc-pushpush

In vc-git-log-edit-mode:

keynamedescription
C-c C-clog-edit-donesave commit
C-c C-klog-edit-kill-buffercancel commit
(use-package vc
  :bind (("C-x v p" . my/vc-project)
         ("C-x v d" . vc-diff)
         :map log-view-mode-map
         ("<tab>" . log-view-toggle-entry-display)
         ("j" . next-line)
         ("k" . previous-line)
         ("l" . forward-char)
         ("h" . backward-char))
  :init
  ;; prot
  (defun my/vc-project ()
    (interactive)
    (vc-dir (vc-root-dir)))
  (defun my/log-edit-toggle-amend ()
    (interactive)
    (log-edit-toggle-header "Amend" "yes"))
  :config
  (use-package log-view)
  (add-hook 'vc-git-log-edit-mode-hook 'auto-fill-mode)
  (define-key diff-mode-map (kbd "M-o") nil))

(use-package log-edit
  :defer t
  :bind (:map log-edit-mode-map
              ("C-c C-a" . my/log-edit-toggle-amend)))

(use-package vc-git
  :init
  (setq vc-git-print-log-follow t
        vc-git-diff-switches '("--patch-with-stat" "--histogram")))

(use-package vc-annotate
  :bind (("C-x v a" . vc-annotate)
         :map vc-annotate-mode-map
         ("t" . vc-annotate-toggle-annotation-visibility))
  :init
  (setq vc-annotate-display-mode 'scale))

magit

(use-package magit
  :ensure t
  :commands (magit-status
             magit-dispatch-popup
             magit-blame-addition
             magit-log-buffer-file)
  :bind (("C-x g" . magit-status)
         ("C-x M-g" . magit-dispatch-popup))
  :init
  (defalias 'magb 'magit-blame-addition)
  (defalias 'gl   'magit-log-buffer-file)
  (defalias 'magl 'magit-log-buffer-file)
  :config
  (add-hook 'magit-blame-mode-hook
            (lambda ()
              (if (or (not (boundp 'magit-blame-mode))
                      magit-blame-mode)
                  (evil-emacs-state)
                (evil-exit-emacs-state)))))

;; most stuff copied from prot
(use-package magit-diff
  :after magit
  :init
  (setq magit-diff-refine-hunk t))

(use-package git-commit
  :after magit
  :init
  (setq git-commit-summary-max-length 50)
  (setq git-commit-style-convention-checks
        '(non-empty-second-line
          overlong-summary-line)))

(use-package magit-repos
  :after magit
  :commands (magit-list-repositories)
  :bind (:map magit-repolist-mode-map
              ("d" . my/magit-repolist-dired))
  :config
  (defun my/magit-repolist-dired ()
    (interactive)
    (--if-let (tabulated-list-get-id)
        (dired (expand-file-name it))
      (user-error "There is no repository at point"))))

(use-package magit-todos
  :ensure t
  :after magit
  :config
  (magit-todos-mode))

git modes

(add-to-list 'auto-mode-alist '("/\\.gitignore\\'"  . conf-unix-mode))
(add-to-list 'auto-mode-alist '("CODEOWNERS\\'"  . conf-unix-mode))

ediff

(use-package ediff
  :commands (ediff-files
             ediff-files3
             ediff-buffers
             ediff-buffers3
             smerge-ediff)
  :init
  (setq ediff-keep-variants nil
        ediff-make-buffers-readonly-at-startup nil
        ediff-show-clashes-only t
        ediff-split-window-function 'split-window-horizontally
        ediff-window-setup-function 'ediff-setup-windows-plain))

git-timemachine

(use-package git-timemachine
  :ensure t
  :commands (git-timemachine)
  :config
  (add-hook
   'git-timemachine-mode-hook
   '(lambda () (evil-local-mode -1))))

diff-hl & git-gutter+

(use-package diff-hl
  :ensure t
  :if is-gui
  :hook ((after-init . global-diff-hl-mode)
         (dired-mode . diff-hl-dired-mode))
  :config
  ;; https://github.com/dgutov/diff-hl#magit
  (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh)
  (defun my/toggle-git-gutters ()
    (interactive)
    (call-interactively 'global-diff-hl-mode)))

(use-package git-gutter+
  :ensure t
  :unless is-gui
  :diminish
  :hook (after-init . global-git-gutter+-mode)
  :config
  (defun my/toggle-git-gutters ()
    (interactive)
    (call-interactively 'global-git-gutter+-mode)))

keybindings

keybind to command mapping

(setq my/leader-keys
      '(
        ("SPC" display-fill-column-indicator-mode)

        ("a" align-regexp)

        ("b" my/eww-browse-dwim)

        ;; dired
        ("dn" find-name-dired)
        ("dg" find-grep-dired)
        ("dv" my/git-grep-dired)

        ;; errors
        ("el" my/toggle-flycheck-error-list)

        ;; browsing/files
        ("fc" my/copy-file-path)
        ("fd" pwd)
        ("fp" my/try-open-dominating-file)
        ("fs" my/create-scratch-buffer-with-mode)

        ;; git/vc

        ("h"  help)

        ;; insert
        ("iu" counsel-unicode-char)

        ;; project
        ("pa" counsel-projectile-ag)
        ("pr" counsel-projectile-rg)
        ("ps" my/rg-project-or-ask)
        ("pt" my/counsel-ag-todos-global)

        ;; show/display
        ("sd" pwd)
        ;; find/search
        ("sa" ag)
        ("sr" rg)
        ("sca" counsel-ag)
        ("scr" counsel-rg)
        ("sr" rgrep)

        ;; toggle
        ("t8" display-fill-column-indicator-mode)
        ("tc" global-company-mode)
        ("tf" my/toggle-font)
        ("tF" my/select-font-variant)
        ("tg" my/toggle-git-gutters)
        ("tl" my/line-numbers)
        ("to" symbol-overlay-mode)
        ("th" hl-line-mode)
        ("ts" flycheck-mode)
        ("tt" my/toggle-theme)
        ("tw" toggle-truncate-lines)

        ;; ui
        ("uh" rainbow-mode)
        ("um" (lambda () (interactive) (call-interactively 'tool-bar-mode) (call-interactively 'menu-bar-mode)))
        ("up" rainbow-delimiters-mode)

        ;; windows
        ("wf" my/window-fixed)
        ("wd" my/window-dedicated)

        ;; theme
        ("Ts" counsel-load-theme)

        ("Q" evil-local-mode)
        ))

setup keybindings

(define-prefix-command 'my/leader-map)

;; (define-key ctl-x-map "m" 'my/leader-map)
(define-prefix-command 'my/leader-map)
(global-set-key (kbd "C-c m") 'my/leader-map)

(dolist (i my/leader-keys)
  (let ((k (car i))
        (f (cadr i)))
    (define-key my/leader-map (kbd k) f)))

(define-prefix-command 'my/major-mode-map)

(if is-gui
    (progn
      ;; distinguish `C-m` from `RET`
      (define-key input-decode-map [?\C-m] [C-m])
      ;; distinguish `C-i` from `TAB`
      ;; (define-key input-decode-map [?\C-i] [C-i])
      (global-set-key (kbd "C-c <C-m>") 'my/leader-map)
      (setq my/major-mode-map-key "<C-m>"))
  (setq my/major-mode-map-key "C-c m m"))

;; on hold
;; (defun my/define-major-mode-keys (hook &rest combinations)
;;   "Bind all pairs of (key . function) under `my/major-mode-map-key'
;;
;; The keys are bound after `hook'."
;;   (add-hook
;;    hook
;;    `(lambda ()
;;       (let ((map (make-sparse-keymap)))
;;         (local-set-key (kbd ,my/major-mode-map-key) map)
;;         (dolist (comb (quote ,combinations))
;;           (define-key map (kbd (car comb)) (cdr comb)))))))

(defun my/define-major-mode-key (mode key func)
  (let* ((map-symbol (intern (format "my/%s-map" mode)))
         (hook (intern (format "%s-hook" mode)))
         (map
          (if (boundp map-symbol)
              (symbol-value map-symbol)
            (progn
              (let ((map- (make-sparse-keymap)))
                (add-hook
                 hook
                 `(lambda ()
                    (local-set-key (kbd ,my/major-mode-map-key) (quote ,map-))))
                (set (intern (format "my/%s-map" mode)) map-))))))
    (define-key map (kbd key) func)
    (evil-leader/set-key-for-mode mode (kbd (format "m %s" key)) func)))

(if is-gui
    (global-set-key (kbd "<C-m>") 'my/major-mode-map)
  (global-set-key (kbd "C-c m m") 'my/major-mode-map))

evil-mode

evil-mode setup

(use-package evil-leader
  :hook (evil-local-mode . evil-leader-mode)
  :ensure t
  :config
  (evil-leader/set-leader "<SPC>")
  (dolist (i my/leader-keys)
    (let ((k (car i))
          (f (cadr i)))
      (evil-leader/set-key k f))))

(use-package evil-visualstar
  :hook (evil-local-mode . evil-visualstar-mode)
  :ensure t)

(use-package evil
  :ensure t
  :hook ((prog-mode
          text-mode
          haskell-cabal-mode
          bibtex-mode
          coq-mode easycrypt-mode phox-mode
          mermaid-mode
          feature-mode
          conf-unix-mode
          conf-colon-mode
          conf-space-mode
          conf-windows-mode
          conf-toml-mode)
         . evil-local-mode)
  :init
  (setq evil-disable-insert-state-bindings t
        evil-want-C-i-jump nil
        evil-undo-system 'undo-tree
        evil-intercept-esc t
        evil-respect-visual-line-mode t
        evil-mode-line-format '(before . mode-line-front-space))
  ;; (setq evil-move-cursor-back nil)  ;; works better with lisp navigation
  :config
  (defun my/make-emacs-mode (mode)
    "Make `mode' use emacs keybindings."
    (delete mode evil-insert-state-modes)
    (add-to-list 'evil-emacs-state-modes mode))

  (global-set-key (kbd "<f5>") 'evil-local-mode)

  ;; don't need C-n, C-p
  (define-key evil-insert-state-map (kbd "C-n") nil)
  (define-key evil-insert-state-map (kbd "C-p") nil)

  ;; magit
  (evil-define-key 'normal magit-blame-mode-map (kbd "q") 'magit-blame-quit)

  ;; intercept ESC when running in terminal
  (evil-esc-mode)

  ;; move search result to center of the screen
  (defadvice evil-search-next
      (after advice-for-evil-search-next activate)
    (evil-scroll-line-to-center (line-number-at-pos)))

  (defadvice evil-search-previous
      (after advice-for-evil-search-previous activate)
    (evil-scroll-line-to-center (line-number-at-pos)))

  ;; this is needed to be able to use C-h
  (global-set-key (kbd "C-h") 'help)
  (define-key evil-normal-state-map (kbd "C-h") 'undefined)
  (define-key evil-insert-state-map (kbd "C-h") 'undefined)
  (define-key evil-visual-state-map (kbd "C-h") 'undefined)

  (define-key evil-emacs-state-map (kbd "C-h") 'help)
  (define-key evil-insert-state-map (kbd "C-k") nil)

  (define-key evil-normal-state-map (kbd "M-.") nil)

  (define-key evil-normal-state-map (kbd "C-h") 'evil-window-left)
  (define-key evil-normal-state-map (kbd "C-j") 'evil-window-down)
  (define-key evil-normal-state-map (kbd "C-k") 'evil-window-up)
  (define-key evil-normal-state-map (kbd "C-l") 'evil-window-right)

  (define-key evil-normal-state-map (kbd ";") 'evil-ex)
  (define-key evil-visual-state-map (kbd ";") 'evil-ex)
  (evil-ex-define-cmd "sv" 'evil-window-split)

  (define-key evil-normal-state-map (kbd "C-p") 'counsel-projectile-find-file)

  (define-key evil-insert-state-map (kbd "C-M-i") 'company-complete)

  (define-key evil-visual-state-map (kbd "<") #'(lambda ()
                                                  (interactive)
                                                  (progn
                                                    (call-interactively 'evil-shift-left)
                                                    (execute-kbd-macro "gv"))))

  (define-key evil-visual-state-map (kbd ">") #'(lambda ()
                                                  (interactive)
                                                  (progn
                                                    (call-interactively 'evil-shift-right)
                                                    (execute-kbd-macro "gv"))))

  ;; redefine so that $ doesn't include the EOL char
  (setq my/evil-$-include-eol nil)
  (evil-define-motion evil-end-of-line (count)
    "Move the cursor to the end of the current line.

If COUNT is given, move COUNT - 1 lines downward first."
    :type inclusive
    (move-end-of-line count)
    (when evil-track-eol
      (setq temporary-goal-column most-positive-fixnum
            this-command 'next-line))
    (unless (and (evil-visual-state-p) my/evil-$-include-eol)
      (evil-adjust-cursor)
      (when (eolp)
        ;; prevent "c$" and "d$" from deleting blank lines
        (setq evil-this-type 'exclusive))))

  ;; https://github.com/emacs-evil/evil-surround/issues/141
  (defmacro my/evil-define-text-object (name key start-regex end-regex)
    (let ((inner-name (make-symbol (concat "evil-inner-" name)))
          (outer-name (make-symbol (concat "evil-a-" name))))
      `(progn
         (evil-define-text-object ,inner-name (count &optional beg end type)
           (evil-select-paren ,start-regex ,end-regex beg end type count nil))
         (evil-define-text-object ,outer-name (count &optional beg end type)
           (evil-select-paren ,start-regex ,end-regex beg end type count t))
         (define-key evil-inner-text-objects-map ,key #',inner-name)
         (define-key evil-outer-text-objects-map ,key #',outer-name))))
  )

evil packages that can be used without evil-mode

(use-package evil-nerd-commenter
  :ensure t
  :bind ("M-;" . evilnc-comment-or-uncomment-lines)
  :init
  ;; evilnc toggles instead of commenting/uncommenting
  (setq evilnc-invert-comment-line-by-line t))

(use-package evil-surround
  :ensure t
  :hook (after-init . global-evil-surround-mode)
  :config
  (evil-define-key 'visual evil-surround-mode-map "s" 'evil-surround-region)
  (defconst my/mark-active-alist
    `((mark-active
       ,@(let ((m (make-sparse-keymap)))
           (define-key m (kbd "C-c s") 'evil-surround-region)
           m))))
  (add-to-list 'emulation-mode-map-alists 'my/mark-active-alist))

terminal cursor

;; in <user-emacs-directory>/lisp
(use-package term-cursor
  :if is-term
  :hook (after-init . global-term-cursor-mode))

Spell checking

(use-package flyspell
  :commands (flyspell-mode flyspell-prog-mode)
  :config
  (add-hook 'flyspell-mode-hook
            (lambda () (add-hook 'hack-local-variables-hook 'flyspell-buffer))))

Buffer & window management

ibuffer

(use-package ibuffer
  :init
  ;; `/ R` to toggle showing these groups
  ;; `/ \` to disable
  (setq-default ibuffer-saved-filter-groups
                `(("Default"
                   ("rg" (name . "\*rg.*\*"))
                   ("Dired" (mode . dired-mode))
                   ("Scratch" (name . "\*scratch.*"))
                   ("Temporary" (name . "\*.*\*"))
                   )))
  (setq ibuffer-show-empty-filter-groups nil)
  :config
  (define-key ibuffer-mode-map (kbd "M-o") nil)
  (global-set-key (kbd "C-x C-b") 'ibuffer)
  (add-hook 'ibuffer-mode-hook #'(lambda () (ibuffer-auto-mode 1))))

avy

(use-package avy
  :ensure t
  :bind (("C-c i" . avy-goto-char-timer)))

ace-window

(use-package ace-window
  :ensure t
  :bind ("C-c o" . ace-window)
  :init
  (setq aw-dispatch-always nil
        aw-keys (string-to-list "asdfghjkl;"))
  (my/add-hooks
   '(term-mode-hook)
   (define-key term-raw-map (kbd "C-c o") #'ace-window)))

buffer-move

(use-package buffer-move
  :ensure t
  :bind (("<C-S-up>" . buf-move-up)
         ("<C-S-down>" . buf-move-down)
         ("<C-S-left>" . buf-move-left)
         ("<C-S-right>" . buf-move-right)))

zoom

(use-package zoom
  :ensure t
  :bind ("M-+" . zoom)
  :init
  (defun my/zoom-size ()
    (let* ((total-w (frame-width))
           (total-h (frame-height))
           (focus-w (max 100 (/ total-w 4)))
           (focus-h (max 65 (/ total-h 3)))
           (rest-w 20)
           (rest-h 10)
           (remain-w (abs (- total-w rest-w)))
           (remain-h (abs (- total-h rest-h)))
           (final-w (min focus-w remain-w))
           (final-h (min focus-h remain-h))
           )
      (cons final-w final-h)
      ))
  (setq zoom-size 'my/zoom-size))

eww

(use-package eww
  :commands (eww)
  :bind (:map eww-mode-map
              ("q" . my/eww-quit))
  :config
  (my/add-hooks
   '(eww-mode-hook)
   (setq shr-width 100)
   (setq-local shr-max-image-proportion 0.35))

  (defun my/eww-quit ()
    (interactive)
    (quit-window :kill)
    (unless (one-window-p) (delete-window))))

dired

(use-package dired
  :bind (:map dired-mode-map
              ("j" . dired-next-line)
              ("J" . dired-next-dirline)
              ("k" . dired-previous-line)
              ("K" . dired-prev-dirline)
              ("h" . backward-char)
              ("l" . forward-char)
              ("C-c C-n" . my/dired-find-file-ace)
              ("C-c C-l" . my/dired-limit-regexp)
              ("M-j" . my/dired-file-jump-from-here)
              ("M-u" . dired-up-directory)
              ("C-c C-q" . my/dired-kill-all-buffers))
  :init
  ;; hide files being edited & flycheck files from dired
  (setq dired-omit-files "\\`[.]?#\\|\\`.flycheck_\\|__pycache__"
        dired-omit-verbose nil)
  (setq dired-hide-details-hide-symlink-targets nil)
  :config
  (add-hook 'dired-mode-hook #'auto-revert-mode)
  (add-hook 'dired-mode-hook #'dired-omit-mode)

  ;; prot
  (defvar my/dired-limit-hist '()
    "Minibuffer history for `my/dired-limit-regexp'")

  (defun my/dired-limit-regexp (regexp omit)
    "Limit Dired to keep files matching REGEXP.

With optional OMIT argument as a prefix (\\[universal-argument]),
exclude files matching REGEXP.

Restore the buffer with \\<dired-mode-map>`\\[revert-buffer]'."
    (interactive
     (list
      (read-regexp
       (concat "Files "
               (when current-prefix-arg
                 (propertize "NOT " 'face 'warning))
               "matching PATTERN: ")
       nil 'prot-dired--limit-hist)
      current-prefix-arg))
    (dired-mark-files-regexp regexp)
    (unless omit (dired-toggle-marks))
    (dired-do-kill-lines)
    (add-to-history 'my/dired-limit-hist regexp))


  (define-key dired-mode-map
    (kbd "C-c v")
    (my/control-function-window-split
     dired-find-file-other-window
     nil 0))
  (define-key dired-mode-map
    (kbd "C-c s")
    (my/control-function-window-split
     dired-find-file-other-window
     0 nil)))

(use-package dired-sidebar
  :ensure t
  :commands (dired-sidebar-hide-sidebar
             dired-sidebar-showing-sidebar-p
             dired-sidebar-jump-to-sidebar
             dired-sidebar-toggle-sidebar
             dired-sidebar-toggle-with-current-directory)
  :bind (("C-\"" . my/dired-sidebar-smart-toggle)
         :map dired-sidebar-mode-map
         ("M-u" . dired-sidebar-up-directory))
  :init
  (setq dired-sidebar-theme 'none
        dired-sidebar-should-follow-file t))

(defun my/dired-sidebar-smart-toggle (curdir)
  (interactive "P")
  (if (eq major-mode 'dired-sidebar-mode)
      (dired-sidebar-hide-sidebar)
    (if (dired-sidebar-showing-sidebar-p)
        (dired-sidebar-jump-to-sidebar)
      (if curdir
          (dired-sidebar-toggle-with-current-directory)
        (dired-sidebar-toggle-sidebar)))))

(use-package dired-subtree
  :ensure t
  :after dired
  :bind (:map dired-mode-map
              ("<tab>" . dired-subtree-toggle)
              ("TAB" . dired-subtree-toggle)))

(use-package dired-filter
  :ensure t
  :after dired)

(use-package dired-git-info
  :ensure t
  :after dired
  :bind (:map dired-mode-map (")" . dired-git-info-mode))
  :commands (dired-git-info-mode))

;; more detailed colors
(use-package diredfl
  :ensure t
  :hook (dired-mode . diredfl-mode))

(defun my/dired-find-file-ace ()
  (interactive)
  (let ((find-file-run-dired t)
        (fname (dired-get-file-for-visit)))
    (if (ace-select-window)
        (find-file fname))))

(defun my/dired-file-jump-from-here ()
  (interactive)
  (let ((find-file-run-dired t)
        (fname (dired-get-file-for-visit)))
    (my/counsel-file-jump-from-here fname)))

(defun my/dired-kill-all-buffers ()
  (interactive)
  (mapc (lambda (buf)
          (when (eq 'dired-mode
                    (buffer-local-value 'major-mode buf))
            (kill-buffer buf)))
        (buffer-list)))

(use-package dired-x
  :after dired
  :init
  (if is-mac (setq dired-use-ls-dired nil)))

regex replace

re-builder (useful for debugging)

(use-package re-builder
  :commands (re-builder)
  :init
  (setq reb-re-syntax 'string))

visual-regexp-steroids

(use-package visual-regexp-steroids
  :ensure t
  :bind (("M-%" . vr/replace)
         ("C-M-%" . vr/query-replace))
  :init
  (setq vr/engine 'python
        vr/match-separator-use-custom-face t))

direnv

(use-package direnv
  :ensure t
  :if (executable-find "direnv")
  :hook (after-init . direnv-mode)
  :init
  (setq direnv-show-paths-in-summary nil
        direnv-always-show-summary nil)
  (unless (fboundp 'file-attribute-size)
    (defun file-attribute-size (attrs) (elt attrs 7))))

lsp

(use-package lsp-mode
  :ensure t
  :commands lsp
  :hook (lsp-mode . lsp-lens-mode)
  :init
  (setq lsp-prefer-flymake nil))

(use-package lsp-ui
  :ensure t
  :commands lsp-ui-mode
  :init
  (setq lsp-ui-doc-delay 1))

LANGUAGES

nix

(use-package nix-mode
  :ensure t
  :mode (("\\.nix\\'" . nix-mode)
         ("\\.drv\\'" . nix-drv-mode))
  :init
  (setq nix-nixfmt-bin "nixpkgs-fmt")
  :config
  (my/add-hooks '(nix-mode-hook) (subword-mode 1))
  (my/define-major-mode-key 'nix-mode "s" 'my/nix-format-and-save)
  (my/define-major-mode-key 'nix-mode "m" 'my/nix-mark-multiline-string)
  (define-key nix-mode-map (kbd "C-c '") 'my/nix-edit-indirect-multiline-string))

(defun my/nix-format-and-save ()
  (interactive)
  (nix-format-buffer)
  (save-buffer))

(defun my/nix-mark-multiline-string ()
  (interactive)
  (deactivate-mark)
  (re-search-backward "''$" nil t)
  (next-line)
  (beginning-of-line 1)
  (call-interactively 'set-mark-command)
  (re-search-forward "^\s*''" nil t)
  (previous-line)
  (end-of-line 1))

(defun my/nix-edit-indirect-multiline-string ()
  (interactive)
  (my/nix-mark-multiline-string)
  (my/edit-indirect-region))

haskell

(use-package haskell-mode
  :ensure t
  :mode (("\\.hs\\'" . haskell-mode)
         ("\\.lhs\\'" . literate-haskell-mode)
         ("\\.cabal\\'" . haskell-cabal-mode)
         ("\\.c2hs\\'" . haskell-c2hs-mode)
         ("\\.hcr\\'" . ghc-core-mode)
         ("\\.dump-simpl\\'" . ghc-core-mode))
  :init
  (setq haskell-align-imports-pad-after-name t
        haskell-hoogle-command "hoogle --count=100"
        haskell-interactive-popup-errors nil
        ;; choices: auto, ghci, cabal-repl, stack-ghci
        ;; cabal-repl is the one to use with nix-shell & direnv
        ;; NOTE: cabal-new-repl is deprecated and equivalent to cabal-repl
        haskell-process-type 'cabal-repl
        )

  (with-eval-after-load 'evil
    (my/evil-define-text-object "haskell-inline-comment" "#" "{- " " -}"))

  ;; TODO: sort out this shit
  (with-eval-after-load 'smartparens
    (with-eval-after-load 'haskell-mode
      (sp-local-pair 'haskell-mode "'" nil :actions nil)))

  ;; fontify special ghcid comments in haskell-mode
  ;; the comments look like this '-- $> '
  ;; and are evaluated if ghcid is started with the '-a/--allow-eval' flag
  (defface my/haskell-ghcid-eval-face
    '((t (:inherit font-lock-warning-face)))
    "Face for highlighting ghcid eval directives in haskell-mode"
    :group 'my/faces)

  (font-lock-add-keywords
   'haskell-mode
   '(("^[ \t]*-- $> .*" 0 'my/haskell-ghcid-eval-face prepend)))

  :config
  (my/highlight-keyword-in-mode 'haskell-mode "error" nil 'font-lock-preprocessor-face)
  (my/highlight-keyword-in-mode 'haskell-mode "undefined" nil 'font-lock-preprocessor-face)

  (my/define-major-mode-key 'haskell-mode "aa" 'my/haskell-align-and-sort-everything)
  (my/define-major-mode-key 'haskell-mode "ai" 'my/haskell-align-and-sort-imports)
  (my/define-major-mode-key 'haskell-mode "al" 'my/haskell-align-and-sort-language-extensions)
  (my/define-major-mode-key 'haskell-mode "c" 'projectile-compile-project)
  (my/define-major-mode-key 'haskell-mode "d" 'my/haskell-open-haddock-documentation)
  (my/define-major-mode-key 'haskell-mode "h" 'hoogle)
  (my/define-major-mode-key 'haskell-mode "i" 'my/haskell-insert-import)
  (my/define-major-mode-key 'haskell-mode "l" 'my/haskell-insert-language-extension)
  (my/define-major-mode-key 'haskell-mode "o" 'my/haskell-insert-ghc-option)
  (my/define-major-mode-key 'haskell-mode "r" 'my/haskell-insert-ghcid-repl-statement)
  (my/define-major-mode-key 'haskell-mode "s" 'my/haskell-format-and-save)
  (my/define-major-mode-key 'haskell-mode "/" 'engine/search-hoogle)
  (my/define-major-mode-key 'haskell-mode "?" 'engine/search-stackage)
  (my/define-major-mode-key 'haskell-mode "#" 'engine/search-haskell-language-extensions)

  (my/define-major-mode-key 'haskell-cabal-mode "s" 'my/haskell-cabal-format-and-save)

  (my/add-hooks
   '(haskell-mode-hook)
   (setq evil-shift-width 2)
   (push '(?# . ("{- " . " -}")) evil-surround-pairs-alist)
   (haskell-decl-scan-mode)
   (subword-mode 1))
  )

(use-package ormolu
  :ensure t
  :commands (ormolu-format
             ormolu-format-buffer
             ormolu-format-region
             ormolu-format-on-save-mode)
  :init
  (setq ormolu-extra-args
        '("-o" "-XTypeApplications"
          "-o" "-XInstanceSigs"
          "-o" "-XBangPatterns"
          "-o" "-XPatternSynonyms"
          "-o" "-XUnicodeSyntax"
          )))

(defvar my/haskell-align-stuff t)
(defvar my/haskell-use-ormolu t)

(defun my/haskell-cabal-format-and-save ()
  (interactive)
  (save-buffer)
  (shell-command (format "cabal format %s" (buffer-file-name)))
  (revert-buffer nil t))

(defun my/haskell-format-and-save (use-ormolu)
  "Format the import statements and save the current file."
  (interactive "P")
  (save-buffer)
  (if (or use-ormolu my/haskell-use-ormolu)
      (ormolu-format-buffer)
    (progn
      (my/haskell-align-and-sort-imports)
      (my/haskell-align-and-sort-language-extensions)))
  (save-buffer))

(defun my/haskell-align-and-sort-imports ()
  (interactive)
  (save-excursion
    (goto-char 0)
    (let ((n-runs 0)
          (max-runs 10))
      (while (and (< n-runs max-runs)
                  (haskell-navigate-imports))
        (progn
          (setq n-runs (1+ n-runs))
          (when my/haskell-align-stuff (call-interactively 'haskell-align-imports))
          (call-interactively 'haskell-sort-imports)))
      (if (>= n-runs max-runs)
          (message "Sorting/aligning imports probably timed out")))))

(defun my/-haskell-mark-language-extensions ()
  (interactive)
  (deactivate-mark)
  (goto-char 0)
  (re-search-forward "^{-# LANGUAGE" nil t)
  (beginning-of-line 1)
  (call-interactively 'set-mark-command)
  (while (re-search-forward "^{-# LANGUAGE" nil t)
    nil)
  (end-of-line 1))

(defun my/haskell-align-and-sort-language-extensions ()
  (interactive)
  (save-excursion
    (when my/haskell-align-stuff
      (my/-haskell-mark-language-extensions)
      (align-regexp (region-beginning) (region-end) "\\(\\s-*\\)#-"))
    (my/-haskell-mark-language-extensions)
    (sort-lines nil (region-beginning) (region-end))))

(defun my/haskell-insert-language-extension ()
  (interactive)
  (let* ((all-exts
          (split-string (shell-command-to-string "ghc --supported-languages")))
         (ext
          (completing-read "extension: "
                           all-exts
                           nil nil nil nil nil)))
    (save-excursion
      (goto-char 0)
      (re-search-forward "^{-#" nil t)
      (beginning-of-line 1)
      (open-line 1)
      (insert (format "{-# LANGUAGE %s #-}" ext)))))

(defun my/haskell-insert-ghc-option ()
  (interactive)
  (let* ((all-opts
          (split-string (shell-command-to-string "ghc --show-options")))
         (ext
          (completing-read "option: "
                           all-opts
                           nil nil nil nil nil)))
    (save-excursion
      (goto-char 0)
      (re-search-forward "^module" nil t)
      (beginning-of-line 1)
      (open-line 1)
      (insert (format "{-# OPTIONS_GHC %s #-}" ext)))))

(defun my/haskell-align-and-sort-everything ()
  (interactive)
  (my/haskell-align-and-sort-imports)
  (my/haskell-align-and-sort-language-extensions))

(defun my/haskell-insert-ghcid-repl-statement (new-line)
  (interactive "P")
  (setq current-prefix-arg nil)
  (when new-line
    (end-of-line 1)
    (call-interactively 'newline))
  (beginning-of-line 1)
  (call-interactively 'delete-horizontal-space)
  (insert "-- $> "))

(defun my/haskell-open-haddock-documentation (use-eww)
  (interactive "P")
  (let ((url "https://haskell-haddock.readthedocs.io/en/latest/markup.html"))
    (if use-eww
        (eww url)
      (browse-url url))))

(defvar my/ghc-source-path (expand-file-name "~/sources/ghc/"))

(defun my/visit-ghc-tags-table ()
  (interactive)
  (let ((tags (expand-file-name "TAGS" my/ghc-source-path)))
    (if (file-exists-p tags)
        (visit-tags-table tags)
      (error "No TAGS file found in ghc source directory"))))
(use-package dante
  :ensure t
  :after haskell-mode
  :commands (dante-mode)
  :hook (haskell-mode . dante-mode)
  :init
  (setq dante-methods '(new-impure-nix))
  (add-hook 'dante-mode-hook
            '(lambda () (flycheck-add-next-checker
                         'haskell-dante
                         '(warning . haskell-hlint)))))

coq (proof-general)

(use-package proof-general
  :ensure t
  :init
  (setq proof-splash-enable nil
        proof-script-fly-past-comments t))

(use-package holes
  :ensure proof-general
  :after proof-general coq-mode
  :commands (holes-mode)
  :diminish)

(use-package coq-mode
  :ensure proof-general
  :mode (("\\.v\\'" . coq-mode))
  :bind (:map coq-mode-map
              ("C-c ." . proof-electric-terminator-toggle)
              ("M-e" . forward-paragraph)
              ("M-a" . backward-paragraph)
              ("M-RET" . proof-goto-point)
              ("M-n" . proof-assert-next-command-interactive)
              ("M-p" . proof-undo-last-successful-command))
  :init
  (setq coq-one-command-per-line nil
        coq-compile-before-require t)
  :config
  (my/add-hooks
   '(my/after-set-theme-hook)
   (when (fboundp 'coq-highlight-selected-hyps)
     (coq-highlight-selected-hyps)))
  (my/add-hooks
   '(coq-mode-hook)
   (setq evil-shift-width 2)
   (push '(?# . ("(* " . " *)")) evil-surround-pairs-alist)
   (undo-tree-mode 1)
   (whitespace-mode 1))

  (defun my/coq-browse-stdlib ()
    (interactive)
    (browse-url "https://coq.inria.fr/library/"))

  (my/define-major-mode-key 'coq-mode "t" 'engine/search-coq-tactics)
  (my/define-major-mode-key 'coq-mode "i" 'my/coq-browse-stdlib)
  ;; use yas-expand instead
  (define-key coq-mode-map (kbd "<C-return>") nil))

python

(use-package python
  :mode ("\\.py\\'" . python-mode)
  :init
  (setq python-shell-prompt-detect-failure-warning nil
        py-outline-minor-mode-p nil)
  :config
  (setq-default flycheck-disabled-checkers
                (append flycheck-disabled-checkers
                        '(python-pycompile python-pylint python-pyright python-mypy)))
  (flycheck-add-next-checker 'python-flake8 '(warning . python-mypy))
  (my/add-hooks
   '(python-mode-hook)
   (setq fill-column 100)
   (setq evil-shift-width 4)
   (my/python-enable-checkers))
  (defun my/python-format-and-save ()
    (interactive)
    (blacken-buffer)
    (py-isort-before-save)
    (save-buffer))
  (my/define-major-mode-key 'python-mode "s" 'my/python-format-and-save)
  (my/define-major-mode-key 'python-mode "v" 'my/python-poetry-venv-activate-current))

(use-package blacken
  :ensure t
  :if (executable-find "black")
  :after python
  :commands (blacken-mode blacken-buffer)
  :diminish)

(use-package py-isort
  :ensure t
  :if (executable-find "isort")
  :after python
  :commands (py-isort-buffer py-isort-before-save))

(define-minor-mode my/python-format-on-save-mode
  "Minor mode for autoformatting python buffers on save."
  :lighter " pyf"
  :global nil
  (if my/python-format-on-save-mode
      (if (eq major-mode 'python-mode)
          (progn
            (blacken-mode +1)
            (add-hook 'before-save-hook #'py-isort-before-save nil :local))
        (progn
          (setq my/python-format-on-save-mode nil)
          (user-error "Not a python-mode buffer")))
    (progn
      (blacken-mode -1)
      (remove-hook 'before-save-hook #'py-isort-before-save :local))))

(use-package elpy
  :ensure t
  :hook ((python-mode . elpy-enable))
  :diminish
  :init
  (setq elpy-modules '(elpy-module-sane-defaults
                       elpy-module-company
                       ;; elpy-module-eldoc
                       elpy-module-pyvenv))
  (setq eldoc-idle-delay 1)
  (setq python-shell-interpreter "ipython"
        python-shell-interpreter-args "-i --simple-prompt"))

(use-package pyvenv
  :ensure t
  :after python
  :commands (pyvenv-workon)
  :config
  (defun my/mode-line-extra-python-mode ()
    (let ((venv pyvenv-virtual-env-name))
      (format "(%s) " (or venv "-")))))

(use-package pyenv-mode
  :ensure t
  :after python
  :commands (pyenv-mode pyenv-mode-set pyenv-mode-unset))

(use-package poetry
  :ensure t
  :after python
  :commands (poetry poetry-venv-workon)
  :config
  (my/define-major-mode-key 'python-mode "v" 'poetry-venv-workon))

(defvar my/python-poetry-env-path
  (if is-mac
      (expand-file-name "~/Library/Caches/pypoetry/virtualenvs")))

(defun my/python-poetry-venv-activate ()
  (interactive)
  (let* ((all-envs (directory-files my/python-poetry-env-path
                                    nil
                                    directory-files-no-dot-files-regexp))
         (env (completing-read "env: "
                               all-envs))
         (env-path (expand-file-name env my/python-poetry-env-path)))
    (my/python-activate-venv (env-path))))

(defun my/python-poetry-venv-activate-current ()
  (interactive)
  (pyvenv-deactivate)
  (let ((venv (shell-command-to-string "printf %s \"$(poetry env info --path --no-ansi)\"")))
    (my/python-activate-venv venv)))

(defun my/python-activate-venv (venv)
  (message (format "Activating venv in %s" venv))
  (pyvenv-activate venv)
  (my/python-enable-checkers))

(defun my/python-enable-checkers ()
  (dolist (conf '(("mypy" . python-mypy)
                  ("flake8" . python-flake8)))
    (let ((exe (car conf))
          (checker (cdr conf)))
      (if (executable-find exe)
          (flycheck--toggle-checker checker t)))))

(defun eshell/workon (arg) (pyvenv-workon arg))
(defun eshell/deactivate () (pyvenv-deactivate))

(add-to-list 'auto-mode-alist '("\\.flake8\\'" . conf-toml-mode))

rust

(use-package rust-mode
  :ensure t
  :mode (("\\.rs\\'" . rust-mode))
                                        ; :hook (rust-mode . lsp)
  )

(use-package cargo
  :ensure t
  :after rust-mode
  :hook (rust-mode . cargo-minor-mode)
  :diminish cargo-minor-mode)

(use-package flycheck-rust
  :ensure t
  :after (rust-mode flycheck)
  :init
  (with-eval-after-load 'rust-mode
    (add-hook 'flycheck-mode-hook #'flycheck-rust-setup)))

(use-package racer
  :ensure t
  :after rust-mode
  :hook (rust-mode . racer-mode)
  :diminish racer-mode
  :init
  :config
  (let* ((root (string-trim
                (shell-command-to-string "rustc --print sysroot")))
         (rust-src (expand-file-name "lib/rustlib/src/rust/library/" root)))
    (setq racer-rust-src-path rust-src)))

ruby

;; https://github.com/howardabrams/dot-files/blob/master/emacs-ruby.org
(use-package enh-ruby-mode
  :ensure t
  :mode (("_spec\\.rb\\'" . ruby-rspec-mode)
         ("\\.rb\\'" . enh-ruby-mode)
         ("Gemfile\\'" . enh-ruby-mode)
         ("\\.rake\\'" . ruby-rake-mode)
         ("Rakefile\\'" . ruby-rake-mode))
  :init
  (setq enh-ruby-indent-level 2
        enh-ruby-indent-tabs-mode nil
        enh-ruby-check-syntax nil)

  (define-derived-mode ruby-rake-mode enh-ruby-mode "ruby-rake")
  (define-derived-mode ruby-rspec-mode enh-ruby-mode "ruby-rspec")

  (defface my/rake-keyword-face
    '((t (:inherit font-lock-function-name-face)))
    "Face for highlighting rake task keywords."
    :group 'my/faces)

  (defface my/rspec-keyword-face
    '((t (:inherit font-lock-function-name-face)))
    "Face for highlighting rspec test keywords."
    :group 'my/faces)

  (defvar my/rake-keywords '("namespace" "desc" "task"))
  (defvar my/rspec-keywords
    '(
      "describe" "context" "it" "before"
      "let"
      "expect" "expect_any_instance_of"
      "allow" "allow_any_instance_of"
      "shared_examples" "it_behaves_like"
      "include_context"
      ))
  :config
  (my/add-hooks
   '(enh-ruby-mode-hook)
   (setq evil-shift-width 2)
   (setq fill-column 120)
   (when (buffer-file-name)
     (add-hook 'before-save-hook #'my/ruby-insert-frozen-string-literal nil :local)))

  (my/define-major-mode-key 'enh-ruby-mode "s" 'my/ruby-format-and-save)

  (dolist (kw my/rake-keywords)
    (my/highlight-keyword-in-mode 'ruby-rake-mode kw nil 'my/rake-keyword-face))

  (dolist (kw my/rspec-keywords)
    (my/highlight-keyword-in-mode 'ruby-rspec-mode kw nil 'my/rspec-keyword-face))

  (custom-set-faces '(enh-ruby-op-face ((t nil))))
  (custom-set-faces '(enh-ruby-string-delimiter-face ((t (:inherit font-lock-string-face)))))
  )

(use-package robe
  :ensure t
  :after enh-ruby-mode
  :hook (enh-ruby-mode . robe-mode)
  :diminish)

(use-package rbenv
  :ensure t
  :after enh-ruby-mode
  :commands (rbenv--locate-file rbenv-use-corresponding)
  ;; :hook (enh-ruby-mode . rbenv-use-corresponding)
  :init
  ;; testing this, runs on every buffer switch so might add overhead
  (defvar my/rbenv-current-version-file nil)
  (defun my/rbenv-use-corresponding ()
    (interactive)
    (when (eq major-mode 'enh-ruby-mode)
      (let ((version-file-path (or (rbenv--locate-file ".ruby-version")
                                   (rbenv--locate-file ".rbenv-version"))))
        (when (and version-file-path
                   (not (string-equal version-file-path
                                      my/rbenv-current-version-file)))
          (setq my/rbenv-current-version-file version-file-path)
          (rbenv-use-corresponding)))))

  (add-hook 'buffer-list-update-hook 'my/rbenv-use-corresponding)
  :config
  (defun my/mode-line-extra-enh-ruby-mode ()
    (let ((version (rbenv--active-ruby-version)))
      (format "(%s) " (or version "-")))))

(use-package rubocopfmt
  :ensure t
  :after enh-ruby-mode
  :commands (rubocopfmt rubocopfmt-mode)
  :init
  (defun my/ruby-format-and-save ()
    (interactive)
    (call-interactively 'rubocopfmt)
    (save-buffer)))

(defvar my/ruby-frozen-string-literal "# frozen_string_literal: true")
(defvar my/ruby-do-insert-frozen-string-literal t)
(defun my/ruby-insert-frozen-string-literal ()
  (interactive)
  (when my/ruby-do-insert-frozen-string-literal
    (save-excursion
      (goto-char (point-min))
      (unless (re-search-forward my/ruby-frozen-string-literal nil t)
        (goto-char (point-min))
        (newline 2)
        (previous-line 2)
        (insert my/ruby-frozen-string-literal)))))

clojure

(use-package cider
  :ensure t
  :commands (cider-jack-in)
  :diminish)

(use-package clojure-mode
  :ensure t
  :mode (("\\.clj\\'" . clojure-mode)
         ("\\.edn\\'" . clojure-mode)))

javascript, typescript, html, css

(use-package rjsx-mode
  :ensure t
  :mode (("\\.jsx?\\'" . rjsx-mode))
  :init
  (setq js2-mode-show-strict-warnings nil)
  :config
  (my/define-major-mode-key 'rjsx-mode "s" 'my/prettier-and-save)
  (my/define-major-mode-key 'rjsx-mode "d" 'js-doc-insert-function-doc)
  (my/define-major-mode-key 'rjsx-mode "D" 'js-doc-insert-file-doc)
  (my/add-hooks
   '(rjsx-mode-hook)
   (setq evil-shift-width 2)
   (define-key js2-mode-map (kbd "C-c C-f") nil)))

(use-package flow-js2-mode
  :ensure t
  :after rjsx-mode
  :diminish)

(use-package typescript-mode
  :ensure t
  :mode (("\\.ts\\'" . typescript-mode))
  :init
  (setq typescript-indent-level 2)
  :config
  (my/define-major-mode-key 'typescript-mode "s" 'my/prettier-and-save)
  (my/add-hooks
   '(typescript-mode-hook)
   (subword-mode 1)
   (setq evil-shift-width 2)))

(use-package js-doc
  :ensure t
  :commands (js-doc-insert-function-doc
             js-doc-insert-file-doc))

(use-package nvm
  :ensure t
  :commands (nvm-use nvm-use-for nvm-use-for-buffer)
  :init
  (defun my/nvm-auto-use ()
    (when (locate-dominating-file (buffer-file-name) ".nvmrc")
      (nvm-use-for-buffer)))
  (defun my/nvm ()
    (interactive)
    (nvm-use-for-buffer))
  (my/add-hooks
   '(rjsx-mode-hook
     typescript-mode-hook
     web-tsx-mode-hook
     web-jsx-mode-hook)
   (my/nvm-auto-use)))

(use-package js
  :commands (js-mode)
  :init
  (setq js-indent-level 2))

(use-package prettier-js
  :ensure t
  :commands (prettier-js prettier-js-mode)
  :init
  (defun my/prettier-and-save ()
    (interactive)
    (prettier-js)
    (save-buffer)))

(use-package add-node-modules-path
  :ensure t
  :commands (add-node-modules-path)
  :hook ((js-mode rjsx-mode typescript-mode web-tsx-mode) . add-node-modules-path))

(use-package web-mode
  :ensure t
  :mode (("\\.html\\'" . web-html-mode)
         ("\\.tsx\\'" . web-tsx-mode))
  :init
  (setq web-mode-markup-indent-offset 2
        web-mode-css-indent-offset 2
        web-mode-code-indent-offset 2
        web-mode-attr-indent-offset 2
        web-mode-enable-auto-quoting nil)
  (define-derived-mode web-tsx-mode web-mode "web-tsx")
  (define-derived-mode web-html-mode web-mode "web-html")
  :config
  ;; web-tsx-mode
  (my/define-major-mode-key 'web-tsx-mode "s" 'my/prettier-and-save)
  (custom-set-faces
   '(web-mode-keyword-face ((t (:inherit font-lock-keyword-face)))))
  (my/add-hooks
   '(web-tsx-mode)
   (subword-mode 1)))

(with-eval-after-load 'mhtml-mode
  (define-key mhtml-mode-map (kbd "M-o") nil))

(use-package css-mode
  :mode (("\\.css\\'" . css-mode))
  :init
  (setq css-indent-offset 2
        css-fontify-colors nil))

(use-package emmet-mode
  :ensure t
  :commands (emmet-expand-line)
  :bind (:map web-html-mode-map
              ("<C-return>" . emmet-expand-line)
              :map html-mode-map
              ("<C-return>" . emmet-expand-line)
              :map css-mode-map
              ("<C-return>" . emmet-expand-line))
  :hook ((web-html-mode html-mode css-mode) . emmet-mode))

purescript

(use-package purescript-mode
  :ensure t
  :mode ("\\.purs\\'" . purescript-mode)
  :init
  (setq purescript-indent-offset 2
        purescript-align-imports-pad-after-name t)
  :config
  (my/define-major-mode-key 'purescript-mode "a" 'my/purescript-sort-and-align-imports)
  (my/define-major-mode-key 'purescript-mode "i" 'purescript-navigate-imports)
  (my/define-major-mode-key 'purescript-mode "s" 'my/purescript-format-and-save)
  (my/define-major-mode-key 'purescript-mode "/" 'engine/search-pursuit)
  (add-hook
   'purescript-mode-hook
   (lambda ()
     (setq evil-shift-width 2)
     (turn-on-purescript-indentation)
     (turn-on-purescript-decl-scan)
     ;; (turn-on-purescript-font-lock)
     (push '(?# . ("{- " . " -}")) evil-surround-pairs-alist)
     (subword-mode 1)
     (make-variable-buffer-local 'find-tag-default-function)
     (setq find-tag-default-function (lambda () (current-word t t)))
     ))
  ;; xref for purescript works a bit weird with qualified identifiers
  ;; (define-key purescript-mode-map (kbd "M-.")
  ;; #'(lambda () (interactive) (xref-find-definitions (current-word t t))))
  )

(defvar my/purescript-align-stuff t)

(defun my/purescript-sort-and-align-imports ()
  (interactive)
  (save-excursion
    (goto-line 1)
    (while (purescript-navigate-imports)
      (progn
        (purescript-sort-imports)
        (when my/purescript-align-stuff (purescript-align-imports))))
    (purescript-navigate-imports-return)))

(defun my/purescript-format-and-save ()
  "Formats the import statements using haskell-stylish and saves
the current file."
  (interactive)
  (my/purescript-sort-and-align-imports)
  (save-buffer))

all lisps

;; expand macros in another window
(define-key
  lisp-mode-map
  (kbd "C-c C-m")
  #'(lambda () (interactive) (macrostep-expand t)))

(my/add-hooks
 '(lisp-mode-hook emacs-lisp-mode-hook lisp-interaction-mode-hook)
 (eldoc-mode))

emacs lisp

(use-package elisp-mode
  :mode (("\\.el\\'" . emacs-lisp-mode)
         ("\\.elc\\'" . elisp-byte-code-mode))
  :config
  (defun my/emacs-lisp-format-and-save ()
    (interactive)
    (my/indent-region-or-buffer)
    (save-buffer))

  (my/define-major-mode-key 'emacs-lisp-mode "s" #'my/emacs-lisp-format-and-save))

(use-package eros
  :ensure t
  :after elisp-mode
  :config
  (eros-mode))

scala

(use-package scala-mode
  :ensure t
  :mode (("\\.scala\\'" . scala-mode))
  :interpreter ("scala" . scala-mode)
  :hook (scala-mode . lsp))

(use-package sbt-mode
  :ensure t
  :after scala-mode
  :commands (sbt-start sbt-command)
  :config
  ;; WORKAROUND: https://github.com/ensime/emacs-sbt-mode/issues/31
  ;; allows using SPACE when in the minibuffer
  (substitute-key-definition 'minibuffer-complete-word
                             'self-insert-command
                             minibuffer-local-completion-map)
  ;; sbt-supershell kills sbt-mode:  https://github.com/hvesalai/emacs-sbt-mode/issues/152
  (setq sbt:program-options '("-Dsbt.supershell=false")))

(use-package lsp-metals
  :ensure t
  :after scala-mode
  :init
  (setq lsp-metals-treeview-show-when-views-received t))

ocaml

(use-package tuareg
  :ensure t
  :mode ("\\.ml[ip]?\\'" . tuareg-mode))

agda

;; currently managed by nixos (emacsPackages.agda2-mode)
(use-package agda2-mode
  :mode ("\\.l?agda\\'" . agda2-mode)
  :config
  (my/add-hooks
   '(agda2-mode-hook)
   (activate-input-method "Agda")))

idris

(use-package idris-mode
  :ensure t
  :mode ("\\.idr\\'" . idris-mode))

c, c++

(setq c-default-style "linux"
      c-basic-offset 4)

dhall

(use-package dhall-mode
  :ensure t
  :mode ("\\.dhall\\'" . dhall-mode)
  :init
  (setq dhall-format-at-save nil
        dhall-format-arguments '("--ascii"))

  (with-eval-after-load 'smartparens
    (with-eval-after-load 'dhall-mode
      (sp-local-pair 'dhall-mode "\\(" ")")))

  :config
  (defun my/dhall-format-and-save ()
    (interactive)
    (dhall-format-buffer)
    (save-buffer))

  (my/add-hooks
   '(dhall-mode-hook)
   (setq indent-tabs-mode nil
         evil-shift-width 2))
  (my/define-major-mode-key 'dhall-mode "s" #'my/dhall-format-and-save))

bazel

(use-package bazel
  :ensure t
  :mode (("\\.bazel\\'" . bazel-mode)
         ("\\.bzl\\'" . bazel-mode)
         ("\\.star\\'" . bazel-starlark-mode))
  :config
  (defun my/bazel-format-and-save ()
    (interactive)
    (let* ((fn (file-name-nondirectory buffer-file-name))
           (ext (file-name-extension fn))
           (tp (cond
                ((string= fn "BUILD.bazel") "build")
                ((string= ext "bzl") "bzl")
                (t (user-error (format "Not a bazel file extension: %s" ext))))))
      (my/format-and-save "buildifier" "--type" tp))
    (save-buffer))
  (my/define-major-mode-key 'bazel-mode "s" #'my/bazel-format-and-save))

nginx

(use-package nginx-mode
  :ensure t
  :mode (("nginx\\.conf\\'" . nginx-mode)
         ("nginx\\.conf\\.template\\'" . nginx-mode)))

terraform

(use-package terraform-mode
  :ensure t
  :mode ("\\.tf\\'" . terraform-mode)
  :config
  (defun my/terraform-format-and-save ()
    (interactive)
    (terraform-format-buffer)
    (save-buffer))
  (my/define-major-mode-key 'terraform-mode "s" #'my/terraform-format-and-save)
  (my/add-hooks
   '(terraform-mode-hook)
   (when (executable-find "terraform")
     (terraform-format-on-save-mode +1))))

docker

(use-package dockerfile-mode
  :ensure t
  :mode ("Dockerfile.*" . dockerfile-mode))

elasticsearch

(add-to-list 'auto-mode-alist '(".es\\'" . js-mode))

json

(use-package json-mode
  :ensure t
  :mode (("\\.json\\'" . json-mode)
         ("\\.json.tmpl\\'" . json-mode)
         ("\\.json.template\\'" . json-mode))
  :config
  (defun my/json-format-and-save ()
    (interactive)
    (json-mode-beautify)
    (save-buffer))
  (my/define-major-mode-key 'json-mode "s" #'my/json-format-and-save))

yaml

(use-package yaml-mode
  :ensure t
  :mode (("\\.ya?ml\\'" . yaml-mode)
         ("\\.ya?ml.tmpl\\'" . yaml-mode)
         ("\\.ya?ml.template\\'" . yaml-mode)
         ("\\.ya?ml.sample\\'" . yaml-mode))
  :config
  (my/add-hooks
   '(yaml-mode-hook)
   (setq evil-shift-width 2))
  )

(use-package flycheck-yamllint
  :ensure t
  :after (flycheck yaml-mode)
  :commands (flycheck-yamllint-setup)
  :hook (yaml-mode . flycheck-yamllint-setup))

conf

;; add env files to conf-mode alist
(add-to-list 'auto-mode-alist '("\\.env\\'" . conf-mode))
(add-to-list 'auto-mode-alist '("\\.env.*\\'" . conf-mode))
(add-to-list 'auto-mode-alist '("env\\.example\\'" . conf-mode))
(add-to-list 'auto-mode-alist '("\\.env\\..*\\.sample\\'" . conf-mode))
(add-to-list 'auto-mode-alist '("\\.env.sample\\'" . conf-mode))

cucumber

(use-package feature-mode
  :ensure t
  :mode (("\\.feature\\'" . feature-mode))
  :init
  ;; add keywords
  (dolist
    (kw '("Example"
          "Rule"
          "Scenario Template"
          ))
  (font-lock-add-keywords
   'feature-mode
   `((,(format "^[ 	]*\\(%s\\):?" kw) . ((1 '(face font-lock-keyword-face))))))))

graphviz

(use-package graphviz-dot-mode
  :ensure t
  :mode (("\\.dot\\'" . graphviz-dot-mode))
  :init
  (setq graphviz-dot-indent-width 4))

mermaid

(use-package mermaid-mode
  :ensure t
  :mode (("\\.mmd\\'" . mermaid-mode)
         ("\\.mermaid\\'" . mermaid-mode)))

mustache

(use-package mustache-mode
  :ensure t
  :mode (("\\.mustache\\'" . mustache-mode))
  :init
  (setq mustache-basic-offset 2)
  :config
  (defconst my/mustache-mode-unescape
    (concat "\\({{&\s*" mustache-mode-mustache-token "\s*}}\\)"))
  (font-lock-add-keywords
   'mustache-mode
   `((,my/mustache-mode-unescape (1 font-lock-variable-name-face))))
  (add-hook 'mustache-mode-hook #'evil-local-mode))

editorconfig

(use-package editorconfig
  :ensure t
  :mode (("\\.editorconfig\\'" . editorconfig-conf-mode)))

WRITING

markdown

(defvar my/markdown-css
  `(,(expand-file-name "static/github.css" user-emacs-directory)
    ,(expand-file-name "static/pygments.css" user-emacs-directory)))

(use-package markdown-mode
  :ensure t
  :commands (markdown-mode gfm-mode)
  :bind (:map markdown-mode-map
              ("M-a" . beginning-of-defun)
              ("M-e" . end-of-defun)
              :map markdown-mode-command-map
              ("r" . ivy-bibtex))
  :mode (("README\\.md\\'" . gfm-mode)
         ("\\.md\\'" . markdown-mode)
         ("\\.markdown\\'" . markdown-mode)
         ("\\.mdx\\'" . markdown-mode)
         ("\\.page\\'" . gfm-mode))
  :init
  (setq markdown-asymmetric-header t
        markdown-enable-wiki-links t
        markdown-wiki-link-fontify-missing t
        markdown-enable-math t
        markdown-gfm-use-electric-backquote nil
        markdown-list-indent-width 2
        markdown-command "pandoc --highlight-style=pygments --mathjax --to html"
        markdown-css-paths my/markdown-css)

  (defface my/markdown-pandoc-native-div-face
    '((t (:inherit font-lock-preprocessor-face)))
    "Face for highlighting pandoc native div blocks (starting with ':::')"
    :group 'my/faces)

  ;; (font-lock-add-keywords
  ;;  'markdown-mode
  ;;  '(("^::::*.*" 0 'my/markdown-pandoc-native-div-face)))

  (defface my/markdown-shortcut-reference-link-face
    '((t (:inherit markdown-link-face)))
    "Face for highlighting shortcut reference links ([link]) in pandoc markdown."
    :group 'my/faces)

  (font-lock-add-keywords
   'markdown-mode
   '(("\\(\\[\\)\\([^]@][^]]*?\\)\\(\\]\\)[^[]" . ((1 markdown-markup-properties)
                                           (2 '(face my/markdown-shortcut-reference-link-face))
                                           (3 markdown-markup-properties)))))

  (defface my/markdown-citation-face
    '((t (:inherit font-lock-function-name-face)))
    "Face for highlighting citations ([@citation]) in pandoc markdown."
    :group 'my/faces)

  (font-lock-add-keywords
   'markdown-mode
   '(("\\(\\[@\\)\\(.+?\\)\\(\\]\\)[^[]" . ((1 markdown-markup-properties)
                                            (2 '(face my/markdown-citation-face))
                                            (3 markdown-markup-properties)))))

  (defface my/markdown-mustache-curly-face
    '((t (:inherit font-lock-preprocessor-face)))
    "Face for highlighting template boundaries in pandoc markdown."
    :group 'my/faces)

  (defface my/markdown-mustache-modifier-face
    '((t (:inherit font-lock-preprocessor-face)))
    "Face for highlighting template modifiers (#, / or ^) in pandoc markdown."
    :group 'my/faces)

  (defface my/markdown-mustache-variable-face
    '((t (:inherit font-lock-warning-face)))
    "Face for highlighting template variables ({{ var }}) in pandoc markdown."
    :group 'my/faces)

  (font-lock-add-keywords
   'markdown-mode
   '(("\\({{\\)\\([#/^&]?\\)\\(.+?\\)\\(}}\\)"
      . ((1 '(face my/markdown-mustache-curly-face))
         (2 '(face my/markdown-mustache-modifier-face))
         (3 '(face my/markdown-mustache-variable-face))
         (4 '(face my/markdown-mustache-curly-face))))))

  :config
  (if (executable-find "marked")
      (setq markdown-command "marked"))
  (my/define-major-mode-key 'markdown-mode "c" 'check-parens)
  (my/define-major-mode-key 'markdown-mode "o" 'my/writeroom)
  (my/add-hooks
   '(markdown-mode-hook)
   (setq evil-shift-width 2)
   (setq fill-column 100)
   (auto-fill-mode 1)
   (whitespace-mode +1)
   (push '(?# . ("<!-- " . " -->")) evil-surround-pairs-alist))
  )

(use-package markdown-toc
  :ensure t
  :after markdown-mode
  :commands (markdown-toc-refresh-toc
             markdown-toc-generate-toc
             markdown-toc-generate-or-refresh-toc)
  :init
  (defalias 'mtoc 'markdown-toc-generate-or-refresh-toc))

writeroom

(use-package writeroom-mode
  :ensure t
  :commands (writeroom-mode)
  :init
  (setq writeroom-global-effects nil
        writeroom-fringes-outside-margins nil
        writeroom-maximize-window nil
        writeroom-width 100
        writeroom-header-line nil
        my/writeroom-fill-column-prev nil)
  (setq writeroom-mode-line
        '((:eval
           (my/split-mode-line-render
            ;; left
            (quote
             ("%e" " "
              mode-line-modified "  "
              mode-line-buffer-identification " "
              (:eval (my/mode-line-major-mode-extra))
              ))
            ;; right
            (quote
             ((:eval (my/mode-line-input-method))
              (:eval (my/mode-line-region-info))
              mode-line-position
              " "
              ))
            ))))
  (defun my/writeroom (variable-pitch)
    (interactive "P")
    (if (and (boundp 'writeroom-mode) writeroom-mode)
        (progn
          (writeroom-mode -1)
          (variable-pitch-mode -1)
          (when my/writeroom-fill-column-prev
            (display-fill-column-indicator-mode)))
      (setq-local writeroom-width fill-column)
      (writeroom-mode)
      (when variable-pitch
        (variable-pitch-mode))
      (setq-local my/writeroom-fill-column-prev
                  (and
                   (boundp 'display-fill-column-indicator-mode)
                   display-fill-column-indicator-mode))
      (display-fill-column-indicator-mode -1))))

reStructuredText

(use-package rst
  :mode ("\\.rst\\'" . rst-mode)
  :bind (:map rst-mode-map
              ("M-a" . rst-backward-section)
              ("M-e" . rst-forward-section))
  :init
  (setq rst-indent-width 2)
  :config
  (my/add-hooks
   '(rst-mode-hook)
   (setq evil-shift-width rst-indent-width)))

asciidoc

(use-package adoc-mode
  :ensure t
  :mode ("\\.adoc\\'" . adoc-mode))

LaTeX

(use-package tex-mode
  :ensure auctex
  :mode (("\\.tex\\'" . latex-mode))
  :init
  (defvar-local my/texcount nil)
  (defun my/texcount-update ()
    (let ((fname (buffer-file-name)))
      (when (and (eq major-mode 'latex-mode)
                 (not (null fname)))
        (setq-local my/texcount
                    (string-trim
                     (shell-command-to-string
                      (format "texcount -sum -brief -total %s" fname)))))))
  (setq TeX-brace-indent-level 0
        LaTeX-item-indent 0
        LaTeX-indent-level 2)
  :config
  (my/add-hooks
   '(LaTeX-mode-hook)
   (smartparens-mode)
   (whitespace-mode)
   (when (executable-find "texcount")
     (my/texcount-update)
     (add-hook 'after-save-hook #'my/texcount-update nil t)))

  (defun my/mode-line-extra-latex-mode ()
    (if (null my/texcount)
        "(-)"
      (let ((face (if (buffer-modified-p)
                      'compilation-warning
                    'compilation-info)))
        (format "(%s)"
                (propertize my/texcount
                            'face
                            face))))))

(use-package latex-preview-pane
  :ensure t
  :after tex-mode
  :commands (latex-preview-pane-mode
             latex-preview-pane-enable))

(use-package ivy-bibtex
  :ensure t
  :commands (ivy-bibtex)
  :config
  (defalias 'ib 'ivy-bibtex)
  (setq bibtex-completion-cite-prompt-for-optional-arguments nil
        ivy-bibtex-default-action 'ivy-bibtex-insert-citation))

elfeed

(use-package elfeed-web
  :ensure t
  :commands (elfeed-web-start)
  :after elfeed
  :config
  (defun my/elfeed-web-browse ()
    (interactive)
    (browse-url "http://localhost:8080/elfeed/")))

(use-package elfeed
  :ensure t
  :commands (elfeed)
  :bind (:map elfeed-search-mode-map
              ("U" . elfeed-update)
              ("o" . my/elfeed-search-other-window)
              ("t" . my/elfeed-filter-in-tag)
              ("T" . my/elfeed-filter-out-tag)
              ("f" . my/elfeed-filter-in-feed)
              ("q" . my/elfeed-kill-buffer-close-window-dwim)
              ("e" . my/elfeed-show-eww)
              ("x" . my/elfeed-search-org-capture)
              ("h" . backward-char)
              ("j" . next-line)
              ("k" . previous-line)
              ("l" . forward-char)
              :map elfeed-show-mode-map
              ("i" . my/elfeed-hide-images)
              ("e" . my/elfeed-show-eww)
              ("q" . my/elfeed-kill-buffer-close-window-dwim)
              ("b" . my/elfeed-show-browse-url)
              ("x" . my/elfeed-show-org-capture)
              ("h" . backward-char)
              ("j" . next-line)
              ("k" . previous-line)
              ("l" . forward-char))
  :init
  (setq elfeed-db-directory (expand-file-name "~/.elfeed")
        elfeed-use-curl t
        elfeed-curl-max-connections 10
        elfeed-search-clipboard-type 'CLIPBOARD
        elfeed-search-filter "@5-days-ago +unread "
        elfeed-search-title-max-width 100
        elfeed-search-title-min-width 30
        elfeed-search-trailing-width 30)
  (setq my/elfeed-org-capture-default-filename (expand-file-name "saved-elfeed-posts.org" my/org-directory))
  :config
  (defun my/elfeed-hide-images (tog)
    (interactive "P")
    (let ((shr-inhibit-images (not tog)))
      (elfeed-show-refresh)))

  (defun my/elfeed-all-visible-feeds ()
    "Return an alist (name -> feed struct) of all currently shown feeds"
    ;; `-compare-fn' is used internally by `-distinct', to remove duplicate
    ;; feeds by name (`car')
    (let ((-compare-fn (-on 'eq 'car)))
      (-distinct
       (-annotate 'elfeed-feed-title
                  (mapcar 'elfeed-entry-feed elfeed-search-entries)))))

  (defun my/elfeed-filter-in-feed ()
    (interactive)
    (call-interactively 'elfeed-search-clear-filter)
    (let* ((feeds (my/elfeed-all-visible-feeds))
           (old-filter elfeed-search-filter)
           (selection (completing-read "Feed: " feeds nil :require-match))
           (feed (cdr (assoc selection feeds)))
           (feed-url (elfeed-feed-url feed))
           (new-filter (format "%s =%s" old-filter feed-url)))
      (elfeed-search-set-filter new-filter)))

  (defun my/elfeed-all-visible-tags ()
    "Return a list of all currently shown tags"
    (-distinct
     (apply #'append
            (mapcar #'elfeed-entry-tags elfeed-search-entries))))

  (defun my/elfeed-filter-tag (clear inverse)
    (when clear
      (call-interactively 'elfeed-search-clear-filter))
    (let* ((tags (my/elfeed-all-visible-tags))
           (old-filter elfeed-search-filter)
           (selection (completing-read "Tag: " tags nil :require-match))
           (op (if inverse "-" "+"))
           (new-filter (format "%s %s%s" old-filter op selection)))
      (elfeed-search-set-filter new-filter)))

  (defun my/elfeed-filter-in-tag (clear)
    (interactive "P")
    (my/elfeed-filter-tag clear nil))

  (defun my/elfeed-filter-out-tag (clear)
    (interactive "P")
    (my/elfeed-filter-tag clear t))

  ;; prot
  (defun my/elfeed-search-other-window (&optional horz)
    (interactive "P")
    (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                      elfeed-show-entry
                    (elfeed-search-selected :ignore-region)))
           (link (elfeed-entry-link entry))
           (win (selected-window)))
      (with-current-buffer (get-buffer "*elfeed-search*")
        (when (null (get-buffer-window "*elfeed-entry*"))
          (if horz
              (split-window win (/ (frame-width) 3) 'right)
            (split-window win (/ (frame-height) 3) 'below)))
        (other-window 1)
        (elfeed-search-show-entry entry))))

  (defun my/elfeed-show-eww (&optional link)
    (interactive)
    (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                      elfeed-show-entry
                    (elfeed-search-selected :ignore-region)))
           (link (if link link (elfeed-entry-link entry))))
      (eww link)
      (add-hook 'eww-after-render-hook 'eww-readable nil t)))

  (defun my/elfeed-show-browse-url ()
    (interactive)
    (browse-url (elfeed-entry-link elfeed-show-entry)))

  (defun my/elfeed-kill-buffer-close-window-dwim ()
    (interactive)
    (let ((win (selected-window)))
      (cond ((eq major-mode 'elfeed-show-mode)
             (elfeed-kill-buffer)
             (unless (one-window-p) (delete-window win))
             (switch-to-buffer "*elfeed-search*"))
            ((eq major-mode 'elfeed-search-mode)
             (if (one-window-p)
                 (elfeed-search-quit-window)
               (delete-other-windows win))))))

  (defun my/elfeed-org-capture-get-tags (entry)
    (let* ((all-tags (elfeed-entry-tags entry))
           (tags (seq-filter #'(lambda (tag) (not (eq tag 'unread))) all-tags))
           (tags-str (mapcar #'(lambda (tag) (format "%s" tag)) tags)))
      (if (null tags)
          ""
        (string-join tags-str ", "))))

  (defvar my/elfeed-org-capture-entry nil)

  (defun my/elfeed-org-capture (entry immediate)
    (let ((my/elfeed-org-capture-entry entry)
          (template (if immediate "E" "e")))
      (org-capture nil template)))

  (defun my/elfeed-search-org-capture (immediate)
    (interactive "P")
    (my/elfeed-org-capture (elfeed-search-selected :ignore-region) immediate))

  (defun my/elfeed-show-org-capture (immediate)
    (interactive "P")
    (my/elfeed-org-capture elfeed-show-entry immediate))

  (add-hook 'elfeed-show-mode-hook #'(lambda () (setq-local shr-width 100)))

  (add-hook 'elfeed-new-entry-hook
            (elfeed-make-tagger :before "2 weeks ago"
                                :remove 'unread))

  ;; (add-hook 'elfeed-search-update-hook #'elfeed-db-save)
  (add-hook 'elfeed-update-init-hooks #'elfeed-db-save)

  (defface my/elfeed-important
    '((t (:foreground "salmon")))
    "Elfeed unread"
    :group 'my/faces)
  (push '(important my/elfeed-important) elfeed-search-face-alist)

  (defun my/elfeed-export-opml-feedly (file)
    "Export all feeds to FILE, categorized for import in feedly."
    (interactive "FOutput OPML file: ")
    (with-temp-file file
      (let ((standard-output (current-buffer))
            (categorized (-group-by 'cadr elfeed-feeds)))
        (princ "<?xml version=\"1.0\"?>\n")
        (xml-print
         `((opml ((version . "1.0"))
                 (head () (title () "Elfeed Export"))
                 (body ()
                       ,@(cl-loop
                          for group in (-group-by 'cadr elfeed-feeds)
                          for category = (car group)
                          for feeds = (cdr group)
                          collect `(outline
                                    ((text . ,(format "%s" category))
                                     (title . ,(format "%s" category)))
                                    ,@(cl-loop
                                       for feed in feeds
                                       for url = (car feed)
                                       for elfeed-feed = (elfeed-db-get-feed url)
                                       for maybe-title = (elfeed-feed-title elfeed-feed)
                                       for title = (if (or (null maybe-title)
                                                           (string-empty-p maybe-title))
                                                       (url-host (url-generic-parse-url url))
                                                     maybe-title)
                                       collect `(outline
                                                 ((xmlUrl . ,url)
                                                  (title . ,title)))))))))))))

  ;; get feeds from personal dir
  (load-file (expand-file-name "lisp/elfeed-feeds.el" my/dropbox-emacs-dir)))

pdf tools

(use-package pdf-tools
  :ensure t
  :defer t
  :magic ("%PDF" . pdf-view-mode)
  :bind (:map pdf-view-mode-map
              ("j" . pdf-view-next-line-or-next-page)
              ("k" . pdf-view-previous-line-or-previous-page)
              ("h" . image-backward-hscroll)
              ("l" . image-forward-hscroll)
              ("C-s" . isearch-forward)
              ("ss" . my/pdf-view-remove-margins-mode)
              ("cc" . pdf-cache-clear-data)
              )
  :init
  (setq pdf-view-display-size 'fit-page
        pdf-view-midnight-colors '("#cfe8e7" . "#0a3749")
        ;; cache stuff, testing
        pdf-cache-image-limit 15
        pdf-cache-prefetch-delay 2
        )
  ;; remove cached images after x seconds
  (setq image-cache-eviction-delay 15)
  :config
  (pdf-tools-install :no-query)

  (defun my/mode-line-extra-pdf-view-mode ()
    (let ((cur (number-to-string (pdf-view-current-page)))
          (tot (or (ignore-errors (number-to-string (pdf-cache-number-of-pages)))
                   "???")))
      (format "(%s/%s)" cur tot)))

  (define-minor-mode my/pdf-view-remove-margins-mode
    "Minor mode for removing margins from every pdf page."
    :lighter " pdf-margins"
    (if (not (eq major-mode 'pdf-view-mode))
        (user-error "Not in a pdf-view-mode buffer")
      (if my/pdf-view-remove-margins-mode
          (progn
            (pdf-view-set-slice-from-bounding-box)
            (add-hook 'pdf-view-after-change-page-hook #'pdf-view-set-slice-from-bounding-box))
        (progn
          (pdf-view-reset-slice)
          (remove-hook 'pdf-view-after-change-page-hook #'pdf-view-set-slice-from-bounding-box)))))
  )

(use-package pdf-outline
  :defer t
  :bind (:map pdf-outline-buffer-mode-map
              ("<backtab>" . outline-hide-sublevels)))

searching

isearch

C-h k C-s to get a help menu for isearch

Some useful isearch keys (before starting the search):

keydescription
M-s .isearch for thing at point

And while inside a search:

keydescription
C-wadd next word to search
C-eadd until EOL to search
M-eedit search in minibuffer (commit with RET)
M-s orun occur
M-s rtoggle regexp search
M-s .mark whole thing for search
M-s h rhighlight current search (hi-lock)
M-%run query-replace on search term
C-M-%run query-replace-regexp on search term
C-lrecenter

And some custom additions (mostly stolen from Protesilaos):

wherekeydescription
in searchC-SPCmark search and exit
in searchC-RETexit search, but move point to the other side
in search<backspace>delete failing part, one char or exit

Other useful stuff:

  • When exiting a search (with RET), C-x C-x will mark from where the search started to where it finished.
(use-package isearch
  :diminish
  :bind (:map isearch-mode-map
              ("C-l" . recenter)
              ("C-SPC" . my/isearch-mark-and-exit)
              ("<C-return>" . my/isearch-other-end)
              ("<M-backspace>" . my/isearch-abort-dwim)
              ("<C-backspace>" . my/isearch-abort-dwim))
  :init
  (setq search-whitespace-regexp ".*?"  ;; spaces match anything
        isearch-lax-whitespace t  ;; the default
        isearch-regex-lax-whitespace nil
        isearch-yank-on-move 'shift
        isearch-allow-scroll 'unlimited
        isearch-lazy-count t  ;; show match count and current match index
        lazy-count-prefix-format nil
        lazy-count-suffix-format " (%s/%s)")
  :config
  ;; prot
  (defun my/isearch-mark-and-exit ()
    (interactive)
    (push-mark isearch-other-end t 'activate)
    (setq deactivate-mark nil)
    (isearch-done))

  (defun my/isearch-other-end ()
    (interactive)
    (isearch-done)
    (when isearch-other-end
      (goto-char isearch-other-end)))

  (defun my/isearch-abort-dwim ()
    "Delete failed `isearch' input, single char, or cancel search.

This is a modified variant of `isearch-abort' that allows us to
perform the following, based on the specifics of the case: (i)
delete the entirety of a non-matching part, when present; (ii)
delete a single character, when possible; (iii) exit current
search if no character is present and go back to point where the
search started."
    (interactive)
    (if (eq (length isearch-string) 0)
        (isearch-cancel)
      (isearch-del-char)
      (while (or (not isearch-success) isearch-error)
        (isearch-pop-state)))
    (isearch-update))

  ;; https://www.reddit.com/r/emacs/comments/b7yjje/isearch_region_search/
  (defun my/isearch-region (&optional not-regexp no-recursive-edit)
    "If a region is active, make this the isearch default search pattern."
    (interactive "P\np")
    (when (use-region-p)
      (let ((search (buffer-substring-no-properties
                     (region-beginning)
                     (region-end))))
        (deactivate-mark)
        (isearch-yank-string search))))

  (advice-add 'isearch-forward :after 'my/isearch-region)
  (advice-add 'isearch-forward-regexp :after 'my/isearch-region)
  (advice-add 'isearch-backward :after 'my/isearch-region)
  (advice-add 'isearch-backward-regexp :after 'my/isearch-region)

  (with-eval-after-load 'evil
    (dolist (st '(normal visual))
      (evil-global-set-key st (kbd "gs") 'isearch-forward)
      (evil-global-set-key st (kbd "gr") 'isearch-backward)))
  )

rg

(use-package rg
  :ensure t
  :commands (rg my/rg-project-or-ask)
  :bind (("C-c g" . my/rg-project-or-ask)
         :map rg-mode-map
         ("m" . rg-menu)
         ("l" . rg-list-searches)
         ("s" . my/rg-save-search-as-name)
         ("N" . my/rg-open-ace-window)
         ("C-n" . next-line)
         ("C-p" . previous-line)
         ("j" . next-line)
         ("k" . previous-line)
         ("M-n" . rg-next-file)
         ("M-p" . rg-prev-file))
  :init
  (setq rg-group-result t
        rg-ignore-case 'smart)
  (setq rg-custom-type-aliases
        '(("coq" . "*.v")
          ("rake" . "*.rake")))
  (defalias 'rgp 'my/rg-project-or-ask)
  :config
  (rg-define-toggle "--multiline --multiline-dotall" "u")
  (rg-define-toggle "--word-regexp" "w")
  (rg-define-toggle "--files-with-matches" "L")

  (rg-define-search my/rg-org-directory
    :query ask
    :format regexp
    :files "org"
    :dir my/org-directory
    :confirm prefix)

  ;; prot
  ;; https://protesilaos.com/dotemacs/#h:31622bf2-526b-4426-9fda-c0fc59ac8f4b
  (rg-define-search my/rg-project-or-ask
    :query ask
    :format regexp
    :files "all"
    :dir (or (projectile-project-root)
             (read-directory-name "rg in: "))
    :confirm prefix)

  (defun my/rg-save-search-as-name ()
    "Save `rg' buffer, naming it after the current search query."
    (interactive)
    (let ((pattern (rg-search-pattern rg-cur-search)))
      (rg-save-search-as-name (concat "«" pattern "»"))))
  )

anzu

(use-package anzu
  :ensure t
  :hook (after-init . global-anzu-mode)
  :diminish
  :init
  (setq anzu-mode-lighter ""))

(use-package evil-anzu
  :ensure t
  :after (evil anzu))

interactively search files/folders in project by regex with fd

;; stolen from prot :)
(use-package dired-aux
  :bind (("C-c C-s" . my/dired-fd-files-and-dirs))
  :init
  (setq dired-isearch-filenames 'dwim
        dired-create-destination-dirs 'ask
        dired-vc-rename-file t)
  :config
  (defmacro my/dired-fd (name doc prompt &rest flags)
    "Make commands for selecting 'fd' results with completion.
NAME is how the function should be named.  DOC is the function's
documentation string.  PROMPT describes the scope of the query.
FLAGS are the command-line arguments passed to the 'fd'
executable, each of which is a string."
    `(defun ,name (&optional arg)
       ,doc
       (interactive "P")
       (let* ((vc (vc-root-dir))
              (dir (expand-file-name (if vc vc default-directory)))
              (regexp (read-regexp
                       (format "%s matching REGEXP in %s: " ,prompt
                               (propertize dir 'face 'bold))))
              (names (process-lines "fd" ,@flags regexp dir))
              (buf "*FD Dired*"))
         (if names
             (if arg
                 (dired (cons (generate-new-buffer-name buf) names))
               (find-file
                (completing-read (format "Items matching %s (%s): "
                                         (propertize regexp 'face 'success)
                                         (length names))
                                 names nil t))))
         (user-error (format "No matches for « %s » in %s" regexp dir)))))

  (my/dired-fd
   my/dired-fd-dirs
   "Search for directories in VC root or PWD.
With \\[universal-argument] put the results in a `dired' buffer.
This relies on the external 'fd' executable."
   "Subdirectories"
   "-i" "-H" "-a" "-t" "d" "-c" "never")

  (my/dired-fd
   my/dired-fd-files-and-dirs
   "Search for files and directories in VC root or PWD.
With \\[universal-argument] put the results in a `dired' buffer.
This relies on the external 'fd' executable."
   "Files and dirs"
   "-i" "-H" "-a" "-t" "d" "-t" "f" "-c" "never")
  )

imenu-list

(use-package imenu-list
  :ensure t
  :bind ("C-|" . my/imenu-list-smart-toggle)
  :config

  (defun my/imenu-list-jump-to-window ()
    "Jump to imenu-list window if visible, otherwise create it and jump."
    (interactive)
    (if (get-buffer-window imenu-list-buffer-name)
        (select-window (get-buffer-window imenu-list-buffer-name))
      (progn
        (imenu-list-minor-mode)
        (select-window (get-buffer-window imenu-list-buffer-name)))))

  (defun my/imenu-list-smart-toggle ()
    "If imenu-list window doesn't exist, create it and jump. If if does but
it is not the current buffer, jump there. If it exists and it's the current
buffer, close it."
    (interactive)
    (if (eq (current-buffer) (get-buffer imenu-list-buffer-name))
        (imenu-list-quit-window)
      (my/imenu-list-jump-to-window)))

  (setq imenu-list-size 40))

company

(use-package company
  :ensure t
  :diminish company-mode
  :bind (("C-M-i" . company-complete)
         :map company-active-map
         ("C-p" . company-select-previous)
         ("C-n" . company-select-next)
         ("C-f" . company-show-location)
         ("TAB" . company-complete-common-or-cycle)
         ("<tab>" . company-complete-common-or-cycle)
         ("<escape>" . company-abort))
  :hook ((after-init . global-company-mode)
         (global-company-mode . company-quickhelp-mode))
  :init
  (setq company-dabbrev-downcase nil
        company-minimum-prefix-length 3
        company-idle-delay 0.4)
  :config
  (setq company-backends (delete 'company-dabbrev company-backends))
  ;; (setq company-backends (delete 'company-capf company-backends))
  (add-to-list 'company-backends 'company-capf)
  (add-to-list 'company-backends 'company-elisp)
  (add-to-list 'company-backends 'company-files))

(use-package company-quickhelp
  :ensure t
  :after company)

flycheck

(defun my/mode-line-flycheck ())

(use-package flycheck
  :ensure t
  :diminish flycheck-mode
  :bind (("C-c ! t" . flycheck-mode))
  :hook (after-init . global-flycheck-mode)
  :init
  (setq flycheck-temp-prefix ".flycheck"
        flycheck-emacs-lisp-load-path 'inherit
        flycheck-check-syntax-automatically '(save mode-enabled)
        flycheck-markdown-markdownlint-cli-config ".markdownlint.yml"
        ;; to check while typing:
        ;; flycheck-check-syntax-automatically '(save idle-change new-line mode-enabled)
        )
  :config
  (defun my/toggle-flycheck-error-list ()
    (interactive)
    (-if-let (window (flycheck-get-error-list-window))
        (quit-window nil window)
      (flycheck-list-errors)))

  (add-to-list 'display-buffer-alist
               `(,(rx bos "*Flycheck errors*" eos)
                 (display-buffer-reuse-window
                  display-buffer-in-side-window)
                 (side            . bottom)
                 (reusable-frames . visible)
                 (window-height   . 0.33)))

  (setq-default flycheck-disabled-checkers
                (append flycheck-disabled-checkers
                        '(javascript-jshint haskell-ghc haskell-stack-ghc yaml-ruby proselint)))
  (flycheck-add-mode 'javascript-eslint 'web-mode)
  (flycheck-add-mode 'javascript-eslint 'js2-mode)
  (flycheck-add-mode 'python-mypy 'python-mode)
  (flycheck-add-mode 'python-flake8 'python-mode)

  ;; modeline stuff
  (defface modeline-flycheck-error
    '((t (:foreground "#e05e5e" :distant-foreground "#e05e5e")))
    "Face for flycheck error feedback in the modeline."
    :group 'modeline-flycheck)
  (defface modeline-flycheck-warning
    '((t (:foreground "#bfb03d" :distant-foreground "#bfb03d")))
    "Face for flycheck warning feedback in the modeline."
    :group 'modeline-flycheck)
  (defface modeline-flycheck-info
    '((t (:foreground "DeepSkyBlue3" :distant-foreground "DeepSkyBlue3")))
    "Face for flycheck info feedback in the modeline."
    :group 'modeline-flycheck)
  (defface modeline-flycheck-ok
    '((t (:foreground "SeaGreen3" :distant-foreground "SeaGreen3")))
    "Face for flycheck ok feedback in the modeline."
    :group 'modeline-flycheck)

  (defvar modeline-flycheck-bullet "•%s")

  (defun my/mode-line-flycheck-state (state)
    (let* ((counts (flycheck-count-errors flycheck-current-errors))
           (errorp (flycheck-has-current-errors-p state))
           (err (or (cdr (assq state counts)) "?"))
           (running (eq 'running flycheck-last-status-change))
           (face (intern (format "modeline-flycheck-%S" state))))
      (if (or errorp running)
          (propertize (format modeline-flycheck-bullet err) 'face face))))

  (defun my/mode-line-flycheck ()
    (let* ((ml-error (my/mode-line-flycheck-state 'error))
           (ml-warning (my/mode-line-flycheck-state 'warning))
           (ml-info (my/mode-line-flycheck-state 'info))
           (ml-status (concat ml-error ml-warning ml-info)))
      (if (null ml-status) "" (concat " " ml-status " ")))))

projectile

configuration

(use-package projectile
  :ensure t
  :hook (after-init . projectile-mode)
  :bind-keymap ("C-c p" . projectile-command-map)
  :diminish projectile-mode
  :init
  (setq projectile-completion-system 'ivy
        projectile-mode-line-function
        '(lambda () (format " P[%s]" (or (projectile-project-name) "-")))))

(use-package perspective
  :ensure t
  :hook (after-init . persp-mode)
  :bind (("M-N" . persp-next)
         ("M-P" . persp-prev)
         ("M-J" . persp-switch))
  :init
  (setq persp-mode-prefix-key (kbd "C-x x"))
  :config
  ;; accidentally changing perspectives while in the minibuffer messes things up
  ;; using `ignore' (rather than nil) makes these keys do nothing
  (add-hook 'minibuffer-setup-hook
            '(lambda ()
               (dolist (k '("M-N" "M-P" "M-J"))
                 (local-set-key (kbd k) 'ignore))))
  (advice-add 'persp-switch
              :after
              #'(lambda (n &optional r)
                  (message (persp-name (persp-curr)))))
  ;; emacs window title
  (setq frame-title-format
        '("" invocation-name
          (:eval (when persp-mode (format "[%s]" (persp-name (persp-curr))))))))

(use-package persp-projectile
  :ensure t
  :after (perspective projectile))

project name override (for use with persp)

;; override chosen project names
(defvar my/projectile-project-name-overrides '())

(defun my/projectile-add-to-project-name-overrides (proj name)
  (add-to-list
   'my/projectile-project-name-overrides
   `(,(file-name-as-directory (expand-file-name proj)) . ,name)))

(defun my/projectile-override-project-name (orig &rest args)
  (let* ((dir (file-name-as-directory (expand-file-name (car args))))
         (match (assoc dir my/projectile-project-name-overrides))
         (name (if (null match) nil (cdr match))))
    (if (null name)
        (apply orig args)
      name)))

(advice-add 'projectile-default-project-name :around #'my/projectile-override-project-name)

;; usage:
;; (dolist (override '(
;;                     ("/path/to/my/project" . "some-name")
;;                     ("/other/project" . "some-other-name")
;;                     ))
;;   (let ((proj (car override))
;;         (name (cdr override)))
;;     (my/projectile-add-to-project-name-overrides proj name)))

ivy/counsel/swiper

counsel-projectile

(use-package counsel-projectile
  :ensure t
  :after projectile
  :bind (:map projectile-command-map
              ("f" . counsel-projectile-find-file)
              ("s" . counsel-projectile-rg)
              ("b" . counsel-projectile-switch-to-buffer))
  :init
  (setq projectile-switch-project-action 'counsel-projectile-find-file))

swiper

(defun my/swiper (fuzzy)
  (interactive "P")
  (if fuzzy
      (let* ((temp-builders
              (copy-alist ivy-re-builders-alist))
             (ivy-re-builders-alist
              (add-to-list 'temp-builders
                           '(swiper . ivy--regex-fuzzy))))
        (swiper))
    (swiper)))

(defun my/swiper-fuzzy-or-all (all)
  (interactive "P")
  (if all
      (swiper-all)
    (my/swiper :fuzzy)))

(defun my/swiper-isearch (fuzzy)
  (interactive "P")
  (if fuzzy
      (let* ((temp-builders
              (copy-alist ivy-re-builders-alist))
             (ivy-re-builders-alist
              (add-to-list 'temp-builders
                           '(swiper-isearch . ivy--regex-fuzzy))))
        (swiper-isearch))
    (swiper-isearch)))

(use-package swiper
  :ensure t
  :bind (("C-c f" . my/swiper-fuzzy-or-all))
  :commands (swiper swiper-isearch swiper-all swiper-multi))

flx, amx

;; better fuzzy matching
(use-package flx
  :ensure t
  :after ivy)

;; mostly to bring recently used M-x targets at the top
;; (trying out instead of `smex`)
(use-package amx
  :ensure t
  :after ivy
  :init
  (setq amx-backend 'auto
        amx-save-file (expand-file-name "amx-items" user-emacs-directory)
        amx-history-length 50
        amx-show-key-bindings nil)
  :config
  (amx-mode +1))

ivy, counsel

(defun my/ivy-reset-builders ()
  (setq ivy-re-builders-alist
        '((swiper               . ivy--regex-plus)
          (swiper-isearch       . ivy--regex-plus)
          (ivy-bibtex           . ivy--regex-ignore-order)
          (counsel-unicode-char . ivy--regex-ignore-order)
          (insert-char          . ivy--regex-ignore-order)
          (ucs-insert           . ivy--regex-ignore-order)
          (counsel-unicode-char . ivy--regex-ignore-order)
          (counsel-ag           . ivy--regex-ignore-order)  ;; NOTE: testing
          (counsel-rg           . ivy--regex-ignore-order)
          (t                    . ivy--regex-fuzzy))))

(defun my/counsel-rg-in ()
  (interactive)
  (counsel-rg nil (read-directory-name "rg in: ") ""))

(defun my/counsel-file-jump-temp-root (reset)
  (interactive "P")
  (my/get-or-set-temp-root reset)
  (let ((current-prefix-arg nil))
    (counsel-file-jump nil my/temp-project-root)))

(defun my/counsel-rg-temp-root (reset)
  (interactive "P")
  (my/get-or-set-temp-root reset)
  (let ((current-prefix-arg nil))
    (counsel-rg "" my/temp-project-root)))

(defun my/set-temp-root-and-jump (dir)
  (setq my/temp-project-root dir)
  (my/counsel-file-jump-temp-root nil))

(defun my/counsel-file-jump-from-here (path)
  (interactive)
  (let ((dir (if (file-directory-p path)
                 path
               (file-name-directory path))))
    (counsel-file-jump "" dir)))

(defun my/ivy-insert-relative (fn)
  (let* ((trimmed-fn (ivy--trim-grep-line-number fn))
         (curdir (file-name-directory (buffer-file-name)))
         (rel-fn (file-relative-name trimmed-fn curdir)))
    (insert rel-fn)))

(use-package counsel
  :ensure t
  :after ivy
  :bind (("M-x" . counsel-M-x)
         ("M-i" . counsel-imenu)
         ("C-x C-f" . counsel-find-file)
         ("C-x f" . counsel-file-jump)
         ("C-x r b" . counsel-bookmark)
         ("C-x C-a" . counsel-recentf)
         ("C-c s" . my/counsel-rg-in)
         ("C-S-p" . my/counsel-file-jump-temp-root)
         ("C-S-s" . my/counsel-rg-temp-root)
         :map org-mode-map
         ("M-i" . counsel-outline)
         :map help-map
         ("f" . counsel-describe-function)
         ("o" . counsel-describe-symbol)
         ("u" . counsel-describe-face)
         ("v" . counsel-describe-variable))
  :diminish
  :init
  (setq counsel-rg-base-command "rg --with-filename --no-heading --line-number --color never -S %s"
        counsel-ag-base-command "ag --vimgrep --nocolor --nogroup %s"
        counsel-find-file-ignore-regexp "\\`.flycheck_\\|__pycache__")
  :config
  (my/ivy-reset-builders)
  (setq ivy-initial-inputs-alist nil) ;; no ^ initially
  ;; counsel-ag
  ;; S-SPC doesn't work properly in counsel-ag anyway
  ;; NOTE: this also applies to rg
  (define-key counsel-ag-map (kbd "S-SPC") nil)

  (defvar my/ripgrep-config (expand-file-name "~/.config/ripgrep/ripgreprc"))
  (when (file-exists-p my/ripgrep-config)
    (setenv "RIPGREP_CONFIG_PATH" my/ripgrep-config))

  (dolist (action '(counsel-find-file counsel-file-jump counsel-recentf))
    (ivy-set-actions
     action
     `(
       ("I"
        my/ivy-insert-relative
        "insert relative")
       ("s"
        ,(my/control-function-window-split
          find-file-other-window
          0 nil)
        "split horizontally")
       ("v"
        ,(my/control-function-window-split
          find-file-other-window
          nil 0)
        "split vertically")
       ("n"
        ,(my/execute-f-with-hook
          find-file
          ace-select-window)
        "select window")
       ("e"
        my/eshell
        "eshell")
       ("j"
        my/counsel-file-jump-from-here
        "jump")
       ("J"
        my/set-temp-root-and-jump
        "set temp root and jump")
       ("r"
        (lambda (dir) (counsel-rg nil dir))
        "counsel-rg")
       )))

  (dolist (action '(counsel-projectile-find-file projectile-recentf))
    (ivy-set-actions
     action
     `(("s"
        ,(my/control-function-window-split
          counsel-projectile-find-file-action-other-window
          0 nil)
        "split horizontally")
       ("v"
        ,(my/control-function-window-split
          counsel-projectile-find-file-action-other-window
          nil 0)
        "split vertically")
       ("n"
        ,(my/execute-f-with-hook
          counsel-projectile-find-file-action
          ace-select-window)
        "select window")
       ("R"
        (lambda (f) (projectile-recentf))
        "recent files")
       ("e"
        (lambda (f) (my/eshell (projectile-expand-root f)))
        "eshell")
       )))

  ;; also applies to counsel-projectile-ag
  (dolist (action '(counsel-ag counsel-rg))
    (ivy-set-actions
     action
     '(("v"
        (lambda (x) (split-window-right) (windmove-right) (counsel-git-grep-action x))
        "split vertically")
       ("s"
        (lambda (x) (split-window-below) (windmove-down) (counsel-git-grep-action x))
        "split horizontally")
       ("n"
        (lambda (x) (ace-select-window) (counsel-git-grep-action x))
        "select window")
       ))))

(defun my/ivy-yank-current-region-or-word (&optional qual)
  "Insert current region, if it's active, otherwise the current word,into
the minibuffer."
  (interactive "P")
  (let (text)
    (with-ivy-window
      (unwind-protect
          (setq text
                (if (region-active-p)
                    (buffer-substring-no-properties (region-beginning) (region-end))
                  (current-word t (not qual))))))
    (when text (insert text))))

(use-package ivy
  :ensure t
  :diminish ivy-mode
  :hook (after-init . ivy-mode)
  :bind (("C-c r" . ivy-resume)
         ("C-x b" . ivy-switch-buffer)
         :map ivy-minibuffer-map
         ("M-j" . my/ivy-yank-current-region-or-word)
         ("M-r" . ivy-rotate-preferred-builders)
         ("C-l" . ivy-call-and-recenter)
         ("C-o" . ivy-minibuffer-grow)
         ("C-S-o" . ivy-minibuffer-shrink))
  :init
  (setq ivy-use-virtual-buffers nil
        ivy-count-format "(%d/%d) "
        ivy-magic-tilde nil
        ivy-initial-inputs-alist nil)
  :config
  (my/ivy-reset-builders)

  ;; minibuffer actions for specific commands
  (ivy-set-actions
   'ivy-switch-buffer
   `(("s"
      ,(my/control-function-window-split
        ivy--switch-buffer-other-window-action
        0 nil)
      "split horizontally")
     ("v"
      ,(my/control-function-window-split
        ivy--switch-buffer-other-window-action
        nil 0)
      "split vertically")
     ("n"
      ,(my/execute-f-with-hook
        (lambda (b) (switch-to-buffer b nil 'force-same-window))
        ace-select-window)
      "select window")
     ("k" kill-buffer "kill buffer")
     ))

  (ivy-set-actions
   'projectile-switch-project
   '(("d"
      dired
      "Open Dired in project's directory")
     ("v"
      projectile-vc
      "Open project root in vc-dir or magit")
     ("r"
      projectile-remove-known-project
      "Remove project(s)"))))

(use-package ivy-xref  ;; currently in lisp/ because of patches
  :commands (ivy-xref-show-xrefs)
  :init (setq xref-show-xrefs-function 'ivy-xref-show-xrefs)
  :after ivy)

yasnippet

(use-package yasnippet-snippets
  :ensure t
  :after yasnippet
  :config
  (yas-reload-all))

(use-package yasnippet
  :ensure t
  :bind (("C-c y" . yas-expand)
         ("<C-return>" . yas-expand))
  :diminish (yas-global-mode yas-minor-mode)
  :config
  ;; NOTE: the reason for not putting it in `after-init-hook' is to defer
  ;; loading until it `yas-expand' has been run the first time
  (yas-global-mode +1))

org-mode

org-babel

;; this is the same as doing org-babel-load-languages
(use-package ob-python :commands (org-babel-execute:python))
(use-package ob-haskell :commands (org-babel-execute:haskell))
(use-package ob-sql :commands (org-babel-execute:sql))
(use-package ob-lilypond
  :commands (org-babel-execute:lilypond)
  :custom
  (org-babel-lilypond-commands '("lilypond -daux-files=#f" "xdg-open" "xdg-open"))
  :config
  (defun my/org-lilypond-preprocess-block (args)
    (let* ((defaults (string-join
                      '("\\layout{"
                        "#(layout-set-staff-size 25)"
                        "}"
                        "\\paper{"
                        "indent=0\\mm"
                        "line-width=200\\mm"
                        "oddFooterMarkup=##f"
                        "oddHeaderMarkup=##f"
                        "bookTitleMarkup=##f"
                        "scoreTitleMarkup=##f"
                        "}"
                        )
                      "\n"))
           (body (car args))
           (newbody (format "%s\n%s" defaults body))
           (params (cadr args)))
      (list newbody params)))
  (advice-add 'org-babel-execute:lilypond
              :filter-args
              'my/org-lilypond-preprocess-block))

org-protocol

;; use this bookmark:
;; javascript:location.href='org-protocol://capture?template=r'+
;;   '&url='+encodeURIComponent(location.href)+
;;   '&title='+encodeURIComponent(document.title)+
;;   '&body='+encodeURIComponent(window.getSelection())
(require 'org-protocol)
;; Without this, quitting an org-protocol capture results in re-opening
;; the link with another mimeapp (firefox), and might result in an
;; infinite loop. This still deletes the client, but it does so cleanly.
;; TODO: think of a better way
(advice-add 'server-return-error
            :override
            '(lambda (proc err)
               (message "exiting client")
               (server-delete-client proc)))

org configuration

(use-package org-indent
  :after org
  :hook (org-mode . org-indent-mode)
  :commands (org-indent-mode)
  :diminish)

(use-package org-src
  :after org
  :bind (:map org-src-mode-map
              ("C-c C-c" . org-edit-src-exit))
  :init
  (setq org-src-fontify-natively t
        org-src-tab-acts-natively t
        org-src-window-setup 'other-window
        org-src-preserve-indentation t))

(use-package ox
  :after org
  :init
  (setq org-export-with-toc nil
        org-export-with-section-numbers 1))

(use-package org
  :mode ("\\.org\\'" . org-mode)
  :bind (("C-c l" . org-store-link)
         ("C-c &" . org-mark-ring-goto))
  :init
  (setq org-directory my/org-directory
        org-default-notes-file (expand-file-name "notes.org" org-directory)
        org-log-done 'time
        org-confirm-babel-evaluate nil
        org-clock-into-drawer t
        org-log-into-drawer t
        org-keep-stored-link-after-insertion t
        org-edit-src-content-indentation 0
        org-src-window-setup 'other-window
        org-adapt-indentation nil
        org-ellipsis ""
        org-tags-column -80
        org-image-actual-width (list 500)
        org-startup-with-inline-images t
        org-blank-before-new-entry '((heading . nil) (plain-list-item . nil))
        org-todo-keywords '((sequence "TODO(!)" "IN PROGRESS(!)" "|" "DONE(!)" "CANCELLED(!)"))
        org-todo-keyword-faces '(("IN PROGRESS" . (:foreground "DodgerBlue" :weight bold))
                                 ("CANCELLED" . (:foreground "red3")))
        org-fontify-done-headline nil
        org-treat-insert-todo-heading-as-state-change t
        org-link-frame-setup '((file . find-file))
        org-habit-graph-column 50
        org-habit-show-habits-only-for-today nil)

  ;; templates
  (setq org-structure-template-alist
        '(("s" . "src")
          ("e" . "example")
          ("q" . "quote")
          ("v" . "verse")
          ("ht" . "export html")
          ("la" . "export latex")
          ("b" . "src bash")
          ("sh" . "src shell")
          ("sr" . "src ruby")
          ("t" . "src terraform")
          ("j" . "src json")
          ("y" . "src yaml")
          ("el" . "src emacs-lisp")
          ("h" . "src haskell")
          ("p" . "src python")
          ("py" . "src python")
          ))

  (setq org-tempo-keywords-alist
        '(("I" . "INDEX")
          ("L" . "LATEX")
          ("T" . "TITLE")
          ("H" . "HTML")))

  ;; refile
  (setq org-refile-targets '((nil . (:maxlevel . 1)))
        org-refile-use-outline-path 'file
        org-outline-path-complete-in-steps nil)
  ;; format string used when creating CLOCKSUM lines and when generating a
  ;; time duration (avoid showing days)
  (setq org-time-clocksum-format
        '(:hours "%d" :require-hours t :minutes ":%02d" :require-minutes t))

  :config
  (require 'org-tempo nil :noerror)
  (require 'org-ref nil :noerror)

  (my/define-major-mode-key 'org-mode "c" 'org-cliplink)
  (my/define-major-mode-key 'org-mode "o" 'my/writeroom)
  (my/define-major-mode-key 'org-mode "it" 'my/org-insert-date-today)
  (my/define-major-mode-key 'org-mode "ti" 'org-toggle-inline-images)
  (my/define-major-mode-key 'org-mode "tl" 'org-toggle-link-display)
  (my/define-major-mode-key 'org-mode "tm" 'my/org-toggle-markup)

  (add-hook 'org-babel-after-execute-hook 'org-display-inline-images 'append)

  (my/add-hooks
   '(org-mode-hook)
   (define-key org-mode-map (kbd "TAB") 'org-cycle)
   (define-key org-mode-map (kbd "<tab>") 'org-cycle)
   (evil-define-key 'normal org-mode-map (kbd "TAB") 'org-cycle)
   (evil-define-key 'normal org-mode-map (kbd "<tab>") 'org-cycle)
   (whitespace-mode +1)
   (smartparens-mode)
   (setq fill-column 100)
   )

  (with-eval-after-load 'smartparens
    (sp-local-pair 'org-mode "=" "=")
    (sp-local-pair 'org-mode "'" "'" :actions '(rem))
    (sp-local-pair 'org-mode "\"" "\"" :actions '(rem)))

  ;; kill any "unsaved" fontification buffer that might cause a save prompt when quitting emacs
  (advice-add 'save-buffers-kill-terminal
              :before
              #'my/org-kill-fontification-buffers))

(defun my/org-insert-date-today (active)
  (interactive "P")
  (org-insert-time-stamp (current-time) nil (not active)))

(defun my/org-toggle-markup ()
  (interactive)
  (setq org-hide-emphasis-markers (not org-hide-emphasis-markers))
  (font-lock-fontify-buffer :interactively))

(defun my/org-export-conf ()
  (interactive)
  (let ((org-export-with-toc t)
        (org-export-with-section-numbers 1)
        (org-html-htmlize-output-type 'css)
        (org-html-head-extra
         (string-join
          '("<link href=\"/htmlize.css\" rel=\"stylesheet\">"
            "<link href=\"/readtheorg.css\" rel=\"stylesheet\">"
            ) "\n")))
    (with-temp-buffer
      (insert-file (expand-file-name "configuration.org" user-emacs-directory))
      (org-export-to-file 'html (expand-file-name "docs/index.html" user-emacs-directory)))))

(defun my/org-kill-fontification-buffers (&optional silent)
  (mapc
   #'kill-buffer
   (seq-filter
    (lambda (buf)
      (string-match "^\\ \\*org-src-fontification:.*\\*$" (buffer-name buf)))
    (buffer-list))))

various extensions

(use-package org-bullets
  :ensure t
  :if is-gui
  :after org
  :hook (org-mode . org-bullets-mode)
  :init
  (setq org-bullets-bullet-list (append '("") (make-list 7 ""))
        org-hide-leading-stars t))

(use-package htmlize
  :ensure t
  :defer t
  :init
  (setq org-html-htmlize-output-type 'inline-css))

modeline

doom-modeline

(use-package doom-modeline
  :ensure t
  :if is-gui
  :hook (after-init . doom-modeline-mode)
  :init
  (setq doom-modeline-height 21
        doom-modeline-hud t
        doom-modeline-icon nil
        doom-modeline-bar-width 8)

  (defun my/doom-modeline--font-height ()
    "Calculate the actual char height of the mode-line."
    (+ (frame-char-height) 2))

  (advice-add #'doom-modeline--font-height :override #'my/doom-modeline--font-height)
  :config
  (add-hook 'my/after-set-font-hook 'doom-modeline-refresh-font-width-cache))

Setup

macos specific

(when is-mac
  (setq mac-command-modifier 'meta)
  (global-set-key [?\A-\C-i] nil))

Per-workstation setup

(defvar my/after-init-hook nil "Hook called after initialization")

;; add extra keys
(defvar my/extra-key-mappings nil "Extra key mappings")
(defun my/add-key (key func)
  (define-key my/leader-map (kbd key) func)
  (evil-leader/set-key key func))
;; these have to be paths to projects that projectile recognizes (e.g. git)
(defvar my/start-up-projects '())

(defun my/open-start-up-projects ()
  (unless (null my/start-up-projects)
    (let ((projectile-switch-project-action 'projectile-dired))
      (dolist (proj my/start-up-projects)
        (projectile-persp-switch-project proj)))
    (persp-switch "main")))

(add-hook 'my/after-init-hook 'my/open-start-up-projects)

;; usage:
;; (add-to-list 'my/start-up-projects "/path/to/some/project")
;; https://nicolas.petton.fr/blog/per-computer-emacs-settings.html
(defvar my/hosts-dir (expand-file-name (expand-file-name "hosts/" user-emacs-directory)))
(defvar my/hostname (substring (shell-command-to-string "hostname") 0 -1))
(let* ((host-file (concat my/hosts-dir "init-" my/hostname ".el")))
  (load-file host-file))
(dolist (mapping my/extra-key-mappings)
  (let ((key (car mapping))
        (func (cdr mapping)))
    (my/add-key key func)))

Performance

;; stolen from doom
(defun my/defer-garbage-collection-h ()
  (setq gc-cons-threshold 100000000))

(defun my/restore-garbage-collection-h ()
  ;; Defer it so that commands launched immediately after will enjoy the
  ;; benefits.
  (run-at-time
   1 nil (lambda () (setq gc-cons-threshold 800000))))

(add-hook 'minibuffer-setup-hook #'my/defer-garbage-collection-h)
(add-hook 'minibuffer-exit-hook #'my/restore-garbage-collection-h)

Global setup

;; make underlines show under mode-line (cleaner)
(setq x-underline-at-descent-line t)

(my/set-theme)
(my/set-font)

(setq linum-format 'dynamic)

(setq default-input-method "greek")

(winner-mode)

(run-hooks 'my/after-init-hook)

(use-package server
  :commands (server-mode server-running-p)
  :hook (after-init . my/maybe-server-mode)
  :init
  (defun my/maybe-server-mode ()
    (unless (server-running-p) (server-mode +1))))

Custom

(dolist (val '((eval . (setq flycheck-disabled-checkers
                             (append flycheck-disabled-checkers
                                     (quote
                                      (intero)))))
               (haskell-hoogle-command . "stack hoogle -- --count=100")
               (projectile-tags-command . "npm run etags")
               (projectile-tags-command . "fast-tags -e -R .")
               (projectile-tags-command . "fast-tags -e -R -o %s --exclude=\"%s\" \"%s\"")
               (psc-ide-output-directory . "build/")
               (my/use-intero . t)
               (my/haskell-align-stuff . nil)
               (my/haskell-use-ormolu . t)
               (my/purescript-align-stuff . nil)
               (org-download-image-dir . "~/Dropbox/emacs/org/static/images/")
               (my/ruby-do-insert-frozen-string-literal . nil)
               (flycheck-markdown-mdl-rules . nil)
               (flycheck-markdown-mdl-style . nil)
               (eval progn
                     (visual-line-mode 1)
                     (visual-fill-column-mode 1))
               ))
  (add-to-list 'safe-local-variable-values val))

(dolist (val '(bibtex-completion-bibliography))
  (put val 'safe-local-variable (lambda (_) t)))

About

My emacs configuration using org-mode

https://alexpeits.github.io/emacs.d


Languages

Language:Emacs Lisp 91.9%Language:CSS 5.8%Language:YASnippet 2.3%Language:Shell 0.0%