;;; init.el --- Emacs configuration file -*- lexical-binding: t; -*-

;; Author: Jessie Hildebrandt <jessieh.net>
;; Homepage: https://gitlab.com/jessieh/dot-emacs
;; Package-Requires: ((emacs "28.1"))

;; This file is not part of GNU Emacs.

;;; Commentary:

;; jessieh's (Mostly) Portable Emacs Config
;;
;; This configuration file was designed to work with Emacs 28.1, and will not
;; work with earlier versions.  You should probably have native-comp enabled.
;;
;; This file will automatically generate an early-init.el file upon first run,
;; or if early-init.el is missing.
;;
;; All configuration efforts here are organized around the use of wrapped
;; `use-package' macros.  The keyword order for any config entry should be:
;;  - if/when/unless
;;  - demand
;;  - after
;;  - requires
;;  - defines/functions
;;  - init/config
;;  - custom/custom-faces
;;  - commands
;;  - mode/interpreter/hook
;;  - bind/bind*/bind-keymap/bind-keymap*
;;
;; Custom functions and variables should generally be prefixed with "user/".
;; This prefix is purposefully unidiomatic so as to avoid collisions with any
;; package prefixes, inbuilt or otherwise.

;;; License:

;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 2, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
;; Floor, Boston, MA 02110-1301, USA.

;;; Code:

;; -------------------------------------------------------------------------- ;;
;;
;; Startup
;;
;; -------------------------------------------------------------------------- ;;

(defconst user/font "Fira Code 10" "Default font.")

(defconst user/init-file (locate-user-emacs-file "init.el") "Location of user init file.")
(defconst user/early-init-file (locate-user-emacs-file "early-init.el") "Location of user early-init file.")

;; ---------------------------------- ;;
;; early-init.el file creation
;; ---------------------------------- ;;

(defvar user/early-init-forms
  `(";;; early-init.el --- Early initialization -*- lexical-binding: t; -*-"

    ";; This file is not part of GNU Emacs."

    ";;; Commentary:"

    ";; This file has been automatically generated by init.el."
    ";; More relevant commentary is likely in init.el, not here."

    ";;; Code:"

    (defconst user/gc-cons-threshold (* 32 1024 1024) "Preferred garbage collection threshold value (32MB).")
    (defconst user/gc-cons-percentage 0.1 "Preferred garbage collection percentage value (10%).")

    (defun user/defer-garbage-collection ()
      "Defer garbage collection by maximizing the collection threshold."
      (setq gc-cons-threshold most-positive-fixnum
            gc-cons-percentage 1.0))
    (defun user/restore-garbage-collection ()
      "Restore the garbage collection threshold parameters in a deferred fashion."
      (setq gc-cons-threshold user/gc-cons-threshold
            gc-cons-percentage user/gc-cons-percentage))

    ";; Defer garbage collection until after initialization"
    (user/defer-garbage-collection)
    (add-hook 'emacs-startup-hook #'user/restore-garbage-collection)

    ";; Clear `file-name-handler-alist' until after initialization"
    (let ((default-file-name-handler-alist file-name-handler-alist))
      (setq file-name-handler-alist nil)
      (add-hook 'emacs-startup-hook (lambda () (setq file-name-handler-alist default-file-name-handler-alist))))

    ";; Configure GUI components before initial frame creation"
    (setq frame-inhibit-implied-resize t
          default-frame-alist '((font . ,user/font)
                                (menu-bar-lines . 0)
                                (tool-bar-lines . 0)
                                (vertical-scroll-bars . nil)
                                (background-color . "gray15")
                                (foreground-color . "gray85")))

    ";; package.el initialization is handled manually in init.el"
    (setq package-enable-at-startup nil)

    ";;; init.el ends here")
  "List of forms that are written to the user early-init file.")

(defun user/write-forms-to-file (forms file)
  "Format and pretty-print list FORMS to FILE."
  (with-temp-file file
    (mapcar (lambda (form)
              (insert (concat
                       (if (stringp form)
                           (prin1-to-string form :no-escape)
                         (pp-to-string form))
                       "\n")))
            forms)))

;; Create (and load) early-init file if it does not yet exist, or if this file
;; is currently being recompiled. We recreate early-init.el during compilation
;; in case `user/early-init-forms' has been changed
(unless (file-exists-p user/early-init-file)
  (user/write-forms-to-file user/early-init-forms user/early-init-file)
  (load user/early-init-file nil nil :no-suffix))

;; Make sure that the user init files are byte-compiled.
(let ((byte-compile-warnings nil))
  (when (file-newer-than-file-p user/early-init-file (concat user/early-init-file "c"))
    (byte-compile-file user/early-init-file))
  (when (file-newer-than-file-p user/init-file (concat user/init-file "c"))
    (byte-compile-file user/init-file)))

;; ---------------------------------- ;;
;; Package manager initialization
;; ---------------------------------- ;;

;; To be evaluated at both compile time and run time
(eval-and-compile
  (setq package-user-dir (locate-user-emacs-file "package/")
        package-native-compile t
        package-check-signature nil
        use-package-hook-name-suffix nil
        use-package-always-demand (daemonp)))

;; To be evaluated only at compile time
(eval-when-compile

  ;; Initialize package.el
  (require 'package)
  (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
  (unless (bound-and-true-p package--initialized)
    (package-initialize))
  (package-refresh-contents)

  ;; Install use-package
  (unless (package-installed-p 'use-package)
    (package-install 'use-package))
  (require 'use-package)

  ;; After initialization, build quickstart file containing package autoload defs
  ;; and compile it alongside any other uncompiled Elisp files in installed packages
  (add-hook 'emacs-startup-hook (lambda ()
                                  (let ((byte-compile-warnings nil))
                                    (byte-recompile-directory package-user-dir 0)
                                    (package-quickstart-refresh)))))

;; Load "package-quickstart.el" file containing package autoload defs with a
;; call to `package-activate-all'. This is must be done to load any installed
;; packages during initialization outside of compile time because we won't have
;; called `package-initialize'. We also require `bind-key' here, which the
;; compiled `use-package' macros still rely on for handling keybinds
(unless (bound-and-true-p package--initialized)
  (package-activate-all)
  (require 'bind-key))

;; -------------------------------------------------------------------------- ;;
;;
;; Custom interactive functions
;;
;; -------------------------------------------------------------------------- ;;

(defun user/open-init-file ()
  "Opens the user init file in a new buffer."
  (interactive)
  (find-file user/init-file))

(defun user/byte-compile-init-files ()
  "Byte-compile the user init and early-init files."
  (interactive)
  (message "Byte-compiling init and early-init file...")
  (byte-compile-file user/init-file)
  (byte-compile-file user/early-init-file))

(defun user/download-latest-init-file ()
  "Download the latest user init file from jessieh.net/emacs.

The user init file will be automatically recompiled after downloading.

If `user/init-file' points to a symlink, nothing will be downloaded."
  (interactive)
  (if (file-symlink-p user/init-file)
      (message "%s is a symlink, operation aborted. Please update the init file manually." user/init-file)
    (when (yes-or-no-p "Download latest init file from jessieh.net/emacs? ")
      (message "Updating init file...")
      (url-copy-file "https://jessieh.net/emacs" user/init-file :ok-if-already-exists)
      (user/byte-compile-init-files))))

(defun user/refresh-packages ()
  "Refresh packages that have been configured for use in the user init file.

This is accomplished by deleting `package-user-dir' and recompiling the user
init file, which initializes the package manager during compile time."
  (interactive)
  (when (yes-or-no-p "Redownload and refresh packages? ")
    (message "Refreshing packages...")
    (delete-directory package-user-dir :recursive)
    (user/byte-compile-init-files)))

(defun user/indent-buffer ()
  "Call `indent-region' on the contents of the active buffer."
  (interactive)
  (indent-region (point-min) (point-max)))

(defun user/select-minibuffer-window ()
  "Select the minibuffer window if it is active."
  (interactive)
  (when (active-minibuffer-window)
    (select-window (active-minibuffer-window))))

(defun user/scratch-buffer ()
  "Open the scratch buffer, (re)creating it if not present."
  (interactive)
  (pop-to-buffer (get-buffer-create "*scratch*") '((display-buffer-reuse-window display-buffer-same-window)))
  (lisp-interaction-mode))

(defun user/scan-directory-for-projects ()
  "Prompt for a directory and then scan for any project roots within.

Inaccessible directories and .git directories are skipped during searching.
When done searching, you will be shown a buffer of all discovered project
directories and will be prompted to index them.  Choosing to index the project
directories will register them with Emacs' project management system."
  (interactive)
  (when-let* ((valid-root-p (lambda (path)
                              (and (file-accessible-directory-p path)
                                   (not (string= (file-name-nondirectory path) ".git")))))
              (directory (read-directory-name "Look for projects in: "))
              (project-dir-list (mapcar #'file-name-directory
                                        (directory-files-recursively directory "\\.git$" #'valid-root-p t)))
              (num-projects-found (length project-dir-list))
              (temp-buffer-name (concat "*" (number-to-string num-projects-found) " Project Roots Found*")))
    (with-output-to-temp-buffer temp-buffer-name
      (princ (mapconcat #'identity project-dir-list "\n")))
    (when (yes-or-no-p "Index all of these directories as projects? ")
      (unless (fboundp 'project-remember-projects-under)
        (require 'project))
      (mapc #'project-remember-projects-under project-dir-list)
      (message "%d%s" num-projects-found " directories indexed as projects."))
    (with-current-buffer temp-buffer-name
      (kill-buffer-and-window))))

;; -------------------------------------------------------------------------- ;;
;;
;; Internal/built-in packages
;;
;; -------------------------------------------------------------------------- ;;

(defmacro editor-feature (name docstring &rest args)
  "Apply NAME and ARGS to `use-package' with `:ensure' defaulted to nil.

DOCSTRING is an optional form that is discarded upon expansion."
  (declare (doc-string 2)
           (indent defun))
  (ignore docstring)
  `(use-package ,name :ensure nil ,@args))

;; ---------------------------------- ;;
;; emacs
;; ---------------------------------- ;;

(defun user/ensure-region-active (func &rest args)
  "Apply ARGS to FUNC only if `region-active-p' is non-nil."
  (when (region-active-p)
    (apply func args)))

(editor-feature emacs
  "Provides an extensible, customizable, self-documenting real-time display editor"

  :config

  ;; Enable upcase/downcase region commands
  (put 'upcase-region 'disabled nil)
  (put 'downcase-region 'disabled nil)
  (advice-add 'upcase-region :around 'user/ensure-region-active)
  (advice-add 'downcase-region :around 'user/ensure-region-active)

  :custom

  ;; Default working directory
  (default-directory (if (eq system-type 'windows-nt)
                         (setq default-directory (getenv "USERPROFILE"))
                       (setq default-directory "~/")))

  ;; General configuration
  (frame-title-format '("Emacs - %b") "Set frame title to buffer name")
  (truncate-lines t "Truncate lines instead of wrapping")
  (message-truncate-lines t "Truncate messages in the echo area")
  (cursor-in-non-selected-windows nil "Hide cursor in inactive windows")
  (x-gtk-use-system-tooltips nil "Disable GTK tooltips in favor of in-editor tooltips")
  (ring-bell-function 'ignore "Disable terminal bell")
  (fill-column 80 "Set default line-wrap column to column 80")
  (max-mini-window-height 10 "Limit minibuffer height to 10 lines")
  (enable-recursive-minibuffers t "Allow minibuffer commands to be called in the minibuffer")
  (load-prefer-newer t "Load from source files if they are newer than bytecode files")

  ;; Startup
  (initial-scratch-message "" "Leave scratch buffer empty on startup")
  (inhibit-startup-screen t "Do not create or show the initial splash screen")
  (inhibit-default-init t "Do not attempt to load any OS-provided init files")

  ;; Default style rules
  (indent-tabs-mode nil "Use spaces for indentation")
  (tab-width 4 "Use 4 spaces for indentation")

  ;; Scrolling
  (mouse-wheel-progressive-speed nil "Disable mouse wheel acceleration during scrolling")
  (scroll-preserve-screen-position 1 "Prevent the cursor from moving during scrolling")
  (scroll-conservatively 101 "Scroll only one line at a time when cursor leaves view")
  (scroll-margin 5 "Maintain margin of 5 lines around cursor during scrolling")

  ;; Performance tweaks
  (redisplay-skip-fontification-on-input t "Improve redisplay performance while scrolling")
  (fast-but-imprecise-scrolling t "Improve redisplay performance while scrolling")
  (jit-lock-defer-time 0 "Defer fontification while input is pending")
  (auto-window-vscroll nil "Prevent calcuation of arbitrary line heights while scrolling")
  (auto-mode-case-fold nil "Disable case-insensitive second pass over `auto-mode-alist'")

  ;; Bidirectional text
  (bidi-inhibit-bpa t "Disable parentheses matching for bidirectional text")
  (bidi-display-reordering 'left-to-right "Force global left-to-right text direction")
  (bidi-paragraph-direction 'left-to-right "Disable paragraph directionality detection")

  ;; Elisp compilation warnings
  (native-comp-async-report-warnings-errors nil "Don't report errors from async native compilation")
  (byte-compile-warnings '(not lexical
                               free-vars
                               noruntime
                               unresolved
                               docstrings))

  ;; Filter out buffer-incompatible interactive commands by default
  (read-extended-command-predicate #'command-completion-default-include-p)

  :bind

  ;; General binds
  ("C-z" . undo)
  ("C-c SPC" . tmm-menubar)
  ("C-c DEL" . fixup-whitespace)
  ("C-c d" . delete-pair)
  ("C-c q" . visual-line-mode)
  ("C-c r" . revert-buffer-quick)
  ("C-c f" . display-fill-column-indicator-mode)
  ("C-c \\" . user/indent-buffer)
  ("C-c o" . user/select-minibuffer-window)
  ("C-c s" . user/scratch-buffer)

  ;; Navigation
  ("M-n" . scroll-up-line)
  ("M-p" . scroll-down-line)
  ("C-M-n" . forward-paragraph)
  ("C-M-p" . backward-paragraph)

  :bind*

  ;; Window navigation
  ("C-c C-i" . windmove-up)
  ("C-c C-k" . windmove-down)
  ("C-c C-j" . windmove-left)
  ("C-c C-l" . windmove-right))

;; ---------------------------------- ;;
;; autorevert
;; ---------------------------------- ;;

(editor-feature autorevert
  "Displays on-disk changes to files in unmodified buffers automatically"
  :config
  (global-auto-revert-mode))

;; ---------------------------------- ;;
;; display-line-numbers
;; ---------------------------------- ;;

(editor-feature display-line-numbers
  "Displays the absolute number of each line in a buffer"
  :custom
  (display-line-numbers-width-start 100 "Count number of lines (+100) in buffer for initial line number width")
  :hook
  (prog-mode-hook . (lambda ()
                      (unless (derived-mode-p 'lisp-interaction-mode)
                        (display-line-numbers-mode))))
  :bind
  ("C-c n" . display-line-numbers-mode))

;; ---------------------------------- ;;
;; elec-pair
;; ---------------------------------- ;;

(editor-feature elec-pair
  "Enables automatic pairing of most paired delimiters"
  :hook
  (prog-mode-hook . electric-pair-local-mode))

;; ---------------------------------- ;;
;; eshell
;; ---------------------------------- ;;

(defun user/abbrev-path (path)
  "Return a PATH with all but the last directory name abbreviated."
  (let* ((components (split-string (abbreviate-file-name path) "/"))
         (str ""))
    (while (cdr components)
      (setq str (concat str
                        (cond ((= 0 (length (car components)))
                               "/")
                              ((= 1 (length (car components)))
                               (concat (car components) "/"))
                              (t
                               (if (string= "." (string (elt (car components) 0)))
                                   (concat (substring (car components) 0 2) "/")
                                 (string (elt (car components) 0) ?/)))))
            components (cdr components)))
    (concat (propertize str 'face 'font-lock-comment-face)
            (propertize (cl-reduce (lambda (a b) (concat a "/" b)) components) 'face 'font-lock-doc-face))))

(defun user/open-eshell ()
  "Switch to an active unfocused `eshell' session, or start a new one."
  (interactive)
  (let ((eshell-buffer (cl-find-if (lambda (buffer)
                                     (eq (buffer-local-value 'major-mode buffer)
                                         'eshell-mode))
                                   (buffer-list))))
    (if eshell-buffer
        (if (eq (current-buffer) eshell-buffer)
            (eshell :new-session)
          (pop-to-buffer eshell-buffer '((display-buffer-reuse-window display-buffer-same-window))))
      (eshell))))

(editor-feature eshell
  "Provides a shell-like interpreter that can process shell or lisp commands"
  :custom
  (eshell-banner-message '(concat "\n" (if (executable-find "fortune")
                                           (shell-command-to-string "fortune -s computers")
                                         "Hello, commander.")))
  (eshell-prompt-function (lambda ()
                            (concat "\n" (user/abbrev-path (eshell/pwd)) (if (= (user-uid) 0) " # " " ❱ "))))
  (eshell-prompt-regexp "^[^#❱\n]* [#❱] ")
  (eshell-visual-commands '("fish" "bash"
                            "ssh" "mosh"
                            "fzf" "top" "htop" "less"))
  (eshell-destroy-buffer-when-process-dies t "Destroys child buffers after their process returns")
  (eshell-error-if-no-glob t "Produces an error if a glob pattern fails to match, like zsh")
  (eshell-hist-ignoredups t "Treat multiple repeating history entries as a single entry")
  :hook
  (eshell-post-command-hook . (lambda ()
                                (rename-buffer (concat eshell-buffer-name
                                                       " "
                                                       (user/abbrev-path (eshell/pwd)))
                                               :unique)))
  (eshell-mode-hook . (lambda ()
                        (setq-local global-hl-line-mode nil
                                    tab-line-tabs-function #'tab-line-tabs-mode-buffers)
                        (tab-line-mode)))
  :bind
  ("C-c RET" . user/open-eshell))

;; `eshell-mode-map' is not available until the `esh-mode' module is loaded, so
;; the local keymap bindings are set up here

(editor-feature esh-mode
  "eshell module that handles input from the user"
  :bind
  (:map eshell-mode-map
        ("C-c RET" . nil)
        ("C-<return> . eshell-copy-old-input")
        ("C-c j" . tab-line-switch-to-prev-tab)
        ("C-c l" . tab-line-switch-to-next-tab)))

;; ---------------------------------- ;;
;; files
;; ---------------------------------- ;;

(defconst user/custom-file (locate-user-emacs-file "custom.el") "Location of user customizations file.")
(defconst user/backup-directory (locate-user-emacs-file "backup/") "Location of user backup directory.")
(defconst user/auto-save-directory (locate-user-emacs-file "auto-save/") "Location of user auto save directory.")
(defconst user/lock-file-directory (locate-user-emacs-file "lock-file/") "Location of user lock file directory.")

(make-directory user/backup-directory :parents)
(make-directory user/auto-save-directory :parents)
(make-directory user/lock-file-directory :parents)

(editor-feature files
  "Defines most of Emacs' file-handling functionality"

  :custom

  ;; Config file
  (custom-file user/custom-file "Store customization info in a separate file")

  ;; Directories
  (backup-directory-alist `((".*" . ,user/backup-directory)))
  (auto-save-file-name-transforms `((".*" ,user/auto-save-directory t)))
  (lock-file-name-transforms `((".*" ,user/lock-file-directory t)))

  ;; Backup behavior
  (backup-by-copying t "Use copying unconditionally when creating backups")
  (version-control t "Use version numbers on backup files")
  (delete-old-versions t "Clean up old backup files")
  (kept-new-versions 5 "Keep 5 recent backup files")
  (kept-old-versions 3 "Keep 3 old backup files")

  :hook
  (before-save-hook . delete-trailing-whitespace))

;; ---------------------------------- ;;
;; frame
;; ---------------------------------- ;;

(defvar user/initial-frame-created nil "Whether or not the first frame has been initialized by the server.")
(defvar user/after-daemon-make-initial-frame-functions nil "Run when the first frame is produced by the server in daemon mode.")

(defun user/set-up-frame (frame)
  "Set up newly-created frame FRAME."
  (when (display-graphic-p frame)
    (let ((winid (frame-parameter frame 'outer-window-id)))
      (start-process "" nil "xprop" "-f" "_GTK_THEME_VARIANT" "8u" "-set" "_GTK_THEME_VARIANT" "dark" "-id" winid))
    (when (and (daemonp)
               (not user/initial-frame-created))
      (with-selected-frame frame
        (run-hooks 'user/after-daemon-make-initial-frame-functions)
        (setq user/initial-frame-created t)))))

(editor-feature frame
  "Graphical frame configuration"
  :config
  (mapc #'user/set-up-frame (frame-list))
  :hook
  (after-make-frame-functions . user/set-up-frame))

;; ---------------------------------- ;;
;; hl-line
;; ---------------------------------- ;;

(editor-feature hl-line
  "Highlights the current line in a buffer"
  :config
  (global-hl-line-mode))

;; ---------------------------------- ;;
;; ispell
;; ---------------------------------- ;;

(editor-feature ispell
  "Checks for spelling errors and suggests corrections from a dictionary"
  :bind
  ("C-c ' '" . ispell)
  ("C-c ' w" . ispell-word)
  ("C-c ' r" . ispell-region)
  ("C-c ' b" . ispell-buffer)
  ("C-c ' ;" . ispell-comment-or-string-at-point)
  ("C-c ' C-;" . ispell-comments-and-strings))

;; ---------------------------------- ;;
;; paren
;; ---------------------------------- ;;

(editor-feature paren
  "Highlights matching delimiter pairs under the cursor"
  :config
  (show-paren-mode)
  :custom
  (show-paren-delay 0.0 "Highlight matching delimiters instantly"))

;; ---------------------------------- ;;
;; savehist
;; ---------------------------------- ;;

(editor-feature savehist
  "Persists minibuffer history between sessions"
  :config
  (savehist-mode))

;; ---------------------------------- ;;
;; server
;; ---------------------------------- ;;

(editor-feature server
  "Allows Emacs to operate as a server for other Emacs processes"
  :config
  ;; When running in a server/client configuration, we don't want the
  ;; foreground/background colors in `default-frame-alist' to overwrite the
  ;; colors set by the active theme every time we open a new client frame
  (when (daemonp)
    (assoc-delete-all 'foreground-color default-frame-alist)
    (assoc-delete-all 'background-color default-frame-alist))
  :custom
  (server-client-instructions nil "Suppress help messages from the server for new frames"))

;; ---------------------------------- ;;
;; so-long
;; ---------------------------------- ;;

(editor-feature so-long
  "Deactivates certain editor features when opening files with very long lines"
  :config
  (global-so-long-mode))

;; ---------------------------------- ;;
;; subword
;; ---------------------------------- ;;

(editor-feature subword
  "Enables detection of subwords as words in camel case or pascal case names"
  :config
  (global-subword-mode))

;; ---------------------------------- ;;
;; tab-bar
;; ---------------------------------- ;;

(editor-feature tab-bar
  "Provides a frame-wide tab bar that allows for tabbed workspace switching"
  :custom
  (tab-bar-format '(tab-bar-format-tabs tab-bar-separator))
  (tab-bar-close-button-show nil "Disable close button on tabs"))

;; ---------------------------------- ;;
;; tab-line
;; ---------------------------------- ;;

(editor-feature tab-line
  "Provides a buffer-local tab line that facilitates quick buffer switching"
  :custom
  (tab-line-close-button-show nil "Disable close button on tabs")
  (tab-line-new-button-show nil "Disable tab creation button")
  (tab-line-left-button nil "Disable the left scroll button")
  (tab-line-right-button nil "Disable the right scroll button")
  (tab-line-switch-cycling t "Enable wrap-around tab cycling"))

;; ---------------------------------- ;;
;; uniquify
;; ---------------------------------- ;;

(editor-feature uniquify
  "Provides better unique names when there are name conflicts between buffers"
  :custom
  (uniquify-buffer-name-style 'forward "Show file path before buffer name")
  (uniquify-after-kill-buffer-p t "Update buffer names after killing")
  (uniquify-ignore-buffers-re "^\\*" "Avoid renaming special buffers"))

;; -------------------------------------------------------------------------- ;;
;;
;; External packages
;;
;; -------------------------------------------------------------------------- ;;

(defmacro external-package (name docstring &rest args)
  "Apply NAME and ARGS to `use-package' with `:ensure' defaulted to t.

DOCSTRING is an optional form that is discarded upon expansion."
  (declare (doc-string 2)
           (indent defun))
  (ignore docstring)
  `(use-package ,name :ensure t ,@args))

;; ---------------------------------- ;;
;; Theme setup
;; ---------------------------------- ;;

;; TEMP: Remove :load-path and change to `external-package' once
;; adwaita-dark-theme is available in MELPA
(editor-feature adwaita-dark-theme
  "Provides the `adwaita-dark' theme and custom fringe bitmaps"
  :load-path
  "adwaita-dark/"
  :config
  (load-theme 'adwaita-dark :no-confirm)
  (adwaita-dark-theme-arrow-fringe-bmp-enable)
  (eval-after-load 'diff-hl #'adwaita-dark-theme-diff-hl-fringe-bmp-enable)
  (eval-after-load 'flymake #'adwaita-dark-theme-flymake-fringe-bmp-enable)
  (eval-after-load 'flycheck #'adwaita-dark-theme-flycheck-fringe-bmp-enable)
  (eval-after-load 'neotree #'adwaita-dark-theme-neotree-configuration-enable)
  :hook
  (user/after-daemon-make-initial-frame-functions . (lambda ()
                                                      (load-theme 'adwaita-dark :no-confirm))))

;; ---------------------------------- ;;
;; Simple language modes
;; ---------------------------------- ;;

(external-package csharp-mode
  "Major mode for C#"
  :mode
  ("\\.cs\\'" . csharp-mode))

(external-package dart-mode
  "Major mode for Dart"
  :mode
  ("\\.dart\\'" . dart-mode))

(external-package docker-compose-mode
  "Major mdoe for docker-compose files"
  :mode
  ("\\docker-compose.yml\\'" . docker-compose-mode))

(external-package dockerfile-mode
  "Major mode for Docker files"
  :mode
  ("\\Dockerfile\\'" . dockerfile-mode))

(external-package fennel-mode
  "Major mode for Fennel"
  :mode
  ("\\.fnl\\'" . fennel-mode))

(external-package fish-mode
  "Major mode for fish files"
  :mode
  ("\\.fish\\'" . fish-mode))

(external-package gdscript-mode
  "Major mode for GDScript"
  :mode
  ("\\.gd\\'" . gdscript-mode)
  ("\\.tscn\\'" . gdscript-mode))

(external-package git-modes
  "Major modes for git files"
  :mode
  ("\\.gitignore\\'" . gitignore-mode)
  ("\\.gitconfig\\'" . gitconfig-mode)
  ("\\.gitmodules\\'" . gitconfig-mode)
  ("\\.gitattributes\\'" . gitattributes-mode))

(external-package glsl-mode
  "Major mode for GLSL"
  :mode
  ("\\.frag\\'" . glsl-mode)
  ("\\.vert\\'" . glsl-mode))

(external-package json-mode
  "Major mode for JSON files"
  :mode
  ("\\.json\\'" . json-mode))

(external-package kotlin-mode
  "Major mode for Kotlin"
  :mode
  ("\\.kt\\'" . kotlin-mode))

(external-package lua-mode
  "Major mode for Lua"
  :mode
  (("\\.lua\\'" . lua-mode)
   ("\\.rockspec\\'" . lua-mode)))

(external-package rust-mode
  "Major mode for Rust"
  :mode
  ("\\.rs\\'" . rust-mode))

(external-package typescript-mode
  "Major mode for TypeScript"
  :mode
  ("\\.tsx?\\'" . typescript-mode))

(external-package web-mode
  "Major mode for web templates"
  :custom
  (web-mode-markup-indent-offset 2 "Use 2 spaces instead of 4 for indenting HTML elements")
  (web-mode-enable-auto-quoting nil "Do not automatically insert quotes after HTML attributes")
  :mode
  (("\\.php\\'" . web-mode)
   ("\\.html\\'" . web-mode)))

;; ---------------------------------- ;;
;; anzu
;; ---------------------------------- ;;

(external-package anzu
  "Displays matching and replacement information in the mode line"
  :config
  (global-anzu-mode)
  :bind
  (("<remap> <query-replace>" . anzu-query-replace)
   ("<remap> <query-replace-regexp>" . anzu-query-replace-regexp)))

;; ---------------------------------- ;;
;; bufler
;; ---------------------------------- ;;

(external-package bufler
  "Presents open buffers in a grouped and organized menu"
  :custom
  (bufler-columns '("Name" "Path"))
  (bufler-list-group-separators '((0 . "\n")))
  (bufler-column-name-modified-buffer-sigil " ●")
  (bufler-list-display-buffer '((display-buffer-reuse-window display-buffer-same-window)))
  (bufler-groups (bufler-defgroups
                   (group
                    ;; Subgroup collecting all named workspaces.
                    (auto-workspace))
                   (group
                    ;; Subgroup collecting all `help-mode' and `info-mode' buffers.
                    (group-or "Help/Info"
                              (mode-match "Help" (rx bos "help-"))
                              (mode-match "Info" (rx bos "info-"))))
                   (group
                    ;; Subgroup collecting all special buffers (i.e. ones that are not file-backed),
                    ;; except certain ones like Dired, Forge, or Magit buffers (which are allowed to
                    ;; fall through to other groups, so they end up grouped with their project buffers).
                    (group-not "Special"
                               (group-or "Special"
                                         (mode-match "Magit" (rx bos "magit-"))
                                         (mode-match "Forge" (rx bos "forge-"))
                                         (mode-match "Dired" (rx bos "dired"))
                                         (mode-match "grep" (rx bos "grep-"))
                                         (mode-match "Compilation" (rx bos "compilation-"))
                                         (auto-file)))
                    (group
                     ;; Subgroup collecting these "special special" buffers
                     ;; separately for convenience.
                     (name-match "Emacs"
                                 (rx bos "*" (or "Messages" "Warnings" "scratch" "Backtrace") "*")))
                    (group
                     ;; Subgroup collecting all other Magit buffers, grouped by directory.
                     (mode-match "Magit" (rx bos "magit-"))
                     (auto-directory))
                    ;; Remaining special buffers are grouped automatically by mode.
                    (auto-mode))
                   (group
                    ;; Subgroup collecting buffers in a version-control project,
                    ;; grouping them by directory (using the parent project keeps,
                    ;; e.g. git worktrees with their parent repos).
                    (auto-parent-project)
                    (group-not "Special"
                               ;; This subgroup collects special buffers so they are
                               ;; easily distinguished from file buffers.
                               (group-or "Non-file-backed and neither Dired nor Magit"
                                         (mode-match "Magit Status" (rx bos "magit-status"))
                                         (mode-match "Dired" (rx bos "dired-"))
                                         (auto-file))))
                   ;; Group remaining buffers by directory, then major mode.
                   (auto-directory)
                   (auto-mode)))
  :bind
  (("C-x C-b" . bufler-list)))

;; ---------------------------------- ;;
;; consult
;; ---------------------------------- ;;

(external-package consult
  "Provides a number of autocompletion-enhanced replacements for default commands"
  :config
  ;; `eshell-mode-map' is not available until the `esh-mode' module is loaded
  (eval-after-load 'esh-mode (lambda ()
                               (bind-key "C-r" #'consult-history 'eshell-mode-map)))
  :bind
  (("M-s l" . consult-line)
   ("M-s L" . consult-line-multi)
   ("C-S-s" . consult-line-multi)
   ("C-x M-b" . consult-buffer-other-window)
   ("<remap> <goto-line>" . consult-goto-line)
   ("<remap> <isearch-forward>" . consult-line)
   ("<remap> <switch-to-buffer>" . consult-buffer)
   ("<remap> <yank-pop>" . consult-yank-from-kill-ring)
   ("<remap> <project-find-regexp>" . consult-ripgrep)))

;; ---------------------------------- ;;
;; corfu
;; ---------------------------------- ;;

(external-package corfu
  "Enhances completion suggestions with a visible popup"
  :init
  (global-corfu-mode)
  :config
  (corfu-popupinfo-mode)
  :custom
  (corfu-auto t "Automatically display popups wherever available")
  (corfu-min-width 20 "Ensure completion popups are at least 20 columns wide")
  (corfu-max-width 50 "Limit completion popup width to 50 columns")
  (corfu-popupinfo-hide nil "Don't hide doc popups while scrolling between candidates")
  (corfu-popupinfo-delay 0.1 "Wait 0.1 seconds before showing a doc popup for a candidate")
  (corfu-echo-documentation nil "Disable displaying documentation strings in the echo area")
  :hook
  ;; Displaying popups aggressively (i.e. without summoning them with a key press) can
  ;; cause the cursor to jump around in `eshell-mode'
  (eshell-mode-hook . (lambda ()
                        (setq-local corfu-auto nil)))
  :bind
  (:map corfu-popupinfo-map
        ("M-n" . corfu-popupinfo-scroll-up)
        ("M-p" . corfu-popupinfo-scroll-down)))

;; ---------------------------------- ;;
;; corfu-terminal
;; ---------------------------------- ;;

(external-package corfu-terminal
  "Allows Corfu to function when not running in a graphical frame"
  :when
  (not (display-graphic-p))
  :config
  (corfu-terminal-mode))

;; ---------------------------------- ;;
;; diff-hl
;; ---------------------------------- ;;

(external-package diff-hl
  "Highlights uncommited changes to version controlled files in the gutter"
  :config
  (global-diff-hl-mode)
  :hook
  (diff-hl-mode-hook . diff-hl-flydiff-mode))

;; ---------------------------------- ;;
;; fish-completion
;; ---------------------------------- ;;

(external-package fish-completion
  "Sources shell completion candidates from the fish shell"
  :when
  (executable-find "fish")
  :hook
  (eshell-mode-hook . fish-completion-mode))

;; ---------------------------------- ;;
;; flycheck
;; ---------------------------------- ;;

(external-package flycheck
  "Enables on-the-fly syntax checking for supported languages"
  :custom
  (flycheck-idle-change-delay 2 "Wait for 2 seconds of idling before invoking any checkers")
  (flycheck-check-syntax-automatically '(save idle-change mode-enabled))
  :hook
  (prog-mode-hook . flycheck-mode)
  :bind
  ("C-c e" . flycheck-list-errors))

;; ---------------------------------- ;;
;; fussy
;; ---------------------------------- ;;

(external-package fussy
  "Provides a flexible completion style that scores and sorts candidates"
  :custom
  (completion-ignore-case t "Ignore case in completion candidates")
  (completion-category-defaults nil "Disable category-specific completion styles")
  (completion-category-overrides nil "Disable category-specific completion styles")
  (completion-styles '(substring fussy basic) "Set fussy as a fallback completion style"))

;; ---------------------------------- ;;
;; hl-todo
;; ---------------------------------- ;;

(external-package hl-todo
  "Highlights certain keywords in comments"
  :custom
  (hl-todo-keyword-faces '(("TODO" . hl-todo)
                           ("TEMP" . hl-todo)
                           ("HACK" . hl-todo)
                           ("DEBUG" . hl-todo)
                           ("FIXME" . hl-todo)))
  :hook
  (prog-mode-hook . hl-todo-mode))

;; ---------------------------------- ;;
;; kind-icon
;; ---------------------------------- ;;

(external-package kind-icon
  "Adds contextual icons in front of Corfu completion candidates"
  :config
  (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter)
  :custom
  (kind-icon-use-icons nil "Use stylized text labels instead of graphical badges")
  (kind-icon-default-face 'corfu-default "Stylize characters using the same face as Corfu"))

;; ---------------------------------- ;;
;; ligature
;; ---------------------------------- ;;

(external-package ligature
  "Enables support for mapping characters to ligatures"
  :config
  (ligature-set-ligatures 'prog-mode
                          '(;; This set of ligatures is for Fira Code, but
                            ;; should work for most any font with ligatures

                            ;; && &&&
                            ;; ;; ;;;
                            ;; %% %%%
                            ;; ?? ??? ?:  ?=  ?.
                            ;; !! !!! !. !: !!. != !== !~
                            (";" (rx (+ ";")))
                            ("&" (rx (+ "&")))
                            ("%" (rx (+ "%")))
                            ("?" (rx (or ":" "=" "\." (+ "?"))))
                            ("!" (rx (+ (or "=" "!" "\." ":" "~"))))

                            ;; \\ \\\ \/
                            ;; ++ +++ ++++ +>
                            ;; :: ::: :::: :> :< := :// ::=
                            ;; // /// //// /\ /* /> /===:===!=//===>>==>==/
                            ;; == === ==== => =| =>>=>=|=>==>> ==< =/=//=// =~ =:= =!=
                            ("\\" (rx (or "/" (+ "\\"))))
                            ("+" (rx (or ">" (+ "+"))))
                            (":" (rx (or ">" "<" "=" "//" ":=" (+ ":"))))
                            ("/" (rx (+ (or ">"  "<" "|" "/" "\\" "\*" ":" "!" "="))))
                            ("=" (rx (+ (or ">" "<" "|" "/" "~" ":" "!" "="))))

                            ;; |> ||> |||> ||||> |] |} || ||| |-> ||-||
                            ;; |->>-||-<<-| |- |== ||=|| |==>>==<<==<=>==//==/=!==:===>
                            ("|" (rx (+ (or ">" "<" "|" "/" ":" "!" "}" "\]" "-" "=" ))))

                            ;; *> */ *)  ** *** ****
                            ;; .. ... .... .= .- .? ..= ..<
                            ;; -- --- ---- -~ -> ->> -| -|->-->>->--<<-|
                            ;; #: #= #! #( #? #[ #{ #_ #_( ## ### #####
                            ;; >: >- >>- >--|-> >>-|-> >= >== >>== >=|=:=>> >> >>> >>>>
                            ("*" (rx (or ">" "/" ")" (+ "*"))))
                            ("\." (rx (or "=" "-" "\?" "\.=" "\.<" (+ "\."))))
                            ("-" (rx (+ (or ">" "<" "|" "~" "-"))))
                            ("#" (rx (or ":" "=" "!" "(" "\?" "\[" "{" "_(" "_" (+ "#"))))
                            (">" (rx (+ (or ">" "<" "|" "/" ":" "=" "-"))))

                            ;; <> <!-- <|> <: <~ <~> <~~ <+ <* <$ </  <+> <*>
                            ;; <$> </> <|  <||  <||| <|||| <- <-| <-<<-|-> <->>
                            ;; <<-> <= <=> <<==<<==>=|=>==/==//=!==:=> << <<< <<<<
                            ("<" (rx (+ (or "\+" "\*" "\$" "<" ">" ":" "~"  "!" "-"  "/" "|" "="))))

                            ;; __ ___ ____ _|_ __|____|_
                            ;; ~~ ~~~ ~=  ~-  ~@ ~> ~~>
                            ("_" (rx (+ (or "_" "|"))))
                            ("~" (rx (or ">" "=" "-" "@" "~>" (+ "~"))))

                            ;; {| [\ ]# (* }# $> ^=
                            "{|"  "[|"  "]#"  "(*"  "}#"  "$>"  "^="

                            ;; www wwww
                            ;; 0xFF 0x12
                            ;; Fl Tl fi fj fl ft
                            ("w" (rx (+ "w")))
                            ("0" (rx (and "x" (+ (in "A-F" "a-f" "0-9")))))
                            "Fl"  "Tl"  "fi"  "fj"  "fl"  "ft"))
  :hook
  (prog-mode-hook . ligature-mode))

;; ---------------------------------- ;;
;; lsp-mode
;; ---------------------------------- ;;

(external-package lsp-mode
  "Provides Language Server Protocol support and integration"
  :custom
  (lsp-eldoc-enable-hover nil "Do not print symbol eldoc information in the echo area")
  (lsp-signature-auto-activate nil "Do not display function signature docs in the echo area")
  (lsp-headerline-breadcrumb-enable nil "Do not display a file/function breadcrumb headerline")
  (lsp-modeline-code-actions-enable nil "Do not display suggested code actions in the mode line")
  :commands
  (lsp
   lsp-deferred)
  :hook
  (css-mode-hook . lsp-deferred)
  (gdscript-mode-hook . lsp-deferred)
  (js-mode-hook . lsp-deferred)
  (json-mode-hook . lsp-deferred)
  (rust-mode-hook . lsp-deferred)
  (typescript-mode-hook . lsp-deferred)
  (web-mode-hook . lsp-deferred))

;; ---------------------------------- ;;
;; lsp-ui
;; ---------------------------------- ;;

(external-package lsp-ui
  "Provides higher-level UI integration of LSP features for `lsp-mode'"
  :config
  (setq lsp-ui-doc-border (face-background 'lsp-ui-doc-background))
  :custom
  (lsp-ui-imenu-enable nil "Disable automatic activation of imenu side window")
  (lsp-ui-peek-always-show t "Always show peek popup, even with only one entry")
  (lsp-ui-doc-enable nil "Disable automatic activation of documentation elements")
  (lsp-ui-doc-max-width 50 "Limit documentation popup width to 50 columns")
  (lsp-ui-doc-max-height 10 "Limit documentation popup height to 10 lines")
  (lsp-ui-doc-header t "Show documentation header displaying the symbol name")
  (lsp-ui-doc-position 'top "Show documentation popups at the top of the window")
  (lsp-ui-doc-alignment 'window "Align documentation popups with the window, not the frame")
  (lsp-ui-doc-include-signature t "Show object signature/type in the documentation popup")
  (lsp-ui-doc-show-with-cursor t "Enable displaying documentation of symbols under the cursor")
  :bind
  (:map lsp-ui-mode-map
        ("M-," . lsp-ui-doc-mode)
        ("M-." . lsp-ui-peek-find-definitions)
        ("M-?" . lsp-ui-peek-find-references)))

;; ---------------------------------- ;;
;; magit
;; ---------------------------------- ;;

(external-package magit
  "Provides a visual interface to git"
  :custom
  (magit-status-margin '(t age magit-log-margin-width nil 18) "Display margin in status buffer")
  (git-commit-summary-max-length 50 "Highlight characters in commit summaries past column 50")
  (magit-display-buffer-function 'magit-display-buffer-same-window-except-diff-v1)
  :bind
  ("C-c g" . magit-status))

;; ---------------------------------- ;;
;; magit-todos
;; ---------------------------------- ;;

(external-package magit-todos
  "Shows a list of keyword-containing comments in the Magit status buffer"
  :hook
  (magit-mode-hook . magit-todos-mode))

;; ---------------------------------- ;;
;; marginalia
;; ---------------------------------- ;;

(external-package marginalia
  "Adds informative annotations to completion candidates in the minibuffer"
  :config
  (defun marginalia--file-owner (_) "")
  (defun marginalia--file-modes (_) "")
  (defun marginalia--symbol-class (_) "")
  (defun marginalia--buffer-status (_) "")
  (marginalia-mode))

;; ---------------------------------- ;;
;; mood-line
;; ---------------------------------- ;;

(external-package mood-line
  "Gives the mode-line a cleaner appearance"
  :config
  (mood-line-mode))

;; ---------------------------------- ;;
;; multiple-cursors
;; ---------------------------------- ;;

(external-package multiple-cursors
  "Provides the ability to summon additional cursors"
  :bind
  (("C->" . mc/mark-next-like-this)
   ("C-<" . mc/mark-previous-like-this)
   ("C-c C->" . mc/mark-all-like-this)
   ("C-c C-<" . mc/mark-all-like-this)))

;; ---------------------------------- ;;
;; neotree
;; ---------------------------------- ;;

(external-package neotree
  "Displays an interactive directory/file tree in a side window"
  :custom
  (neo-smart-open t "Jump to current file when focusing the neotree window")
  (neo-window-width 30 "Limit neotree window width to 30 columns")
  (neo-show-updir-line nil "Disable dipspying updir (..) line in the neotree window")
  (neo-confirm-create-file #'off-p "Skip confirmation when creating a new file from neotree")
  (neo-confirm-create-directory #'off-p "Skip confirmation when creating a new directory from neotree")
  :bind
  (("C-c t" . neotree)
   (:map neotree-mode-map
         ("C-c w" . (lambda () (interactive) nil))
         ("C-c q" . (lambda () (interactive) nil))
         ("C-c n" . (lambda () (interactive) nil)))))

;; ---------------------------------- ;;
;; package-lint
;; ---------------------------------- ;;

;; Load Package-Lint
(external-package package-lint
  "Provides a command for linting Emacs Lisp packages"
  :commands
  (package-lint-current-buffer))

;; ---------------------------------- ;;
;; page-break-lines
;; ---------------------------------- ;;

(external-package page-break-lines
  "Displays form feed characters as horizontal rules"
  :config
  (global-page-break-lines-mode))

;; ---------------------------------- ;;
;; rainbow-delimiters
;; ---------------------------------- ;;

(external-package rainbow-delimiters
  "Highlights matching delimiters with colors according to depth"
  :hook
  (prog-mode-hook . rainbow-delimiters-mode))

;; ---------------------------------- ;;
;; resize-window
;; ---------------------------------- ;;

(external-package resize-window
  "Provides an overlay that allows for quick window management inputs"
  :bind
  ("C-c w" . resize-window))

;; ---------------------------------- ;;
;; restclient
;; ---------------------------------- ;;

(external-package restclient
  "Allows for the execution of HTTP queries from plain-text query sheets"
  :config
  (defun restclient ()
    "Open the restclient buffer, (re)creating it if not present."
    (interactive)
    (pop-to-buffer (get-buffer-create "*restclient*") '((display-buffer-reuse-window display-buffer-same-window)))
    (unless (derived-mode-p 'restclient-mode)
      (restclient-mode)))
  :commands
  (restclient))

;; ---------------------------------- ;;
;; slime
;; ---------------------------------- ;;

(external-package slime
  "Provides an interactive programming environment for Common Lisp"
  :custom
  (inferior-lisp-program (executable-find "sbcl") "Set SBCL as the default Common Lisp implementation")
  :commands
  (slime))

;; ---------------------------------- ;;
;; slime-docker
;; ---------------------------------- ;;

(external-package slime-docker
  "Integrates SLIME with Lisps running in Docker containers"
  :custom
  (slime-docker-program "sbcl" "Set SBCL as the default Common Lisp implementation in containers")
  :commands
  (slime-docker))

;; ---------------------------------- ;;
;; string-inflection
;; ---------------------------------- ;;

(external-package string-inflection
  "Provides case conversion functions for symbols and strings"
  :commands
  (string-inflection-underscore-function
   string-inflection-pascal-case-function
   string-inflection-camelcase-function
   string-inflection-upcase-function
   string-inflection-kebab-case-function
   string-inflection-capital-underscore-function))

;; ---------------------------------- ;;
;; solaire-mode
;; ---------------------------------- ;;

(external-package solaire-mode
  "Recolors special buffers to distinguish them from regular buffers"
  :custom
  (solaire-mode-real-buffer-fn (lambda ()
                                 (and (buffer-name (buffer-base-buffer))
                                      (not (derived-mode-p 'neotree-mode))
                                      (not (string-match "\*Echo Area" (buffer-name (buffer-base-buffer)))))))
  :hook
  (server-after-make-frame-hook . solaire-global-mode)
  (emacs-startup-hook . solaire-global-mode))

;; ---------------------------------- ;;
;; undo-fu-session
;; ---------------------------------- ;;

(defconst user/undo-history-directory (locate-user-emacs-file "undo-history/") "Location of undo-fu-session history backups.")

(external-package undo-fu-session
  "Saves and recovers undo history of files between editing sessions"
  :config
  (global-undo-fu-session-mode)
  :custom
  (undo-fu-session-directory user/undo-history-directory "Set custom undo history storage location"))

;; ---------------------------------- ;;
;; vertico
;; ---------------------------------- ;;

(external-package vertico
  "Provides a vertical autocompletion UI in the minibuffer"
  :custom
  (vertico-cycle t "Enable wrap-around candidate cycling")
  (vertico-count 9 "Display a maximum of 9 candidates in the minibuffer")
  (vertico-resize nil "Keep the minibuffer at a static size")
  (vertico-group-format nil "Disable candidate group titles")
  (vertico-count-format nil "Disable candidate count display")
  :hook
  (emacs-startup-hook . vertico-mode)
  (vertico-mode-hook . vertico-mouse-mode)
  (rfn-eshadow-update-overlay-hook . vertico-directory-tidy)
  :bind
  (:map vertico-map
        ("\r" . vertico-directory-enter)
        ("\d" . vertico-directory-delete-char)
        ("\M-\d" . vertico-directory-delete-word)))

;; ---------------------------------- ;;
;; vundo
;; ---------------------------------- ;;

(external-package vundo
  "Visualizes undo history as a tree in an interactive buffer"
  :custom
  (vundo-glyph-alist vundo-unicode-symbols "Visualize undo history with pretty unicode symbols")
  :hook
  (vundo-mode-hook . (lambda ()
                       (setq-local global-hl-line-mode nil)))
  :bind
  ("C-x u" . vundo))

;; ---------------------------------- ;;
;; writeroom-mode
;; ---------------------------------- ;;

(external-package writeroom-mode
  "Centers the working text area in the active buffer"
  :custom
  (writeroom-width 140 "Set the working area width to 140 columns.")
  (writeroom-maximize-window nil "Do not maximize the active window.")
  (writeroom-mode-line t "Show the mode line while writeroom-mode is active.")
  (writeroom-header-line t "Show the header line while writeroom-mode is active.")
  (writeroom-global-effects nil "Disable all frame-wide writeroom effects.")
  (writeroom-fringes-outside-margins nil "Keep the fringes close to the text.")
  :bind
  (("C-c c" . writeroom-mode)
   (:map writeroom-mode-map
         ("C-<next>" . writeroom-decrease-width)
         ("C-<prior>" . writeroom-increase-width))))

;; ---------------------------------- ;;
;; yansippet
;; ---------------------------------- ;;

(defconst user/yasnippet-directory (locate-user-emacs-file "snippet/") "Location of user snippet files.")

(external-package yasnippet
  "Allows for the definition and automatic expansion of text templates"
  :custom
  (yas-also-indent-empty-lines t "Indent lines in expanded templates even if empty")
  (yas-also-auto-indent-first-line t "Always indent first line in expanded templates")
  (yas-snippet-dirs '(user/yasnippet-directory) "Set snippet files location")
  :hook
  (prog-mode-hook . yas-minor-mode)
  (text-mode-hook . yas-minor-mode)
  (fundamental-mode-hook . yas-minor-mode))

;; --------------------------------------------------------------------------
;;
;; End of init file
;;
;; --------------------------------------------------------------------------

(provide 'init)

;;; init.el ends here