Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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

My emacs configuration using org-mode
https://github.com/alexpeits/emacs.d

emacs

Last synced: 2 months ago
JSON representation

My emacs configuration using org-mode

Awesome Lists containing this project

README

        

* Variables and PATH

#+BEGIN_SRC emacs-lisp
(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 "[email protected]")

(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")
#+END_SRC

* Package management
** package.el

#+begin_src emacs-lisp
(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))
#+end_src

** use-package

#+begin_src emacs-lisp
(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))
#+end_src

** load required packages

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

* Various utility functions

#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC

* Various configurations
** disable custom file

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

** basic editing

#+BEGIN_SRC emacs-lisp
;; 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) ((control) . nil)) ;; one line at a time
mouse-wheel-progressive-speed nil ;; 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
("" . 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
("" . 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)
#+END_SRC

** advise raise-frame with wmctrl (linux only)

#+begin_src emacs-lisp
(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))
#+end_src

** compilation

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** Smartparens

Paredit keys:

| key | opposite | description | example |
|---------+----------+-----------------------+---------------------------------|
| =C-M-f= | =C-M-b= | forward/backward sexp | =_(...)(...)= <-> =(...)_(...)= |
| =C-M-d= | =C-M-u= | down-up sexp | =_(...)= <-> =(_...)= |
| =C-M-n= | =C-M-p= | up-down sexp (end) | =(..._)= <-> =(...)_= |

#+BEGIN_SRC emacs-lisp
(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-" . sp-splice-sexp-killing-backward)
("M-" . 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-" . sp-forward-slurp-sexp)
("C-}" . sp-forward-barf-sexp)
("C-" . sp-forward-barf-sexp)
("C-(" . sp-backward-slurp-sexp)
("C-M-" . sp-backward-slurp-sexp)
("C-{" . sp-backward-barf-sexp)
("C-M-" . 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)
#+END_SRC

** Documentation & help

#+BEGIN_SRC emacs-lisp
(use-package which-key
:ensure t
:hook (after-init . which-key-mode)
:diminish which-key-mode)
#+END_SRC

** mark

#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC

** abbrev etc

#+begin_src emacs-lisp
(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)))
#+end_src

** engine-mode

#+BEGIN_SRC emacs-lisp
(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 "#")
)
#+END_SRC

** browser

#+begin_src emacs-lisp
(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))))
#+end_src

** prettify symbols

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

** recentf

#+BEGIN_SRC emacs-lisp
(use-package recentf
:hook (after-init . recentf-mode)
:init
(setq recentf-max-saved-items 100))
#+END_SRC

** hi-lock & symbol overlay

#+begin_src emacs-lisp
(use-package hi-lock
:hook (after-init . global-hi-lock-mode)
:init
(setq hi-lock-face-defaults
'(
"hi-green-b"
"hi-red-b"
"hi-blue-b"
"hi-black-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)
#+end_src

** highlight keywords in some modes

#+BEGIN_SRC emacs-lisp
(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
python-ts-mode
idris-mode
agda-mode
rust-mode
c-mode
emacs-lisp-mode
coq-mode
enh-ruby-mode
))
(my/highlight-comment-keywords mode))
#+END_SRC

** alignment

#+begin_src emacs-lisp
(use-package align
:bind ("C-c \\" . align-regexp))
#+end_src

** temp project roots

#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC

** edit-indirect

#+BEGIN_SRC emacs-lisp
(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))))
#+END_SRC

** which-function-mode

#+begin_src emacs-lisp
(use-package which-func
:commands (which-function-mode)
:hook ((after-init . which-function-mode))
:init
(setq which-func-modes '(python-mode python-ts-mode org-mode)
which-func-unknown "?"))
#+end_src

#+begin_src emacs-lisp :tangle no
(my/add-hooks
'(python-mode-hook python-ts-mode-hook)
(setq header-line-format
'(""
(:eval (doom-modeline--buffer-file-name buffer-file-name buffer-file-truename 'shrink))
" → "
which-func-format))
(setq mode-line-misc-info
(assq-delete-all 'which-function-mode mode-line-misc-info))
)

(setq which-func-format '((:propertize which-func-current face which-func)))
(setq doom-modeline-buffer-file-name-style 'file-name)
#+end_src

* term & eshell
** terms

#+BEGIN_SRC emacs-lisp
(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))))
#+END_SRC

** eshell

#+BEGIN_SRC emacs-lisp
(use-package em-hist :after eshell)

(use-package eshell
:commands (eshell)
:bind (("C-!" . my/eshell)
("" . 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"))))))
#+END_SRC

** vterm

#+begin_src emacs-lisp
;; NOTE: on NixOS this is managed by the OS, not melpa
(use-package vterm
:ensure t
:commands (vterm)
:bind (("C-@" . my/vterm)
("" . 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)))
#+end_src

* UI
** various

#+BEGIN_SRC emacs-lisp
;; 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 python-ts-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))
#+END_SRC

** highlight trailing whitespace

#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC

* Theme
** theme loading

#+BEGIN_SRC emacs-lisp
(setq custom--inhibit-theme-enable nil)

(setq my/avail-themes '((wombat . dark)))
(defvar my/current-theme 0)

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

(defun my/set-theme (&optional theme)
"Themes can be theme-name, function, or (theme-name . dark-or-light)"
(let ((theme (or theme (elt my/avail-themes my/current-theme))))
(mapc 'disable-theme custom-enabled-themes)
(cond
((functionp theme) (funcall theme))
((listp theme) (let* ((theme-sym (car theme))
(variant (cdr theme))
(overrides (cond
((eq variant 'dark) 'my/dark-theme-overrides)
((eq variant 'light) 'my/light-theme-overrides)
(t (error (format "Unknown theme variant '%s'" (symbol-name variant)))))))
(load-theme theme-sym t)
(funcall overrides theme-sym)))
(t (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))
#+END_SRC

** theme overrides

Mostly header-line customisation

#+begin_src emacs-lisp
(defun my/dark-theme-overrides (theme)
(custom-theme-set-faces
theme
`(fill-column-indicator ((t :foreground "#646464")))
;; `(header-line ((t :background "#303030" :box "#757575")))
`(header-line ((t :background "#2a123a" :box "#5c2e70")))
`(doom-modeline-project-dir ((t :foreground "#d38faf")))
`(doom-modeline-project-parent-dir ((t :foreground "#989898")))
`(which-func ((t :weight bold :foreground "#00bcff")))))

(defun my/light-theme-overrides (theme)
(custom-theme-set-faces
theme
`(fill-column-indicator ((t :foreground "#c4c4c4")))
`(header-line ((t :background "#f5f5f5" :box "#c4c4c4")))
`(doom-modeline-project-dir ((t :foreground "#8f0075")))
`(doom-modeline-project-parent-dir ((t :foreground "#9f9f9f")))
`(which-func ((t :weight bold :foreground "#3548cf")))))
#+end_src

** modus themes

#+begin_src emacs-lisp
(use-package modus-themes
:ensure t
:init
(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")))
(setq modus-vivendi-tinted-palette-overrides
'((bg-region bg-added-refine)))
(setq modus-operandi-palette-overrides nil))
#+end_src

** ef-themes

#+begin_src emacs-lisp
(use-package ef-themes
:ensure t
:init
(setq ef-night-palette-overrides
'((bg-region bg-cyan-subtle))))
#+end_src

* Fonts

#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC

* VCS
** vc

Common prefix is =C-x v=

Some useful commands:

| key | name | description |
|----------------+---------------------+---------------------------------------------------|
| C-x v C-h | - | show help for vc-related actions |
| C-x v p | =my/vc-project= | run =vc-dir= in repo root |
| C-x v v | =vc-next-action= | next logical action in a repo (init, add, commit) |
| C-x v d /or/ = | =vc-diff= | show diff for current file |
| C-x v D | =vc-root-diff= | show diff for whole repo |
| C-x v a | =vc-annotate= | show history, color-coded |
| C-x v h | =vc-region-history= | show history (buffer or region) |
| C-x v l | =vc-print-log= | show log for current file |
| C-x v + | =vc-update= | pull |
| C-x v P | =vc-push= | push |

In =vc-git-log-edit-mode=:

| key | name | description |
|---------+------------------------+---------------|
| C-c C-c | =log-edit-done= | save commit |
| C-c C-k | =log-edit-kill-buffer= | cancel commit |

#+BEGIN_SRC emacs-lisp
(use-package vc
:bind (("C-x v p" . my/vc-project)
("C-x v d" . vc-diff)
:map log-view-mode-map
("" . 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))
#+END_SRC

** magit

#+BEGIN_SRC emacs-lisp
(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)
;; git-commit
(setq git-commit-summary-max-length 50)
(setq git-commit-style-convention-checks
'(non-empty-second-line
overlong-summary-line))
: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 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))
#+END_SRC

** git modes

#+begin_src emacs-lisp
(add-to-list 'auto-mode-alist '("/\\.gitignore\\'" . conf-unix-mode))
(add-to-list 'auto-mode-alist '("CODEOWNERS\\'" . conf-unix-mode))
#+end_src

** ediff

#+begin_src emacs-lisp
(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))
#+end_src

** git-timemachine

#+BEGIN_SRC emacs-lisp
(use-package git-timemachine
:ensure t
:commands (git-timemachine)
:config
(add-hook
'git-timemachine-mode-hook
#'(lambda () (evil-local-mode -1))))
#+END_SRC

** diff-hl

#+BEGIN_SRC emacs-lisp
(use-package diff-hl
:ensure t
:if is-gui
:hook ((after-init . global-diff-hl-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 diff-hl-dired
:after diff-hl
:if is-gui
:hook ((dired-mode . diff-hl-dired-mode)))
#+END_SRC

* keybindings
** keybind to command mapping

#+BEGIN_SRC emacs-lisp
(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)
))
#+END_SRC

** setup keybindings

#+begin_src emacs-lisp
(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 ") 'my/leader-map)
(setq my/major-mode-map-key ""))
(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-or-modes key func)
(let ((modes (if (listp mode-or-modes) mode-or-modes (list mode-or-modes))))
(dolist (mode modes)
(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 "") 'my/major-mode-map)
(global-set-key (kbd "C-c m m") 'my/major-mode-map))
#+end_src

* evil-mode
** evil-mode setup

#+BEGIN_SRC emacs-lisp
(use-package evil-leader
:hook (evil-local-mode . evil-leader-mode)
:ensure t
:config
(evil-leader/set-leader "")
(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 "") '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))))
)
#+END_SRC

** evil packages that can be used without evil-mode

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** terminal cursor

#+begin_src emacs-lisp
;; in /lisp
(use-package term-cursor
:if is-term
:hook (after-init . global-term-cursor-mode))
#+end_src

* Spell checking

#+BEGIN_SRC emacs-lisp
(use-package flyspell
:commands (flyspell-mode flyspell-prog-mode)
:config
(add-hook 'flyspell-mode-hook
(lambda () (add-hook 'hack-local-variables-hook 'flyspell-buffer))))
#+END_SRC

* Buffer & window management
** ibuffer

#+begin_src emacs-lisp
(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))))
#+end_src

** avy

#+BEGIN_SRC emacs-lisp
(use-package avy
:ensure t
:bind (("C-c i" . avy-goto-word-1)))
#+END_SRC

** ace-window

#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC

** buffer-move

#+BEGIN_SRC emacs-lisp
(use-package buffer-move
:ensure t
:bind (("" . buf-move-up)
("" . buf-move-down)
("" . buf-move-left)
("" . buf-move-right)))
#+END_SRC

** zoom

#+begin_src emacs-lisp
(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))
#+end_src

* eww

#+begin_src emacs-lisp
(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))))
#+end_src

* dired

#+BEGIN_SRC emacs-lisp
(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 \\`\\[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
("" . 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)))
#+END_SRC

* regex replace
** re-builder (useful for debugging)

#+begin_src emacs-lisp
(use-package re-builder
:commands (re-builder)
:init
(setq reb-re-syntax 'string))
#+end_src

** visual-regexp-steroids

#+begin_src emacs-lisp
(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))
#+end_src

* direnv

#+begin_src emacs-lisp
(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))))
#+end_src

* lsp

#+begin_src emacs-lisp
(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))
#+end_src

* LANGUAGES
** nix

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** haskell

#+BEGIN_SRC emacs-lisp
(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"))))
#+END_SRC

#+BEGIN_SRC emacs-lisp :tangle no
(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)))))
#+END_SRC

** coq (proof-general)

#+BEGIN_SRC emacs-lisp
(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 "") nil))
#+END_SRC

** python

#+BEGIN_SRC emacs-lisp

(use-package python
:mode (("\\.py\\'" . python-ts-mode)
("\\.pants\\'" . pants-mode))
:init
(unless (treesit-language-available-p 'python)
(treesit-install-language-grammar "python"))
(setq my/python-mode-hooks '(python-mode-hook python-ts-mode-hook))
(defun my/is-python-mode ()
(or (eq major-mode 'python-mode) (eq major-mode 'python-ts-mode)))

(setq python-shell-prompt-detect-failure-warning nil
py-outline-minor-mode-p nil)
;; (define-derived-mode pants-mode python-mode "pants")
(define-derived-mode pants-mode python-ts-mode "pants")
(setq
lsp-pylsp-plugins-black-enabled nil
lsp-pylsp-plugins-flake8-enabled nil
lsp-pylsp-plugins-flake8-max-line-length 100
lsp-pyls-plugins-flake8-max-line-length 100
lsp-pylsp-plugins-flake8-ignore '("F821" "W503")
lsp-pylsp-plugins-isort-enabled nil
lsp-pylsp-plugins-mypy-enabled nil
lsp-pylsp-plugins-ruff-enabled nil
lsp-ruff-server-command '("ruff" "server" "--preview")
lsp-pylsp-plugins-ruff-line-length 100
)
: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-ruff))
(flycheck-add-next-checker 'python-flake8 '(warning . python-mypy))
(my/add-hooks
my/python-mode-hooks
(setq fill-column 100)
(setq evil-shift-width 4)
(my/python-enable-checkers)
;; (lsp-deferred)
)
(defun my/python-format-and-save ()
(interactive)
(blacken-buffer)
(py-isort-before-save)
(save-buffer))
(my/define-major-mode-key '(python-mode python-ts-mode) "s" 'my/python-format-and-save)
(my/define-major-mode-key '(python-mode python-ts-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
:init
(setq blacken-line-length 100))

(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 (my/is-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
:disabled
: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 pyvenv-activate)
: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 python-ts-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)
("ruff" . python-ruff)))
(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))
#+END_SRC

** rust

#+begin_src emacs-lisp
(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)))
#+end_src

** go

#+begin_src emacs-lisp
(use-package go-mode
:ensure t
:mode (("\\.go\\'" . go-mode))
:hook (go-mode . lsp-deferred)
:config
(my/add-hooks
'(go-mode-hook)
(add-hook 'before-save-hook #'gofmt nil t))
)
#+end_src

** ruby

#+begin_src emacs-lisp
;; 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)))))
#+end_src

** clojure

#+begin_src emacs-lisp
(use-package cider
:ensure t
:commands (cider-jack-in)
:diminish)

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

** javascript, typescript, html, css

#+BEGIN_SRC emacs-lisp
(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
("" . emmet-expand-line)
:map html-mode-map
("" . emmet-expand-line)
:map css-mode-map
("" . emmet-expand-line))
:hook ((web-html-mode html-mode css-mode) . emmet-mode))
#+END_SRC

** purescript

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** all lisps

#+BEGIN_SRC emacs-lisp
;; 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))
#+END_SRC

** emacs lisp

#+BEGIN_SRC 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))
#+END_SRC

** scala

#+begin_src emacs-lisp
(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))
#+end_src

** ocaml

#+begin_src emacs-lisp
(use-package tuareg
:ensure t
:mode ("\\.ml[ip]?\\'" . tuareg-mode))
#+end_src

** agda

#+begin_src emacs-lisp
;; 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")))
#+end_src

** idris

#+BEGIN_SRC emacs-lisp
(use-package idris-mode
:ensure t
:mode ("\\.idr\\'" . idris-mode))
#+END_SRC

** c, c++

#+BEGIN_SRC emacs-lisp
(setq c-default-style "linux"
c-basic-offset 4)
#+END_SRC

** dhall

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** bazel

#+begin_src emacs-lisp
(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))
#+end_src

** nginx

#+begin_src emacs-lisp
(use-package nginx-mode
:ensure t
:mode (("nginx\\.conf\\'" . nginx-mode)
("nginx\\.conf\\.template\\'" . nginx-mode)))
#+end_src

** terraform

#+begin_src emacs-lisp
(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))))
#+end_src

** docker

#+BEGIN_SRC emacs-lisp
(use-package dockerfile-mode
:ensure t
:mode ("Dockerfile.*" . dockerfile-mode))
#+END_SRC

** elasticsearch

#+begin_src emacs-lisp
(add-to-list 'auto-mode-alist '(".es\\'" . js-mode))
#+end_src

** json

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** yaml

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** conf

#+begin_src emacs-lisp
;; 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))
#+end_src

** cucumber

#+begin_src emacs-lisp
(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))))))))
#+end_src

** graphviz

#+begin_src emacs-lisp
(use-package graphviz-dot-mode
:ensure t
:mode (("\\.dot\\'" . graphviz-dot-mode))
:init
(setq graphviz-dot-indent-width 4))
#+end_src

** mermaid

#+begin_src emacs-lisp
(use-package mermaid-mode
:ensure t
:mode (("\\.mmd\\'" . mermaid-mode)
("\\.mermaid\\'" . mermaid-mode)))
#+end_src

** mustache

#+begin_src emacs-lisp
(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))
#+end_src

** editorconfig

#+begin_src emacs-lisp
(use-package editorconfig
:ensure t
:mode (("\\.editorconfig\\'" . editorconfig-conf-mode)))
#+end_src

* WRITING
** markdown

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** writeroom

#+begin_src emacs-lisp
(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))))

#+end_src

** reStructuredText

#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC

** asciidoc

#+BEGIN_SRC emacs-lisp
(use-package adoc-mode
:ensure t
:mode ("\\.adoc\\'" . adoc-mode))
#+END_SRC

** LaTeX

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

* elfeed

#+BEGIN_SRC emacs-lisp :tangle no
(use-package elfeed-wb
: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 "\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)))
#+END_SRC

* pdf tools

#+BEGIN_SRC emacs-lisp
(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
("" . outline-hide-sublevels)))
#+END_SRC

* searching
** isearch

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

Some useful isearch keys (before starting the search):

| key | description |
|-------+----------------------------|
| M-s . | isearch for thing at point |

And while inside a search:

| key | description |
|---------+-----------------------------------------------|
| C-w | add next word to search |
| C-e | add until EOL to search |
| M-e | edit search in minibuffer (commit with =RET=) |
| M-s o | run =occur= |
| M-s r | toggle regexp search |
| M-s . | mark whole thing for search |
| M-s h r | highlight current search (=hi-lock=) |
| M-% | run =query-replace= on search term |
| C-M-% | run =query-replace-regexp= on search term |
| C-l | =recenter= |

And some custom additions (mostly stolen from Protesilaos):

| where | key | description |
|-----------+-------------+-----------------------------------------------|
| in search | C-SPC | mark search and exit |
| in search | C-RET | exit search, but move point to the other side |
| in search | | 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.

#+BEGIN_SRC emacs-lisp
(use-package isearch
:diminish
:bind (:map isearch-mode-map
("C-l" . recenter)
("C-SPC" . my/isearch-mark-and-exit)
("" . my/isearch-other-end)
("" . my/isearch-abort-dwim)
("" . 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)))
)
#+END_SRC

** rg

#+BEGIN_SRC emacs-lisp
(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 "»"))))
)

#+END_SRC

** anzu

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** interactively search files/folders in project by regex with fd

#+begin_src emacs-lisp
;; 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")
)
#+end_src

* imenu-list

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

* company

#+BEGIN_SRC emacs-lisp
(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)
("" . company-complete-common-or-cycle)
("" . 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-files))

(use-package company-quickhelp
:ensure t
:after company)
#+END_SRC

* flycheck

#+BEGIN_SRC emacs-lisp
(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-mypy 'python-ts-mode)
(flycheck-add-mode 'python-flake8 'python-mode)
(flycheck-add-mode 'python-flake8 'python-ts-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 " ")))))
#+END_SRC

* projectile
** configuration

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

** project name override (for use with persp)

#+begin_src emacs-lisp
;; 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)))
#+end_src

* ivy/counsel/swiper
** counsel-projectile

#+BEGIN_SRC emacs-lisp
(use-package counsel-projectile
:ensure t
:after projectile
:bind (:map projectile-command-map
("f" . counsel-projectile-find-file)
("s" . counsel-projectile-rg)
("C-s" . my/counsel-projectile-rg-no-tests)
("S" . my/counsel-projectile-rg-no-tests)
("b" . counsel-projectile-switch-to-buffer))
:init
(setq projectile-switch-project-action 'counsel-projectile-find-file)
(setq my/counsel-projectile-rg-tests-map
'(
(python-mode . "-g !tests")
(python-ts-mode . "-g !tests")
))
(defun my/counsel-projectile-rg-no-tests ()
(interactive)
(counsel-projectile-rg (cdr (assoc major-mode my/counsel-projectile-rg-tests-map)))))
#+END_SRC

** swiper

#+BEGIN_SRC emacs-lisp
(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)
("C-s" . swiper))
:commands (swiper swiper-isearch swiper-all swiper-multi))
#+END_SRC

** flx, amx

#+BEGIN_SRC emacs-lisp
;; 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))
#+END_SRC

** ivy, counsel

#+BEGIN_SRC emacs-lisp
(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 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)

;; center thing we jumped to after confirming
(advice-add 'ivy-call :after #'recenter-top-bottom)

;; 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)
#+END_SRC

* yasnippet

#+BEGIN_SRC emacs-lisp
(use-package yasnippet-snippets
:ensure t
:after yasnippet
:config
(yas-reload-all))

(use-package yasnippet
:ensure t
:bind (("C-c y" . yas-expand)
("" . 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))
#+END_SRC

* org-mode
** org-babel

#+BEGIN_SRC emacs-lisp
;; 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))
#+END_SRC

** org-protocol

#+BEGIN_SRC emacs-lisp
;; 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)))
#+END_SRC

** org configuration

#+BEGIN_SRC emacs-lisp
(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 "") 'org-cycle)
(evil-define-key 'normal org-mode-map (kbd "TAB") 'org-cycle)
(evil-define-key 'normal org-mode-map (kbd "") '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
'(""
""
) "\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))))
#+END_SRC

** various extensions

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

* modeline
** doom-modeline

#+begin_src emacs-lisp
(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-buffer-encoding nil
doom-modeline-indent-info 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))
#+end_src

* Setup
** macos specific

#+begin_src emacs-lisp
(when is-mac
(setq mac-command-modifier 'meta)
(setq mac-option-modifier 'super)
(global-set-key [?\A-\C-i] nil))
#+end_src

** Per-workstation setup

#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC

#+BEGIN_SRC emacs-lisp
;; 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")
#+END_SRC

#+BEGIN_SRC emacs-lisp
;; 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))
#+END_SRC

#+begin_src emacs-lisp
(dolist (mapping my/extra-key-mappings)
(let ((key (car mapping))
(func (cdr mapping)))
(my/add-key key func)))
#+end_src

** Performance

#+BEGIN_SRC emacs-lisp
;; 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)
#+END_SRC

** Global setup

#+BEGIN_SRC emacs-lisp
;; 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))))
#+END_SRC

** Custom

#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC