;;; 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)

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

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

(use-package benchmark-init
  :config (progn
            (add-hook 'after-init-hook #'benchmark-init/deactivate 99)))

;;; 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)

(when (or (daemonp)
          window-system)
  (use-package stimmung-themes
    :config (progn
              (require 'stimmung-themes-light-theme)
              (let ((light-mode-theme 'stimmung-themes-light)
                    (dark-mode-theme 'stimmung-themes-dark)
                    (original-stimmung-themes-string stimmung-themes-string))
                (load-theme light-mode-theme :noconfirm :noenable)
                (load-theme dark-mode-theme :noconfirm :noenable)
                (enable-theme light-mode-theme)
                (defun toggle-stimmung-string-highlighting ()
                  (interactive)
                  (let ((current-theme (car custom-enabled-themes)))
                    (setq stimmung-themes-string (if (eq stimmung-themes-string original-stimmung-themes-string)
                                                     'none
                                                   original-stimmung-themes-string))
                    (load-theme current-theme :noconfirm)))))))

(global-set-key (kbd "<pinch>") 'ignore)
(global-set-key (kbd "<C-wheel-up>") 'ignore)
(global-set-key (kbd "<C-wheel-down>") '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)

(when (and (eq system-type 'darwin)
           (fboundp 'mac-mouse-wheel-mode))
  (mac-mouse-wheel-mode +1))
(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)))

(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 4)
(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))))

(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)

;;; Encoding

(setq-default bidi-paragraph-direction 'left-to-right
              bidi-display-reordering 'left-to-right)
(setq bidi-inhibit-bpa t
      require-final-newline t)

;;; 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))

(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
            (setq prescient-history-length 10000)
            (prescient-persist-mode +1)))

(use-package vertico-prescient
  :after prescient
  :ghook '(vertico-mode-hook))

(use-package marginalia
  :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 orderless
  :config (progn
            (add-to-list 'completion-styles 'orderless)
            (setq orderless-matching-styles '(orderless-literal-prefix
                                              orderless-prefixes
                                              orderless-regexp))))

(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 consult-dir
  :defer 10
  :general ("C-x C-d" #'consult-dir)
  (:keymaps 'vertico-map
            "C-x C-d" #'consult-dir
            "C-x C-j" #'consult-jump))

(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
  :after magit
  :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 eyebrowse
  :after (evil)
  :config (progn
            (setq eyebrowse-new-workspace t
                  eyebrowse-mode-line-left-delimiter ""
                  eyebrowse-mode-line-right-delimiter ""
                  eyebrowse-mode-line-separator " "
                  eyebrowse-mode-line-style 'always)
            (eyebrowse-mode +1))
  :general (:keymaps 'evil-window-map
                     "0" #'eyebrowse-switch-to-window-config-0
                     "1" #'eyebrowse-switch-to-window-config-1
                     "2" #'eyebrowse-switch-to-window-config-2
                     "3" #'eyebrowse-switch-to-window-config-3
                     "4" #'eyebrowse-switch-to-window-config-4
                     "5" #'eyebrowse-switch-to-window-config-5
                     "6" #'eyebrowse-switch-to-window-config-6
                     "7" #'eyebrowse-switch-to-window-config-7
                     "8" #'eyebrowse-switch-to-window-config-8
                     "9" #'eyebrowse-switch-to-window-config-9)
  :ghook ('after-init-hook #'eyebrowse-switch-to-window-config-1)
  :ghook ('evil-after-load-hook #'eyebrowse-setup-evil-keys))

(use-package winner
  :after evil
  :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)
  (:states 'normal
           ";" #'evil-ex)
  (:states '(normal motion)
           "g s" #'evil-avy-goto-symbol-1))

(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
  :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
              "<escape>")
            (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 tabnine-core
  :config (progn
            (with-demoted-errors "TabNine error: %s"
              (global-tabnine-mode))

            (define-key tabnine-completion-map (kbd "TAB") #'tabnine-accept-completion)
            (define-key tabnine-completion-map (kbd "<tab>") #'tabnine-accept-completion)

            (define-key tabnine-completion-map (kbd "M-f") #'tabnine-accept-completion-by-word)
            (define-key tabnine-completion-map (kbd "M-<return>") #'tabnine-accept-completion-by-line)
            (define-key tabnine-completion-map (kbd "C-e") #'tabnine-accept-completion-by-line)
            (define-key tabnine-completion-map (kbd "<right>") #'tabnine-accept-completion-by-line)

            (define-key tabnine-completion-map (kbd "C-g") #'tabnine-clear-overlay)
            (define-key tabnine-completion-map (kbd "M-[") #'tabnine-next-completion)
            (define-key tabnine-completion-map (kbd "M-]") #'tabnine-previous-completion))
  :init (progn
          (advice-add 'tabnine-start-process :around #'quiet)

          (add-hook 'kill-emacs-hook #'tabnine-kill-process)))

(use-package tempel
  :general ("M-+" #'tempel-complete ;; Alternative tempel-expand
            "M-*" #'tempel-insert
            :keymaps 'tempel-mode-map
            "<tab>" #'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)

(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))))

(use-package ssh-deploy
  :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)))

(use-package dired-git-info
  :general (:keymaps 'dired-mode-map
                     :states 'normal
                     ")" #'dired-git-info-mode))

;;; Shells

(use-package eshell
  :defer 5
  :commands (eshell)
  :functions (eshell/pwd)
  :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
            (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))

(use-package chatgpt-shell
  :defer 5
  :config (progn
            (chatgpt-shell-ollama-load-models :override t)
            (setq chatgpt-shell-model-version "llama3.3")))

;;; Editing

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

(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
(with-eval-after-load 'nix-mode
  (setq nix-mode-use-smie t
        nix-indent-function #'smie-indent-line))

(use-package nix-ts-mode
  :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 10
  :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
  :general (:keymaps 'project-prefix-map
                     "s" #'project-search)
  :config (progn
            (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))))

(use-package treemacs
  :general (:keymaps 'treemacs-mode-map
                     [mouse-1] #'treemacs-single-click-expand-action)
  :config (progn
            (treemacs-project-follow-mode t)
            (setq treemacs-is-never-other-window t
                  treemacs-select-when-already-in-treemacs 'move-back
                  treemacs-eldoc-display nil
                  treemacs-indentation '(8 px)
                  treemacs-show-hidden-files nil
                  treemacs-recenter-after-project-jump 'on-distance
                  treemacs-missing-project-action 'remove)))

(use-package treemacs-evil
  :after treemacs)

(use-package treemacs-magit
  :after treemacs)

(use-package treemacs-nerd-icons
  :after treemacs
  :init (progn
          (setq treemacs-nerd-icons-tab " "))
  :config (progn
            (treemacs-load-theme "simple")))

(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)
  :init (progn
          (defvar magit-auto-revert-mode nil)
          (setq magit-auto-revert-mode nil)
          (require 'project)
          (add-to-list 'project-switch-commands '(magit-project-status "Magit") t))
  :config (progn
            (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
  :after magit
  :config (progn
            (magit-todos-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 1
  :ghook ('(text-mode-hook
            prog-mode-hook
            conf-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
      (add-hook 'before-save-hook #'eglot-format-buffer nil 'local)
    (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
            css-base-mode-hook
            lua-mode-hook
            nim-mode-hook
            html-mode-hook
            nix-mode-hook
            templ-ts-mode-hook
            toml-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")))
            (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 nil)
                             :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-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 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" "--out-format" "json" "--issues-exit-code=1"])))

            (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 format-all
  :defer 10
  :ghook ('prog-mode-hook)
  :gfhook #'format-all-ensure-formatter
  :init (progn
          (advice-add 'format-all-ensure-formatter
                      :around #'quiet)
          (defun turn-off-format-all-mode ()
            (when (bound-and-true-p format-all-mode)
              (format-all-mode -1))))
  :config (progn
            (setq format-all-show-errors nil)))

(use-package apheleia
  :defer 11                             ; load after format-all for hook ordering?
  :ghook 'prog-mode-hook
  :config (progn
            (setf (alist-get 'shfmt apheleia-formatters)
                  '("shfmt"))
            (setq apheleia-formatters
                  (append apheleia-formatters '((nixpkgs-fmt "nixpkgs-fmt")
                                                (golines "golines")
                                                (taplo "taplo" "format" "-")
                                                (prettier-gotmpl
                                                 "prettier" "--stdin-filepath" filepath
                                                 "--parser=go-template" (apheleia-formatters-indent "--use-tabs" "--tab-width")))))
            (setq apheleia-mode-alist (append apheleia-mode-alist '((nix-ts-mode . nixpkgs-fmt)
                                                                    (nix-mode . nixpkgs-fmt)
                                                                    (toml-ts-mode . taplo))))
            (add-hook 'apheleia-mode-hook #'turn-off-format-all-mode))
  :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
  "r" #'my/ls-rename
  "q" #'evil-delete-buffer
  "p" '(:keymap project-prefix-map :package project)
  "v" #'split-window-right
  "o" #'other-window
  "s" #'treemacs-select-window
  "u" #'universal-argument
  ";" #'execute-extended-command
  "a" #'my/ls-code-actions
  "bb" #'consult-buffer
  "bx" #'kill-this-buffer
  "br" #'revert-buffer
  "bk" #'kill-buffer
  "dd" #'dired
  "D" #'consult-dir
  "e" '(:keymap envrc-command-map :package envrc)
  "fs" #'save-buffer
  "ff" #'find-file
  "fw" #'write-file
  "fd" #'my/delete-file-and-buffer
  "fr" #'crux-rename-file-and-buffer
  "gs" #'magit-status
  "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: