all repos — nixfiles @ e9c0cb34d270bb0f5ef2e4b5f4826900842940b4

System and user configuration, managed by nix and home-manager

user/settings/emacs/init.el (view raw)

;;; init --- user init file  -*- lexical-binding: t; -*-
(eval '(setq inhibit-startup-echo-area-message "alan"))
(defvar default-file-name-handler-alist file-name-handler-alist)
(setq file-name-handler-alist nil
      gc-cons-threshold most-positive-fixnum
      gc-cons-percentage 0.6
      read-process-output-max (* 4 1024 1024))
(defun set-max-gc-cons ()
  (setq gc-cons-threshold most-positive-fixnum))
(defun set-default-gc-cons ()
  (setq gc-cons-threshold (* 16 1024 1024)
        gc-cons-percentage 0.1))
(add-hook 'minibuffer-setup-hook #'set-max-gc-cons)
(add-hook 'minibuffer-exit-hook #'set-default-gc-cons)
(add-hook 'after-init-hook #'set-default-gc-cons)

(defun restore-file-name-handler-alist ()
  (setq file-name-handler-alist default-file-name-handler-alist))
(add-hook 'emacs-startup-hook #'restore-file-name-handler-alist)

(defun maybe-start-server ()
  (require 'server)
  (unless (server-running-p)
    (server-start)))
(add-hook 'after-init-hook #'maybe-start-server)

(eval-when-compile (require 'fringe-helper))

(defun reload-user-init-file ()
  "Reload init file."
  (interactive)
  (load-file user-init-file))

(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(run-with-idle-timer 300 nil #'package-refresh-contents)

(setq use-package-enable-imenu-support t)
(require 'use-package)
(setq use-package-always-demand (daemonp)
      use-package-compute-statistics nil)

(defmacro quietly (&rest body)
  `(let ((inhibit-message t))
     ,@body))
(defun quiet (original-function &rest args)
  (quietly (apply original-function args)))

;;; Customize

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file :noerror :nomessage)

(use-package crux
  :defer 1)

(use-package general
  :functions (general-unbind general-define-key)
  :config (progn
            (general-override-mode +1)
            (when (eq system-type 'darwin)
              (general-unbind "s-x"))))

;;; Styles

;; I prefer an always-visible cursor.  Feels less distracting.
(blink-cursor-mode -1)

(setq-default truncate-lines t)

;; Don't ring the bell
(setq ring-bell-function #'ignore)

(use-package doom-themes
  :if (or (daemonp)
          window-system)
  :config (progn
            (doom-themes-org-config)))

(let ((light-mode-theme 'doom-one-light)
      (dark-mode-theme 'doom-one))
  (load-theme light-mode-theme :noconfirm :noenable)
  (load-theme dark-mode-theme :noconfirm :noenable)

  (defun my/switch-theme-variant (mode)
    (interactive (list
                  (intern (completing-read "Make it: " '("light" "dark") nil t))))
    (cond
     ((eq mode 'dark)
      (disable-theme light-mode-theme)
      (enable-theme dark-mode-theme))
     ((eq mode 'light)
      (disable-theme dark-mode-theme)
      (enable-theme light-mode-theme)))
    (modify-all-frames-parameters '((ns-appearance mode))))

  (defun my/system-appearance ()
    (if (and (eq system-type 'darwin)
             (string-equal
              "Dark"
              (string-trim-right (shell-command-to-string "defaults read -g AppleInterfaceStyle"))))
        'dark
      'light))

  (defun my/match-theme-system-appearance ()
    (interactive)
    (my/switch-theme-variant (my/system-appearance)))

  (my/switch-theme-variant (my/system-appearance))

  (use-package kqueue
    :if (eq window-system 'ns)
    :config (progn
              (defun my/gp-notify-callback (event)
                (let* ((filename (elt event 2))
                       (basename (file-name-nondirectory filename)))
                  (when (string-equal basename ".GlobalPreferences.plist")
                    (my/match-theme-system-appearance))))
              (setq my/kq-watcher-global-preferences
                    (kqueue-add-watch (expand-file-name "~/Library/Preferences/")
                                      '(write)
                                      #'my/gp-notify-callback)))))

(use-package exec-path-from-shell
  :if (eq system-type 'darwin)
  :config (progn
            (exec-path-from-shell-initialize)))

(global-set-key (kbd "") 'ignore)
(global-set-key (kbd "") 'ignore)
(global-set-key (kbd "") 'ignore)

(setq font-lock-maximum-decoration '((t . 1))
      jit-lock-stealth-time 1.25
      jit-lock-stealth-nice 0.5
      jit-lock-chunk-size 4096)

(context-menu-mode +1)

;;; Chrome
(column-number-mode -1)
(line-number-mode -1)

(use-package nerd-icons
  :config (progn
            (setq nerd-icons-color-icons t
                  nerd-icons-scale-factor 1.3)))

(use-package doom-modeline
  :hook (emacs-startup . doom-modeline-mode)
  :config (progn
            (setq doom-modeline-buffer-file-name-style 'relative-from-project
                  doom-modeline-buffer-encoding 'nondefault
                  doom-modeline-buffer-modification-icon nil
                  doom-modeline-check-simple-format t
                  doom-modeline-percent-position nil
                  doom-modeline-project-detection 'project
                  doom-modeline-vcs-max-length 24
                  doom-modeline-env-version nil
                  doom-modeline-height 28)
            (let ((foreground (face-attribute 'font-lock-comment-face :foreground)))
              (set-face-attribute 'doom-modeline-buffer-modified nil :foreground foreground))))

(when (eq system-type 'darwin)
  (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
  (add-to-list 'default-frame-alist '(ns-appearance . 'light))
  (setq ns-use-srgb-colorspace nil))

(add-to-list 'default-frame-alist '(width . 100))
(add-to-list 'default-frame-alist '(height . 40))
(setq-default display-line-numbers 'relative
              display-line-numbers-widen t
              display-line-numbers-width 3)

(defun turn-off-display-line-numbers-mode ()
  (interactive)
  (display-line-numbers-mode -1))

(defun turn-on-display-line-numbers-mode ()
  (interactive)
  (display-line-numbers-mode (default-value 'display-line-numbers)))

(setq frame-resize-pixelwise t
      window-resize-pixelwise t
      display-buffer-alist `(("\\*\\(?:shell\\|compilation\\)\\*" display-buffer-in-side-window
                              (side . bottom) (slot . 0) (preserve-size . (nil . t))
                              (no-other-window . t) (no-delete-other-windows . t))))

(use-package buffer-terminator
  :defer 60
  :config (progn
            (buffer-terminator-mode +1)
            (setf (alist-get 'kill-buffer-major-modes buffer-terminator-rules-alist)
                  '(magit-diff-mode
                    magit-log-mode
                    magit-process-mode
                    magit-revision-mode
                    grep-mode
                    emacs-lisp-compilation-mode
                    xref--xref-buffer-mode
                    helpful-mode)
                  (alist-get 'kill-buffer-name-regexp buffer-terminator-rules-alist)
                  (rx
                   (seq string-start
                        "*"
                        (or "EGLOT "
                            "envrc"
                            "helpful"
                            "Async-native-compliation")
                        (zero-or-more (not "*"))
                        "*"
                        string-end)))
            (setq buffer-terminator-verbose nil)))

(defun noct-relative ()
  "Show relative line numbers."
  (when display-line-numbers
    (setq-local display-line-numbers 'relative)))

(defun noct-absolute ()
  "Show absolute line numbers."
  (when display-line-numbers
    (setq-local display-line-numbers t)))

(add-hook 'evil-insert-state-entry-hook #'noct-absolute)
(add-hook 'evil-insert-state-exit-hook #'noct-relative)

(use-package ultra-scroll
  :defer 1
  :config (progn
            (setq scroll-margin 0
                  scroll-conservatively 101)
            (ultra-scroll-mode +1)))

;;; Encoding

(setq-default bidi-paragraph-direction 'left-to-right
              bidi-display-reordering 'left-to-right)
(setq bidi-inhibit-bpa t
      require-final-newline t)
(add-to-list 'auto-coding-alist '("prescient-save\\.el\\'" . utf-8-emacs))

;;; Dates & Times

(defvar calendar-week-start-day 1)
(defvar calendar-date-style 'iso)

(defun insert-date (prefix)
  "Insert the current date.
With PREFIX, use British format.
With two prefix arguments, write out the day and month name."
  (interactive "P")
  (let ((format (cond
                 ((not prefix) "%Y-%m-%d")
                 ((equal prefix '(4)) "%d/%m/%Y")
                 ((equal prefix '(16)) "%A, %d %B %Y"))))
    (insert (format-time-string format))))

(defun insert-datetime (prefix)
  "Insert current date and time.  With PREFIX, use ISO8601 format."
  (interactive "P")
  (let ((format (cond
                 ((not prefix) "%Y-%m-%d %H:%M:%S")
                 ((equal prefix '(4)) "%Y-%m-%dT%H:%M:%SZ"))))
    (insert (format-time-string format))))

;;; Keybindings

(defun prot/keyboard-quit-dwim ()
  "Do-What-I-Mean behaviour for a general `keyboard-quit'.

The generic `keyboard-quit' does not do the expected thing when
the minibuffer is open.  Whereas we want it to close the
minibuffer, even without explicitly focusing it.

The DWIM behaviour of this command is as follows:

- When the region is active, disable it.
- When a minibuffer is open, but not focused, close the minibuffer.
- When the Completions buffer is selected, close it.
- In every other case use the regular `keyboard-quit'."
  (interactive)
  (cond
   ((region-active-p)
    (keyboard-quit))
   ((derived-mode-p 'completion-list-mode)
    (delete-completion-window))
   ((> (minibuffer-depth) 0)
    (abort-recursive-edit))
   (t
    (keyboard-quit))))

(define-key global-map (kbd "C-g") #'prot/keyboard-quit-dwim)

(when (eq system-type 'darwin)
  (setq mac-option-modifier 'meta
        mac-right-option-modifier 'none
        mac-control-modifier 'control
        mac-right-control-modifier 'left
        mac-command-modifier 'super
        mac-right-command-modifier 'left
        mac-function-modifier 'hyper)
  (define-key global-map (kbd "s-w") #'kill-current-buffer)
  (define-key global-map (kbd "s-k") #'kill-current-buffer :remove))

(use-package avy
  :defer 2
  :config (setq avy-all-windows nil))
(use-package ace-link
  :after avy
  :commands (ace-link-setup-default)
  :config (ace-link-setup-default))

;; Popup keybindings following a prefix automatically.

(use-package which-key
  :defer 5
  :config (progn
            (which-key-mode +1)
            (which-key-setup-side-window-right-bottom)))

;;; Minibuffer

(setq enable-recursive-minibuffers t
      save-silently t
      uniquify-buffer-name-style 'forward
      read-extended-command-predicate #'command-completion-default-include-p)
(minibuffer-depth-indicate-mode t)

(use-package hydra
  :defer 2)

(use-package recentf
  :defer 1
  :custom (recentf-auto-cleanup 1800)
  :config (progn
            (quietly (recentf-mode +1))
            (advice-add 'recentf-cleanup :around #'quiet)))

(use-package savehist
  :init (savehist-mode +1)
  :config (progn
            (add-to-list 'savehist-additional-variables 'command-history)))

(use-package persist-state
  :defer 1
  :ghook ('server-mode-hook #'persist-state-mode))

(use-package vertico
  :config (progn
            (vertico-mode +1)))

(use-package prescient
  :defer 1
  :config (progn
            (add-to-list 'completion-styles 'prescient)
            (setq prescient-history-length 10000)
            (prescient-persist-mode +1)))

(use-package vertico-prescient
  :after prescient
  :config (progn
            (vertico-prescient-mode +1)))

(use-package marginalia
  :after vertico
  :general (:keymaps 'minibuffer-local-map
                     "M-A" #'marginalia-cycle)
  :init (marginalia-mode +1))

(setq completion-ignore-case t
      read-buffer-completion-ignore-case t
      read-file-name-completion-ignore-case t
      completion-styles '(flex substring basic)
      completion-category-overrides '((file (styles basic partial-completion))))

(use-package consult
  :general ([remap isearch-forward] #'consult-line
            [remap isearch-backward] #'consult-line
            [remap project-switch-to-buffer] #'consult-project-buffer
            [remap project-search] #'consult-ripgrep)
  :config (progn
            (setq consult-ripgrep-args
                  "rg --null --line-buffered --color=never --max-columns=1000 --path-separator /   --smart-case --no-heading --with-filename --line-number --search-zip --follow")))

(use-package embark
  :general ("C-." #'embark-act
            "M-." #'embark-dwim
            "C-h B" #'embark-bindings)
  :config (progn
            (setq embark-prompter #'embark-keymap-prompter)
            (add-to-list 'display-buffer-alist
                         '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
                           nil
                           (window-parameters (mode-line-format . none))))))

(use-package embark-consult
  :ghook ('consult-preview-at-point-mode-hook #'embark-collect-mode))

(use-package smerge-mode
  :defer 2
  :config
  (defhydra unpackaged/smerge-hydra
    (:color pink :hint nil :post (smerge-auto-leave))
    "
^Move^       ^Keep^               ^Diff^                 ^Other^
^^-----------^^-------------------^^---------------------^^-------------
_C-j_: next  _b_ase               _<_: upper/base        _C_ombine
_C-k_: prev  _u_pper              _=_: upper/lower       _s_mart resolve
^^           _l_ower              _>_: base/lower        _k_ill current
^^           _a_ll                _R_efine (words)
^^           _RET_: current       _E_diff
"
    ("C-j" smerge-next)
    ("C-k" smerge-prev)
    ("b" smerge-keep-base)
    ("u" smerge-keep-upper)
    ("l" smerge-keep-lower)
    ("a" smerge-keep-all)
    ("RET" smerge-keep-current)
    ("\C-m" smerge-keep-current)
    ("<" smerge-diff-base-upper)
    ("=" smerge-diff-upper-lower)
    (">" smerge-diff-base-lower)
    ("R" smerge-refine)
    ("E" smerge-ediff)
    ("C" smerge-combine-with-next)
    ("s" smerge-resolve)
    ("k" smerge-kill-current)
    ("w" (lambda ()
           (interactive)
           (save-buffer)
           (bury-buffer))
     "Save and bury buffer" :color blue)
    ("q" nil "cancel" :color blue))
  :hook (magit-diff-visit-file . (lambda ()
                                   (when (bound-and-true-p smerge-mode)
                                     (unpackaged/smerge-hydra/body)))))

;;; Windows

(defun split-window-properly (&optional window)
  (let ((window (or window (selected-window))))
    (or (and (window-splittable-p window)
             ;; Split window vertically.
             (with-selected-window window
               (split-window-below)))
        (and (window-splittable-p window t)
             ;; Split window horizontally.
             (with-selected-window window
               (split-window-right))))))

(setq split-window-preferred-function #'split-window-properly
      split-height-threshold 20
      split-width-threshold 160)

(use-package winner
  :defer 8
  :config (progn
            (setq winner-boring-buffers '("*Completions*" "*Help*" "*Apropos*" "*Buffer List*" "*info*" "*Compile-Log*"))
            (winner-mode +1))
  :general (:keymaps 'evil-window-map
                     "u" #'winner-undo
                     "r" #'winner-redo
                     "C-r" #'winner-redo))
;;; Evil

(eval-and-compile
  (defvar evil-want-keybinding nil))
(use-package evil
  :demand t
  :commands (evil-mode evil-delete-buffer evil-ex-define-cmd)
  :config (progn
            (setq-default evil-shift-width 2)
            (setq evil-mode-line-format nil)
            (evil-set-undo-system 'undo-redo)
            (add-to-list 'evil-emacs-state-modes 'eshell-mode)
            (evil-mode +1))
  :general
  (:states 'motion
           "C-;" #'evil-avy-goto-line
           "C-" #'evil-switch-to-windows-last-buffer)
  (:states 'normal
           ";" #'evil-ex)
  (:states '(normal)
           "g ." #'my/ls-code-actions
           "g s" #'consult-imenu
           "g S" #'my/ls-consult-symbol))

(add-hook 'c-mode-common-hook ; make b/w/e include underscore as *part* of a word
          (lambda () (modify-syntax-entry ?_ "w")))

(use-package evil-anzu
  :defer 2
  :after evil
  :config (global-anzu-mode +1))

(use-package evil-collection
  :demand t
  :general ( :keymaps 'evil-collection-unimpaired-mode-map
             :states 'normal
             "[ d" #'evil-collection-unimpaired-previous-error
             "] d" #'evil-collection-unimpaired-next-error)
  :config (progn
            (setq evil-collection-magit-use-y-for-yank nil
                  evil-collection-corfu-key-themes '(default magic-return))
            (general-unbind 'normal magit-mode-map
              "")
            (evil-collection-init)))

(general-create-definer my-leader-def
  :keymaps 'override
  :states '(normal motion)
  :prefix ",")

(use-package evil-space
  :defer 1
  :after evil
  :config (evil-space-mode +1))

;; this macro was copied from here: https://stackoverflow.com/a/22418983/4921402
(defmacro define-and-bind-quoted-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))))

(use-package evil-surround
  :after evil
  :defer 2
  :config (progn
            (add-hook 'js-base-mode-hook (lambda ()
                                           (define-and-bind-quoted-text-object "slash" "/" "\\/" "\\/")
                                           (push '(?\/ . ("/" . "/")) evil-surround-pairs-alist)))
            (add-hook 'emacs-lisp-mode-hook (lambda ()
                                              (push '(?` . ("`" . "'")) evil-surround-pairs-alist)))
            (global-evil-surround-mode +1)))


(use-package evil-embrace
  :after evil-surround
  :ghook ('LaTex-mode-hook #'embrace-LaTeX-mode-hook)
  :ghook ('org-mode-hook #'embrace-org-mode-hook)
  :ghook ('ruby-base-mode-hook #'embrace-ruby-mode-hook)
  :ghook ('emacs-lisp-mode-hook #'embrace-emacs-lisp-mode-hook)
  :config (progn
            (setq evil-embrace-show-help-p nil)
            (push ?\/ evil-embrace-evil-surround-keys)
            (evil-embrace-enable-evil-surround-integration)))

(use-package evil-exchange
  :after evil
  :config (progn
            (evil-exchange-cx-install)))

(use-package evil-commentary
  :after evil
  :defer 2
  :config (evil-commentary-mode +1))

(use-package evil-lion
  :after evil
  :defer 10
  :config (progn
            (evil-lion-mode +1)))

(use-package evil-matchit
  :after evil
  :defer 2
  :config (progn
            (global-evil-matchit-mode +1)))

(use-package evil-quickscope
  :after evil
  :commands (evil-quickscope-mode)
  :ghook ('(magit-mode-hook git-rebase-mode-hook) #'turn-off-evil-quickscope-mode)
  :config (global-evil-quickscope-mode +1))

(use-package evil-numbers
  :after evil
  :general (:states '(normal visual)
                    "C-a" #'evil-numbers/inc-at-pt
                    "C-q" #'evil-numbers/dec-at-pt))

(use-package evil-org
  :after org
  :commands (evil-org-set-key-theme)
  :init (progn
          (add-hook 'org-agenda-mode-hook #'evil-org-agenda-set-keys))
  :ghook ('org-mode-hook #'evil-org-mode)
  :gfhook #'evil-org-set-key-theme)

(use-package evil-textobj-tree-sitter
  :defer 5
  :config (progn
            (defun etts/start-of-next-function ()
              (interactive)
              (evil-textobj-tree-sitter-goto-textobj "function.outer" nil nil))
            (defun etts/start-of-prev-function ()
              (interactive)
              (evil-textobj-tree-sitter-goto-textobj "function.outer" t nil))
            (defun etts/end-of-next-function ()
              (interactive)
              (evil-textobj-tree-sitter-goto-textobj "function.outer" nil t))
            (defun etts/end-of-prev-function ()
              (interactive)
              (evil-textobj-tree-sitter-goto-textobj "function.outer" t t))
            (general-define-key :keymaps 'evil-outer-text-objects-map
                                "f" (evil-textobj-tree-sitter-get-textobj "function.outer")
                                "a" (evil-textobj-tree-sitter-get-textobj ("conditional.outer" "loop.outer")))
            (general-define-key :keymaps 'evil-inner-text-objects-map
                                "f" (evil-textobj-tree-sitter-get-textobj "function.inner")
                                "a" (evil-textobj-tree-sitter-get-textobj ("conditional.inner" "loop.inner"))))
  :general
  (:states 'normal
           "]f" #'etts/start-of-next-function
           "[f" #'etts/start-of-prev-function
           "]F" #'etts/end-of-next-function
           "[F" #'etts/end-of-prev-function))

;;; Completion

(use-package corfu
  :defer 1
  :config (progn
            (global-corfu-mode +1)
            (setq corfu-auto t
                  corfu-auto-delay 0.1
                  corfu-auto-prefix 3
                  corfu-on-exact-match nil
		          corfu-preview-current nil
                  corfu-preselect 'prompt
                  corfu-on-exact-match 'quit)
            (defun corfu-enable-in-minibuffer ()
              "Enable Corfu in the minibuffer if `completion-at-point' is bound."
              (when (where-is-internal #'completion-at-point (list (current-local-map)))
                (setq-local corfu-auto nil) ;; Enable/disable auto completion
                (setq-local corfu-echo-delay nil ;; Disable automatic echo and popup
                            corfu-popupinfo-delay nil)
                (corfu-mode 1)))
            (add-hook 'minibuffer-setup-hook #'corfu-enable-in-minibuffer)
            (add-hook 'eshell-mode-hook (lambda ()
                                          (setq-local corfu-auto nil)
                                          (corfu-mode +1)))))

(use-package cape
  :after (corfu)
  :general (:states 'insert
                    "C-x C-f" #'cape-file))

(use-package kind-icon
  :after (corfu)
  :config (progn
            (setq kind-icon-default-face 'corfu-default)
            (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter)))

(use-package tempel
  :general ("M-+" #'tempel-complete ;; Alternative tempel-expand
            "M-*" #'tempel-insert
            :keymaps 'tempel-mode-map
            "" #'tempel-next)
  :config (progn
            (global-tempel-abbrev-mode +1)))

(use-package tempel-collection
  :after tempel)

;;; Documentation

(use-package eldoc
  :defer 5
  :config (progn
            (setq eldoc-idle-delay 0.5
                  eldoc-echo-area-use-multiline-p nil)
            (global-eldoc-mode +1)))

(use-package eldoc-box
  :defer t
  :general (:states '(normal)
                    "g h" #'eldoc-box-help-at-point))

(use-package ehelp
  :defer 15
  :general ([remap help-map] 'electric-help-map))

(use-package helpful
  :general (ehelp-map
            "k" #'helpful-key
            "v" #'helpful-variable
            "f" #'helpful-callable))

;;; Files

;;;; Auto-saving

(setq auto-save-default nil
      make-backup-files nil
      create-lockfiles nil)

;;;; Auto-reloading

(use-package autorevert
  :config (progn
            (setq auto-revert-verbose nil
                  auto-revert-use-notify t)
            (global-auto-revert-mode 1)))

(setq delete-by-moving-to-trash t)

(defun my/delete-file-and-buffer ()
  "Kill the current buffer and deletes the file it is visiting."
  (interactive)
  (let ((filename (buffer-file-name)))
    (when filename
      (when (y-or-n-p (format "Are you sure you want to delete %s? " filename))
        (delete-file filename delete-by-moving-to-trash)
        (message "Deleted file %s" filename)
        (kill-buffer)))))

(use-package goto-chg
  :defer 1)

;;;; TRAMP

(use-package tramp
  :defer 10
  :config (progn
            (add-to-list 'tramp-default-proxies-alist
                         '(nil "\\`root\\'" "/ssh:%h:"))
            (add-to-list 'tramp-default-proxies-alist
                         `(,(regexp-quote (system-name)) nil nil))

            ;; https://coredumped.dev/2025/06/18/making-tramp-go-brrrr./
            (setq remote-file-name-inhibit-locks t
                  remote-file-name-inhibit-auto-save-visited t
                  tramp-use-scp-direct-remote-copying t
                  tramp-copy-size-limit 1048576
                  tramp-default-method "rsync")

            (connection-local-set-profile-variables 'remote-direct-async-process
                                                    '((tramp-direct-async-process . t)))

            (connection-local-set-profiles '(:application tramp :machine "linde")
                                           'remote-direct-async-process)

            (setq magit-tramp-pipe-stty-settings 'pty)

            (with-eval-after-load 'compile
              (remove-hook 'compilation-mode-hook #'tramp-compile-disable-ssh-controlmaster-options))))

(use-package ssh-deploy
  :defer 20
  :config (progn
            (ssh-deploy-line-mode +1)
            (ssh-deploy-add-find-file-hook)
            (ssh-deploy-add-after-save-hook)))

;;; Directories

(setq dired-dwim-target t
      dired-recursive-copies 'top
      dired-listing-switches "-alh"
      dired-kill-when-opening-new-dired-buffer t
      dired-recursive-deletes (if delete-by-moving-to-trash
                                  'always
                                'top))

(add-hook 'dired-mode-hook
          (lambda ()
            (dired-hide-details-mode)))

;;; Shells

(use-package eshell
  :defer 5
  :commands (eshell)
  :functions (eshell/pwd)
  :gfhook #'turn-off-display-line-numbers-mode
  :general (:keymaps 'eshell-command-map
                     "C-r" #'eshell-history-backwards
                     "C-s" #'eshell-history-forwards)
  :init (progn
          (with-eval-after-load 'evil-ex
            (evil-ex-define-cmd "esh[ell]" #'eshell)))
  :config (progn
            (eshell-load-modules eshell-modules-list)
            (setq eshell-prompt-function (lambda ()
                                           (concat (eshell/pwd) "\n$ "))
                  eshell-prompt-regexp "^[$][[:blank:]]"
                  eshell-cmpl-cycle-completions nil)
            (add-hook 'eshell-exit-hook (lambda ()
                                          (when (window-deletable-p)
                                            (delete-window))))))

(use-package eshell-toggle
  :commands (eshell-toggle)
  :general ("C-`" #'eshell-toggle)
  :config (progn
            (setq eshell-toggle-find-project-root-package 'project)))

(declare-function eshell-push-command "esh-buf-stack" (CMD))
(defun my-bind-esh-push ()
  (general-define-key
   :states '(normal insert)
   :keymaps 'local
   "M-q" #'eshell-push-command))

(use-package esh-buf-stack
  :after (eshell)
  :ghook ('eshell-mode-hook #'my-bind-esh-push)
  :config (setup-eshell-buf-stack))

(use-package esh-help
  :after (eshell)
  :config (setup-esh-help-eldoc))

(use-package eshell-fringe-status
  :after eshell
  :ghook '(eshell-mode-hook))

(use-package eshell-up
  :after (eshell))

(use-package shell
  :defer t
  :general (:keymaps 'shell-mode-map
                     "C-d" #'comint-delchar-or-maybe-eof))

(use-package comint
  :defer t
  :general (:keymaps 'comint-mode-map
                     "C-c C-l" #'counsel-shell-history))

;;; Editing

(setq-default tab-always-indent 'complete
              indent-tabs-mode nil
              tab-width 4)

(if (fboundp 'kill-ring-deindent-mode)
    (kill-ring-deindent-mode +1))

(electric-pair-mode +1)

(use-package ws-butler
  :ghook ('prog-mode-hook))

(use-package dtrt-indent
  :commands (dtrt-indent-mode))

(use-package rainbow-mode
  :ghook ('(css-mode-hook
            conf-xdefaults-mode-hook)
          #'rainbow-mode))

(use-package expand-region
  :general (:states 'visual
                    "SPC" #'er/expand-region))

;;; Major modes

;;;; tree-sitter
(use-package treesit-auto
  :config (progn
            (global-treesit-auto-mode)
            (treesit-auto-add-to-auto-mode-alist)))

;;;; golang
(with-eval-after-load 'project
  (add-to-list 'project-vc-extra-root-markers "go.mod"))

(setq-default go-ts-mode-indent-offset 2)
(use-package templ-ts-mode
  :gfhook #'eglot-format-before-save-mode
  :defer t
  :config (progn
            (setq-default go-ts-mode-indent-offset 2)))

;;;; nim
(use-package nim-mode
  :defer t
  :config (progn
            (add-to-list 'eglot-server-programs
                         '(nim-mode "nimlsp"))))

;;;; js
(setq js-enabled-frameworks '(javascript))
(add-to-list 'auto-mode-alist '("\\.[cm]js\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.lock" . json-ts-mode))

;;;; typescript

(use-package astro-ts-mode
  :mode (("\\.astro\\'" . astro-ts-mode)))

(autoload 'ansi-color-apply-on-region "ansi-color")
(defun colourise-compilation-buffer ()
  (ansi-color-apply-on-region compilation-filter-start (point-max)))
(add-hook 'compilation-filter-hook #'colourise-compilation-buffer)

;;;; shell
(general-add-hook '(sh-mode-hook bash-ts-mode-hook fish-mode-hook)
                  (lambda ()
                    (general-add-hook 'after-save-hook
                                      #'executable-make-buffer-file-executable-if-script-p :append :local)))

(add-to-list 'auto-mode-alist '("\\.env\\'" . conf-unix-mode))
(add-to-list 'auto-mode-alist '("\\.zsh\\'" . shell-script-mode))
(add-to-list 'auto-mode-alist '("zshenv\\'" . shell-script-mode))

(add-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on)

(when (eq system-type 'gnu/linux)
  (setenv "SSH_AUTH_SOCK" (expand-file-name "ssh-agent" (getenv "XDG_RUNTIME_DIR"))))

(use-package fish-mode
  :mode (("\\.fish\\'" . fish-mode))
  :config (progn
            (setq fish-enable-auto-indent t)))

;;;; nix
(use-package nix-mode
  :defer t
  :config (progn
            (setq nix-mode-use-smie nil
                  nix-indent-function #'smie-indent-line)))

(use-package nix-ts-mode
  :defer 2
  :mode (("\\.nix\\'" . nix-ts-mode)))

(use-package nix-update
  :commands (nix-update-fetch))

;;;; gitlab-ci.yml
(with-eval-after-load 'git-gutter-fringe
  (fringe-helper-define 'flycheck-fringe-bitmap-double-arrow '(center repeated)
    "XXX....."))

(use-package gitlab-ci-mode-flycheck
  :ghook ('gitlab-ci-mode-hook (list #'gitlab-ci-mode-flycheck-enable
                                     #'flycheck-mode)))

;;;; *ignore
(use-package gitignore-mode
  :mode ((".dockerignore\\'" . gitignore-mode)))

;;;; gitolite
(use-package gl-conf-mode
  :defer t)

;;;; lisps

(use-package racket-mode
  :ghook ('racket-mode-hook #'racket-xp-mode))

(use-package clojure-mode
  :defer t)
(use-package cider
  :defer t
  :after clojure-mode)
(use-package rainbow-delimiters
  :ghook '(clojure-mode-hook
           emacs-lisp-mode-hook))

(use-package lispyville
  :ghook '(emacs-lisp-mode-hook
           clojure-mode-hook
           racket-mode-hook))

;;;; org

(use-package org
  :defer t
  :config (progn
            (setq org-ellipsis "…"
                  org-modules nil
                  org-directory "~/Documents/org")))

;;;; web modes (html)

(use-package css-mode
  :defer t)

(use-package web-mode
  :mode (("\\.html?.erb\\'" . web-mode)
         ("\\.gotmpl\\'" . web-mode)
         ("\\.tmpl\\'" . web-mode))
  :config (setq web-mode-enable-auto-pairing nil
                web-mode-style-padding 2
                web-mode-script-padding 2
                web-mode-engines-alist '(("go" . "\\.tmpl\\'")
                                         ("go" . "\\.gotmpl\\'"))))

(use-package emmet-mode
  :ghook '(web-mode-hook sgml-mode-hook templ-ts-mode-hook))

;;; IDE features

(fringe-helper-define 'left-vertical-bar '(center repeated)
  "XXX.....")
(use-package flymake
  :defer 5
  :config (setq flymake-error-bitmap '(left-vertical-bar compilation-error)
                flymake-warning-bitmap '(left-vertical-bar compilation-warning)))
(use-package flymake-popon
  :after flymake
  :ghook ('flymake-mode-hook #'flymake-popon-mode))

(use-package flycheck
  :defer t
  :config (progn
            (setq flycheck-check-syntax-automatically '(save idle-buffer-switch mode-enabled)
                  flycheck-highlighting-mode 'sexps)
            (flycheck-define-error-level 'error
              :severity 100
              :compilation-level 2
              :overlay-category 'flycheck-error-overlay
              :fringe-bitmap 'left-vertical-bar
              :fringe-face 'flycheck-fringe-error
              :error-list-face 'flycheck-error-list-error)
            (flycheck-define-error-level 'warning
              :severity 10
              :compilation-level 1
              :overlay-category 'flycheck-warning-overlay
              :fringe-bitmap 'left-vertical-bar
              :fringe-face 'flycheck-fringe-warning
              :warning-list-face 'flycheck-error-list-warning)
            (flycheck-define-error-level 'info
              :severity -10
              :compilation-level 0
              :overlay-category 'flycheck-info-overlay
              :fringe-bitmap 'left-vertical-bar
              :fringe-face 'flycheck-fringe-info
              :info-list-face 'flycheck-error-list-info)))

;;; Projects

(use-package project
  :bind (("s-p" . #'project-find-file)
         ("s-o" . #'project-switch-project))
  :general (:keymaps 'project-prefix-map
                     "s" #'project-search)
  :config (progn
            (setq project-switch-commands #'magit-project-status)
            (with-eval-after-load 'evil-ex
              (evil-ex-define-cmd "pesh[ell]" #'project-eshell)
              (evil-ex-define-cmd "pb" #'project-switch-to-buffer)
              (evil-ex-define-cmd "psw[itch]" #'project-switch-project))))

(defun my/project-or-not-find-file (&optional include-all)
  (interactive "P")
  (if (project-current nil)
      (call-interactively #'project-find-file)
    (call-interactively #'find-file)))

(defun ap/consult-ghq-switch-project (dir)
  "Append a slash to avoid project.el remembering two different
paths for the same project."
  (interactive)
  (project-switch-project (if (string-suffix-p "/" dir)
                              dir
                            (concat dir "/"))))
(use-package consult-ghq
  :defer 5
  :general (:keymaps 'project-prefix-map
                     "o" #'consult-ghq-switch-project)
  :config (progn
            (setq consult-ghq-grep-function #'consult-grep
                  consult-ghq-find-function #'consult-find
                  consult-ghq-switch-project-function #'ap/consult-ghq-switch-project)))

(use-package envrc
  :defer 2
  :config (progn
            (setq envrc-show-summary-in-minibuffer nil)
            (envrc-global-mode)))

(use-package magit
  :defer 5
  :commands (magit-status magit-dispatch)
  :general ([remap project-vc-dir] #'magit-project-status)
  (:keymaps 'project-prefix-map "m" #'magit-project-status)
  (:keymaps 'magit-mode-map "z" nil)
  (:keymaps 'magit-status-mode-map "z" #'magit-stash)
  (:keymaps 'magit-section-mode-map "C-" nil)
  :config (progn
            (add-to-list 'magit-git-environment "NO_COLOR=1")
            (with-eval-after-load 'evil-ex
              (evil-ex-define-cmd "mg" #'magit-status))
            (setq magit-section-visibility-indicator nil
                  magit-diff-refine-hunk t
                  magit-auto-revert-mode nil
                  magit-auto-revert-immediately nil ; unnecessary when global-auto-revert-mode is enabled
                  magit-display-buffer-function #'magit-display-buffer-fullcolumn-most-v1)
            (remove-hook 'magit-status-sections-hook 'magit-insert-tags-header)
            (remove-hook 'magit-section-highlight-hook 'magit-section-highlight)
            (remove-hook 'magit-section-highlight-hook 'magit-section-highlight-selection)
            (remove-hook 'magit-section-highlight-hook 'magit-diff-highlight)
            (require 'magit-extras)))

(use-package magit-todos
  :defer 10
  :config (progn
            (magit-todos-mode +1)
            (add-to-list 'magit-todos-exclude-globs "*.map")))

(use-package difftastic
  :defer 5)
(use-package difftastic-bindings
  :after magit
  :config (progn
            (difftastic-bindings-mode +1)))

(use-package git-gutter-fringe
  :defer 5
  :config (progn
            (add-hook 'magit-post-refresh-hook
                      #'git-gutter:update-all-windows)
            ;; places the git gutter outside the margins.
            (setq-default fringes-outside-margins nil)
            ;; thin fringe bitmaps
            (fringe-helper-define 'git-gutter-fr:added '(center repeated)
              ".XXX....")
            (fringe-helper-define 'git-gutter-fr:modified '(center repeated)
              ".XXX....")
            (fringe-helper-define 'git-gutter-fr:deleted '(center repeated)
              ".XXX....")
            (setq git-gutter-fr:side 'left-fringe)
            (global-git-gutter-mode 1)))

(use-package vc-msg
  :defer 30)

(use-package git-timemachine
  :commands (git-timemachine))

(use-package editorconfig
  :defer 2
  :config (progn
            (editorconfig-mode +1)
            (setq editorconfig-lisp-use-default-indent t)
            (setf (alist-get 'templ-ts-mode editorconfig-indentation-alist)
                  'go-ts-mode-indent-offset)))

(setq-default ispell-dictionary "en_GB-ise-w_accents")
(setq ispell-extra-args '("--sug-mode=ultra" "--camel-case"))

(use-package jinx-mode
  :defer 10
  :ghook 'text-mode-hook
  :general ([remap ispell-word] #'jinx-correct-word
            [remap evil-prev-flyspell-error] #'jinx-previous
            [remap evil-next-flyspell-error] #'jinx-next)
  :config (progn
            (advice-add 'jinx--load-dicts :after (lambda ()
                                                   (unless jinx--dicts
                                                     (global-jinx-mode -1))))))

(use-package feature-mode
  :defer t
  :config (progn
            (setq feature-cucumber-command "cucumber-js {options} \"{feature}\"")))

(defvaralias 'typescript-ts-mode-hook 'typescript-mode-hook)

(defvaralias 'dockerfile-ts-mode-hook 'dockerfile-mode-hook)

(defvaralias 'yaml-ts-mode-hook 'yaml-mode-hook)

(defvaralias 'go-ts-mode-hook 'go-mode-hook)

(defvaralias 'nix-ts-mode-hook 'nix-mode-hook)

(use-package consult-lsp
  :commands (consult-lsp-symbols
             consult-lsp-diagnostics))

(define-minor-mode eglot-format-before-save-mode
  "Whether to ask the LSP to format the buffer before saving"
  :init-val nil
  (if eglot-format-before-save-mode
      (progn
        (add-hook 'before-save-hook #'eglot-format-buffer nil 'local)
        (when (bound-and-true-p apheleia-mode)
          (apheleia-mode -1)))
    (remove-hook 'before-save-hook #'eglot-format-buffer 'local)))

(use-package eglot
  :defer 3
  :general (:states 'normal :keymaps 'eglot-mode-map
                    "gr" #'xref-find-references
                    "C-t" #'xref-go-back)
  :ghook ('(typescript-mode-hook
            dockerfile-mode-hook
            yaml-mode-hook
            js-base-mode-hook
            fish-mode-hook
            css-base-mode-hook
            lua-mode-hook
            markdown-mode-hook
            nim-mode-hook
            html-mode-hook
            nix-mode-hook
            templ-ts-mode-hook
            toml-ts-mode-hook
            just-ts-mode-hook
            haskell-mode-hook)
          #'eglot-ensure)
  :config (progn
            (when (assoc 'nix-mode eglot-server-programs)
              (setf (car (assoc 'nix-mode eglot-server-programs)) '(nix-mode nix-ts-mode)))
            (nconc eglot-server-programs '((toml-ts-mode "taplo" "lsp" "stdio")
                                           (just-ts-mode "just-lsp")
                                           (fish-mode "fish-lsp" "start")))
            (advice-add 'eglot--message :before-until (lambda (formatstring &rest rest)
                                                        (s-starts-with-p "Connected!" formatstring)))
            (defun my/setup-eglot-eldoc ()
              (push 'flymake-eldoc-function eldoc-documentation-functions))
            (add-hook 'eglot-managed-mode-hook 'my/setup-eglot-eldoc)
            (setq-default eglot-workspace-configuration
                          '( :yaml (:keyOrdering :json-false)
                             :nil (:nix (:flake (:autoArchive t)))
                             :gopls ( :staticcheck t
                                      :usePlaceholders t)))
            (defun my/eglot-capf ()
              (setq-local completion-at-point-functions
                          (list (cape-capf-super
                                 #'eglot-completion-at-point
                                 #'tempel-expand
                                 #'cape-file))))
            (add-hook 'eglot-managed-mode-hook #'my/eglot-capf)))

(use-package eglot-booster
  :after eglot
  :config (progn
            (eglot-booster-mode +1)))

(use-package eglot-tempel
  :after eglot
  :config (progn
            (eglot-tempel-mode +1)))

(use-package consult-eglot
  :commands (consult-eglot-symbols)
  :after eglot)

(use-package lsp-mode
  :defer 3
  :ghook ('(go-mode-hook)
          #'lsp-deferred)
  ('lsp-mode-hook #'lsp-enable-which-key-integration)
  ('lsp-completion-mode-hook #'my/lsp-mode-setup-completion)
  :general (:states 'normal :keymaps 'lsp-mode-map
                    "gr" #'xref-find-references
                    "C-t" #'xref-go-back)
  :config (progn
            (setq lsp-auto-guess-root t
                  lsp-auto-execute-action nil
                  lsp-headerline-breadcrumb-enable nil
                  lsp-enable-suggest-server-download nil
                  lsp-modeline-diagnostics-enable nil
                  lsp-completion-provider :none ; value `:capf' is actually for company :(
                  lsp-diagnostics-provider t     ; this means prefer flymake over flycheck, why‽
                  )

            (defun lsp-booster--advice-json-parse (old-fn &rest args)
              "Try to parse bytecode instead of json."
              (or
               (when (equal (following-char) ?#)
                 (let ((bytecode (read (current-buffer))))
                   (when (byte-code-function-p bytecode)
                     (funcall bytecode))))
               (apply old-fn args)))
            (advice-add (if (progn (require 'json)
                                   (fboundp 'json-parse-buffer))
                            'json-parse-buffer
                          'json-read)
                        :around
                        #'lsp-booster--advice-json-parse)

            (defun lsp-booster--advice-final-command (old-fn cmd &optional test?)
              "Prepend emacs-lsp-booster command to lsp CMD."
              (let ((orig-result (funcall old-fn cmd test?)))
                (if (and (not test?)                             ;; for check lsp-server-present?
                         (not (file-remote-p default-directory)) ;; see lsp-resolve-final-command, it would add extra shell wrapper
                         lsp-use-plists
                         (not (functionp 'json-rpc-connection))  ;; native json-rpc
                         (executable-find "emacs-lsp-booster"))
                    (progn
                      (when-let ((command-from-exec-path (executable-find (car orig-result))))  ;; resolve command from exec-path (in case not found in $PATH)
                        (setcar orig-result command-from-exec-path))
                      (message "Using emacs-lsp-booster for %s!" orig-result)
                      (cons "emacs-lsp-booster" orig-result))
                  orig-result)))
            (advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command)

            (defun my/lsp-mode-setup-completion ()
              (setf (alist-get 'styles (alist-get 'lsp-capf completion-category-defaults))
                    '(flex)))

            (lsp-register-custom-settings
             '(("golangci-lint.command"
                ["golangci-lint" "run" "--output.json.path" "/dev/stdout"])))

            (lsp-register-client
             (make-lsp-client :new-connection (lsp-stdio-connection
                                               '("golangci-lint-langserver"))
                              :activation-fn (lsp-activate-on "go")
                              :language-id "go"
                              :priority 0
                              :server-id 'golangci-lint
                              :add-on? t
                              :library-folders-fn #'lsp-go--library-default-directories
                              :initialization-options (lambda ()
                                                        (gethash "golangci-lint"
                                                                 (lsp-configuration-section "golangci-lint")))))))

(use-package yasnippet
  :after lsp-mode
  :ghook ('lsp-completion-mode-hook #'yas-minor-mode))

(defun my/ls-rename ()
  (interactive)
  (if lsp-mode
      (call-interactively #'lsp-rename)
    (call-interactively #'eglot-rename)))

(defun my/ls-consult-symbol ()
  (interactive)
  (if lsp-mode
      (call-interactively #'consult-lsp-symbols)
    (call-interactively #'consult-eglot-symbols)))

(defun my/ls-code-actions ()
  (interactive)
  (call-interactively
   (if lsp-mode
       #'lsp-execute-code-action
     #'eglot-code-actions)))

;;;; Reformat on save

(use-package apheleia
  :defer 11
  :ghook 'prog-mode-hook
  :config (progn
            (setf (alist-get 'shfmt apheleia-formatters)
                  '("shfmt"))
            (setq apheleia-formatters
                  (append apheleia-formatters '((golines "golines")
                                                (taplo "taplo" "format" "-")
                                                (sqlfluff "sqlfluff" "format" "--stdin-filename" filepath "-")
                                                (prettier-gotmpl
                                                 "prettier" "--stdin-filepath" filepath
                                                 "--parser=go-template" (apheleia-formatters-indent "--use-tabs" "--tab-width")))))
            (setf (alist-get 'sql-mode apheleia-mode-alist) 'sqlfluff)
            (setq apheleia-mode-alist (append apheleia-mode-alist '((toml-ts-mode . taplo)))))
  :init (progn
          (apheleia-global-mode +1)))

;;; Take me to my leader

(my-leader-def
  "" nil
  "`" #'eshell-toggle
  "h" '(:keymap ehelp-map :package ehelp)
  "w" '(:keymap evil-window-map :package evil)
  "x" '(:keymap ctl-x-map)
  "c" (general-simulate-key "C-c")
  "j" #'my/ls-consult-symbol
  "m" #'magit-status
  "r" #'my/ls-rename
  "q" #'evil-delete-buffer
  "p" '(:keymap project-prefix-map :package project)
  "v" #'split-window-right
  "o" #'other-window
  "u" #'universal-argument
  ";" #'execute-extended-command
  "a" #'my/ls-code-actions
  "bb" #'consult-buffer
  "bx" #'kill-current-buffer
  "br" #'revert-buffer
  "bk" #'kill-buffer
  "dd" #'dired
  "D" #'consult-dir
  "e" '(:keymap envrc-command-map :package envrc)
  "fs" #'save-buffer
  "ff" #'my/project-or-not-find-file
  "fw" #'write-file
  "fd" #'my/delete-file-and-buffer
  "fr" #'crux-rename-file-and-buffer
  "gm" #'vc-msg-show
  "gg" #'magit-dispatch
  "gn" #'git-gutter:next-hunk
  "gp" #'git-gutter:previous-hunk
  "gi" #'git-gutter:popup-hunk
  "gs" #'git-gutter:stage-hunk
  "go" #'git-gutter:revert-hunk
  "gt" #'git-timemachine
  "gl" #'magit-log-buffer-file
  "bz" #'bury-buffer
  "iu" #'insert-char
  "xe" #'eval-last-sexp
  "xx" #'eval-defun
  "xi" #'consult-imenu
  "z" '(:keymap ssh-deploy-prefix-map :package ssh-deploy))

(let ((mail-config (expand-file-name "mail.el" user-emacs-directory)))
  (if (file-readable-p mail-config)
      (load mail-config :noerror :nomessage)))


;; # Local Variables:
;; # flycheck-disabled-checkers: 'emacs-lisp-checkdoc
;; # End: