;;; +emacs.el --- measured defaults for Emacs -*- lexical-binding: t -*-

;;; Commentary:

;; I find myself copy-pasting a lot of "boilerplate" type code when
;; bankrupting my Emacs config and starting afresh.  Instead of doing
;; that, I'm putting it here, where it'll be easier to include in my
;; config.

;; Of course, some might say I could just ... stop bankrupting my
;; Emacs.  But like, why would I want to?

;; Other notable packages include
;; - https://git.sr.ht/~technomancy/better-defaults/
;; - https://github.com/susam/emfy

;;; Code:

(require 'early-init (locate-user-emacs-file "early-init.el"))

(defun +set-major-mode-from-buffer-name (&optional buf)
  "Set the major mode for BUF from the buffer's name.
Do this only if the buffer is not visiting a file."
  (unless buffer-file-name
    (let ((buffer-file-name (buffer-name buf)))
      (set-auto-mode))))


;;; General settings

(setq-default
 apropos-do-all t
 async-shell-command-buffer 'new-buffer
 async-shell-command-display-buffer nil
 auto-hscroll-mode 'current-line
 auto-revert-verbose nil
 auto-save-file-name-transforms `((".*" ,(.etc "auto-save/" t) t))
 auto-save-interval 60
 auto-save-list-file-prefix (.etc "auto-save/.saves-" t)
 auto-save-timeout 60
 auto-save-visited-interval 60
 auto-window-vscroll nil
 backup-by-copying t
 backup-directory-alist `((".*" . ,(.etc "backup/" t)))
 blink-cursor-blinks 1
 comp-deferred-compilation nil
 completion-category-defaults nil
 completion-category-overrides '((file (styles . (partial-completion))))
 completion-ignore-case t
 completion-styles '(substring partial-completion)
 create-lockfiles nil
 cursor-in-non-selected-windows 'hollow
 cursor-type 'bar
 custom-file (.etc "custom.el")
 delete-old-versions t
 echo-keystrokes 0.1
 ediff-window-setup-function 'ediff-setup-windows-plain
 eldoc-echo-area-use-multiline-p nil
 eldoc-idle-delay 0.1
 enable-recursive-minibuffers t
 executable-prefix-env t
 fast-but-imprecise-scrolling t
 file-name-shadow-properties '(invisible t intangible t)
 fill-column 80
 find-file-visit-truename t
 frame-resize-pixelwise t
 global-auto-revert-non-file-buffers t
 global-mark-ring-max 100
 hscroll-margin 1
 hscroll-step 1
 imenu-auto-rescan t
 image-use-external-converter (or (executable-find "convert")
                                  (executable-find "gm")
                                  (executable-find "ffmpeg"))
 indent-tabs-mode nil
 indicate-buffer-boundaries 'left
 indicate-empty-lines nil
 inhibit-startup-screen t
 initial-buffer-choice t
 kept-new-versions 6
 kept-old-versions 2
 kill-do-not-save-duplicates t
 kill-read-only-ok t
 kill-ring-max 500
 kmacro-ring-max 20
 load-prefer-newer noninteractive
 major-mode '+set-major-mode-from-buffer-name
 mark-ring-max 50
 minibuffer-eldef-shorten-default t
 minibuffer-prompt-properties (list 'read-only t
                                    'cursor-intangible t
                                    'face 'minibuffer-prompt)
 mode-require-final-newline 'visit-save
 mouse-drag-copy-region t
 mouse-wheel-progressive-speed nil
 mouse-yank-at-point t
 native-comp-async-report-warnings-errors 'silent
 native-comp-deferred-compilation nil
 read-answer-short t
 read-buffer-completion-ignore-case t
 ;; read-extended-command-predicate (when (fboundp
 ;;                                        'command-completion-default-include-p)
 ;;                                   'command-completion-default-include-p)
 recenter-positions '(top middle bottom)
 regexp-search-ring-max 100
 regexp-search-ring-max 200
 save-interprogram-paste-before-kill t
 scroll-conservatively 101
 scroll-down-aggressively 0.01
 scroll-margin 2
 scroll-preserve-screen-position 1
 scroll-step 1
 scroll-up-aggressively 0.01
 search-ring-max 200
 search-ring-max 200
 sentence-end-double-space t
 set-mark-command-repeat-pop t
 show-paren-delay 0
 show-paren-style 'mixed
 show-paren-when-point-in-periphery t
 show-paren-when-point-inside-paren t
 ;;show-trailing-whitespace t
 tab-bar-show 1
 tab-width 4
 tramp-backup-directory-alist backup-directory-alist
 undo-limit 100000000                   ; 10 MB
 use-dialog-box nil
 use-file-dialog nil
 use-short-answers t
 vc-follow-symlinks t
 vc-make-backup-files t
 version-control t
 view-read-only t
 visible-bell nil
 window-resize-pixelwise t
 x-select-enable-clipboard t
 x-select-enable-primary t
 yank-pop-change-selection t
 )

;; Programming language offsets.
;; Set these after the initial block so I can use `tab-width'
(setq-default
 c-basic-offset tab-width)

;; Emacs 28 ships with an option, `use-short-answers', that makes this form
;; obsolete, but I still use 27 at work.
(when (version< emacs-version "28")
  (fset 'yes-or-no-p 'y-or-n-p))


;;; Encodings

;; Allegedly, this is the only one you need...
(set-language-environment "UTF-8")
;; But I still set all of these, for fun.
(setq-default locale-coding-system 'utf-8-unix
              coding-system-for-read 'utf-8-unix
              coding-system-for-write 'utf-8-unix
              buffer-file-coding-system 'utf-8-unix
              default-process-coding-system '(utf-8-unix . utf-8-unix)
              x-select-request-type '(UTF8_STRING
                                      COMPOUND_TEXT
                                      TEXT
                                      STRING))

(set-charset-priority 'unicode)
(prefer-coding-system 'utf-8-unix)
(set-default-coding-systems 'utf-8-unix)
(set-terminal-coding-system 'utf-8-unix)
(set-keyboard-coding-system 'utf-8-unix)

(pcase system-type
  ((or 'ms-dos 'windows-nt)
   (set-clipboard-coding-system 'utf-16-le)
   (set-selection-coding-system 'utf-16-le))
  (_
   (set-selection-coding-system 'utf-8)
   (set-clipboard-coding-system 'utf-8)))


;;; Modes

(dolist (enable-mode '(global-auto-revert-mode
                       blink-cursor-mode
                       electric-pair-mode
                       show-paren-mode
                       global-so-long-mode
                       minibuffer-depth-indicate-mode
                       file-name-shadow-mode
                       minibuffer-electric-default-mode
                       delete-selection-mode
                       column-number-mode))
  (when (fboundp enable-mode)
    (funcall enable-mode +1)))

(dolist (disable-mode '(tooltip-mode
                        tool-bar-mode
                        menu-bar-mode
                        scroll-bar-mode
                        horizontal-scroll-bar-mode))
  (when (fboundp disable-mode)
    (funcall disable-mode -1)))


;;; Hooks

(add-hook 'after-save-hook 'executable-make-buffer-file-executable-if-script-p)
(add-hook 'minibuffer-setup-hook 'cursor-intangible-mode)


;;; Better-default functions ...

(defun +cycle-spacing (&optional n preserve-nl-back mode)
  "Negate N argument on `cycle-spacing'.
That is, with a positive N, deletes newlines as well, leaving -N
spaces.  If N is negative, it will not delete newlines and leave
N spaces.  See docstring of `cycle-spacing' for the meaning of
PRESERVE-NL-BACK and MODE."
  (interactive "*p")
  (cycle-spacing (- n) preserve-nl-back mode))

(defun +save-buffers-quit (&optional arg)
  "Silently save each buffer, then kill the current connection.
If the current frame has no client, kill Emacs itself using
`save-buffers-kill-emacs' after confirming with the user.

With prefix ARG, silently save all file-visiting buffers, then
kill without asking."
  (interactive "P")
  (save-some-buffers t)
  (if (and (not (frame-parameter nil 'client))
           (and (not arg)))
      (when (yes-or-no-p "Sure you want to quit? ")
        (save-buffers-kill-emacs))
    (delete-frame nil :force)))

(defun +kill-word-backward-or-region (&optional arg backward-kill-word-fn)
  "Kill active region or ARG words backward.
BACKWARD-KILL-WORD-FN is the function to call to kill a word
backward.  It defaults to `backward-kill-word'."
  (interactive "P")
  (call-interactively (if (region-active-p)
                          #'kill-region
                        (or backward-kill-word-fn #'backward-kill-word))))

(defun +backward-kill-word-wrapper (fn &optional arg)
  "Kill backward using FN until the beginning of a word, smartly.
If point is on at the beginning of a line, kill the previous new
line.  If the only thing before point on the current line is
whitespace, kill that whitespace.

With argument ARG: if ARG is a number, just call FN
ARG times.  Otherwise, just call FN."
  ;; I want this to be a wrapper so that I can call other word-killing functions
  ;; with it.  It's *NOT* advice because those functions probably use
  ;; `backward-kill-word' under the hood (looking at you, paredit), so advice
  ;; will make things weird.
  (if (null arg)
      (cond
       ((looking-back "^" 1)
        (let ((delete-active-region nil))
          (delete-backward-char 1)))
       ((looking-back "^[  ]*")
        (delete-horizontal-space :backward-only))
       (t (call-interactively fn)))
    (funcall fn (if (listp arg) 1 arg))))

(defun +backward-kill-word (&optional arg)
  "Kill word backward using `backward-kill-word'.
ARG is passed to `backward-kill-word'."
  (interactive "P")
  (+backward-kill-word-wrapper #'backward-kill-word arg))

;; ... and advice

;; Indent the region after a yank.
(defun +yank@indent (&rest _)
  "Indent the current region."
  (indent-region (min (point) (mark)) (max (point) (mark))))
(advice-add #'yank :after #'+yank@indent)
(advice-add #'yank-pop :after #'+yank@indent)


;;; Bindings

;; I need to place these bindings under `+key-mode-map' so that they aren't
;; shadowed by other maps.  There might be a better way to do this.
(require '+key)

(dolist (binding '(("C-x C-c" . +save-buffers-quit)
                   ("M-SPC" . +cycle-spacing)
                   ("M-/" . hippie-expand)
                   ("M-=" . count-words)
                   ("C-x C-b" . ibuffer)
                   ("C-s" . isearch-forward-regexp)
                   ("C-r" . isearch-backward-regexp)
                   ("C-M-s" . isearch-forward)
                   ("C-M-r" . isearch-backward)))
  (define-key +key-mode-map (kbd (car binding)) (cdr binding)))


;;; Required libraries

(when (require 'uniquify nil :noerror)
  (setq-default uniquify-buffer-name-style 'forward
                uniquify-separator path-separator
                uniquify-after-kill-buffer-p t
                uniquify-ignore-buffers-re "^\\*"))

(when (require 'goto-addr)
  (if (fboundp 'global-goto-address-mode)
      (global-goto-address-mode +1)
    (add-hook 'after-change-major-mode-hook 'goto-address-mode)))

(when (require 'recentf nil :noerror)
  (setq-default recentf-save-file (.etc "recentf.el")
                recentf-max-menu-items 100
                recentf-max-saved-items nil
                recentf-auto-cleanup 'mode)
  (add-to-list 'recentf-exclude .etc)
  (recentf-mode +1))

(when (require 'savehist nil :noerror)
  (setq-default history-length t
                history-delete-duplicates t
                history-autosave-interval 60
                savehist-file (.etc "savehist.el"))
  (dolist (var '(extended-command-history
                 global-mark-ring
                 kill-ring
                 regexp-search-ring
                 search-ring
                 mark-ring))
    (add-to-list 'savehist-additional-variables var))
  (savehist-mode +1))

(when (require 'saveplace nil :noerror)
  (setq-default save-place-file (.etc "places.el")
                save-place-forget-unreadable-files (eq system-type 'gnu/linux))
  (save-place-mode +1))

;; (when (require 'tramp)
;;   ;; thanks Irreal! https://irreal.org/blog/?p=895
;;   (add-to-list 'tramp-default-proxies-alist
;;                '(nil "\\`root\\'" "/ssh:%h:"))
;;   (add-to-list 'tramp-default-proxies-alist
;;                '((regexp-quote (system-name)) nil nil)))


;;; Newer features
;; These aren't in older version of Emacs, but they're so nice.

(when (fboundp 'repeat-mode)
  (setq-default repeat-exit-key "g"
                repeat-exit-timeout 5)
  (repeat-mode +1))

(when (fboundp 'pixel-scroll-precision-mode)
  (pixel-scroll-precision-mode +1))

(provide '+emacs)
;;; +emacs.el ends here
