;;; core-editor.el -*- lexical-binding: t; -*- (defvar doom-detect-indentation-excluded-modes '(fundamental-mode so-long-mode) "A list of major modes in which indentation should be automatically detected.") (defvar-local doom-inhibit-indent-detection nil "A buffer-local flag that indicates whether `dtrt-indent' should try to detect indentation settings or not. This should be set by editorconfig if it successfully sets indent_style/indent_size.") (defvar-local doom-large-file-p nil) (put 'doom-large-file-p 'permanent-local t) (defvar doom-large-file-size 1 "The threshold above which Doom enables emergency optimizations. This threshold is in MB. See `doom--optimize-for-large-files-a' for implementation details.") (defvar doom-large-file-excluded-modes '(so-long-mode special-mode archive-mode tar-mode jka-compr git-commit-mode image-mode doc-view-mode doc-view-mode-maybe ebrowse-tree-mode pdf-view-mode tags-table-mode) "Major modes that `doom-check-large-file-h' will ignore.") ;; ;;; File handling (defadvice! doom--optimize-for-large-files-a (orig-fn &rest args) "Set `doom-large-file-p' if the file is too large. Uses `doom-large-file-size' to determine when a file is too large. When `doom-large-file-p' is set, other plugins can detect this and reduce their runtime costs (or disable themselves) to ensure the buffer is as fast as possible." :around #'after-find-file (if (setq doom-large-file-p (and buffer-file-name (not doom-large-file-p) (file-readable-p buffer-file-name) (> (nth 7 (file-attributes buffer-file-name)) (* 1024 1024 doom-large-file-size)))) (prog1 (apply orig-fn args) (if (memq major-mode doom-large-file-excluded-modes) (setq doom-large-file-p nil) (so-long-minor-mode +1) (message "Large file detected! Cutting a few corners to improve performance..."))) (apply orig-fn args))) ;; Resolve symlinks when opening files, so that any operations are conducted ;; from the file's true directory (like `find-file'). (setq find-file-visit-truename t) ;; Disable the warning "X and Y are the same file". It's fine to ignore this ;; warning as it will redirect you to the existing buffer anyway. (setq find-file-suppress-same-file-warnings t) ;; Create missing directories when we open a file that doesn't exist under a ;; directory tree that may not exist. (add-hook! 'find-file-not-found-functions (defun doom-create-missing-directories-h () "Automatically create missing directories when creating new files." (let ((parent-directory (file-name-directory buffer-file-name))) (when (and (not (file-exists-p parent-directory)) (y-or-n-p (format "Directory `%s' does not exist! Create it?" parent-directory))) (make-directory parent-directory t))))) ;; Don't autosave files or create lock/history/backup files. The ;; editor doesn't need to hold our hands so much. We'll rely on git ;; and our own good fortune instead. Fingers crossed! (setq auto-save-default nil create-lockfiles nil make-backup-files nil ;; But have a place to store them in case we do use them... auto-save-list-file-name (concat doom-cache-dir "autosave") backup-directory-alist `(("." . ,(concat doom-cache-dir "backup/")))) (add-hook! 'after-save-hook (defun doom-guess-mode-h () "Guess mode when saving a file in `fundamental-mode'." (when (and (eq major-mode 'fundamental-mode) (buffer-file-name (buffer-base-buffer))) (set-auto-mode)))) ;; ;;; Formatting ;; Indentation (setq-default tab-width 4 tab-always-indent t indent-tabs-mode nil fill-column 80) ;; Word wrapping (setq-default word-wrap t truncate-lines t truncate-partial-width-windows nil) (setq sentence-end-double-space nil delete-trailing-lines nil require-final-newline t tabify-regexp "^\t* [ \t]+") ; for :retab ;; Favor hard-wrapping in text modes (add-hook 'text-mode-hook #'auto-fill-mode) ;; ;;; Clipboard / kill-ring ;; Eliminate duplicates in the kill ring. That is, if you kill the same thing ;; twice, you won't have to use M-y twice to get past it to older entries in the ;; kill ring. (setq kill-do-not-save-duplicates t) ;; (setq x-select-request-type '(UTF8_STRING COMPOUND_TEXT TEXT STRING)) ;; Save clipboard contents into kill-ring before replacing them (setq save-interprogram-paste-before-kill t) ;; ;;; Extra file extensions to support (push '("/LICENSE\\'" . text-mode) auto-mode-alist) ;; ;;; Built-in plugins (use-package! autorevert ;; revert buffers when their files/state have changed :hook (focus-in . doom-auto-revert-buffers-h) :hook (after-save . doom-auto-revert-buffers-h) :hook (doom-switch-buffer . doom-auto-revert-buffer-h) :hook (doom-switch-window . doom-auto-revert-buffer-h) :config (setq auto-revert-verbose t ; let us know when it happens auto-revert-use-notify nil auto-revert-stop-on-user-input nil) ;; Instead of using `auto-revert-mode' or `global-auto-revert-mode', we employ ;; lazy auto reverting on `focus-in-hook' and `doom-switch-buffer-hook'. ;; ;; This is because autorevert abuses the heck out of inotify handles which can ;; grind Emacs to a halt if you do expensive IO (outside of Emacs) on the ;; files you have open (like compression). We only really need to revert ;; changes when we switch to a buffer or when we focus the Emacs frame. (defun doom-auto-revert-buffer-h () "Auto revert current buffer, if necessary." (unless auto-revert-mode (let ((revert-without-query (list "."))) (auto-revert-handler)))) (defun doom-auto-revert-buffers-h () "Auto revert's stale buffers (that are visible)." (unless auto-revert-mode (dolist (buf (doom-visible-buffers)) (with-current-buffer buf (doom-auto-revert-buffer-h)))))) (use-package! recentf ;; Keep track of recently opened files :defer-incrementally easymenu tree-widget timer :after-call after-find-file :commands recentf-open-files :config (setq recentf-save-file (concat doom-cache-dir "recentf") recentf-auto-cleanup 'never recentf-max-menu-items 0 recentf-max-saved-items 200 recentf-exclude (list "\\.\\(?:gz\\|gif\\|svg\\|png\\|jpe?g\\)$" "^/tmp/" "^/ssh:" "\\.?ido\\.last$" "\\.revive$" "/TAGS$" "^/var/folders/.+$" ;; ignore private DOOM temp files (lambda (path) (ignore-errors (file-in-directory-p path doom-local-dir))))) (defun doom--recent-file-truename (file) (if (or (file-remote-p file nil t) (not (file-remote-p file))) (file-truename file) file)) (setq recentf-filename-handlers '(doom--recent-file-truename abbreviate-file-name)) (add-hook! '(doom-switch-window-hook write-file-functions) (defun doom--recentf-touch-buffer-h () "Bump file in recent file list when it is switched or written to." (when buffer-file-name (recentf-add-file buffer-file-name)) ;; Return nil for `write-file-functions' nil)) (add-hook! 'dired-mode-hook (defun doom--recentf-add-dired-directory-h () "Add dired directory to recentf file list." (recentf-add-file default-directory))) (when doom-interactive-mode (add-hook 'kill-emacs-hook #'recentf-cleanup) (quiet! (recentf-mode +1)))) (use-package! savehist ;; persist variables across sessions :defer-incrementally custom :after-call post-command-hook :config (setq savehist-file (concat doom-cache-dir "savehist") savehist-save-minibuffer-history t savehist-autosave-interval nil ; save on kill only savehist-additional-variables '(kill-ring search-ring regexp-search-ring)) (savehist-mode +1) (add-hook! 'kill-emacs-hook (defun doom-unpropertize-kill-ring-h () "Remove text properties from `kill-ring' for a smaller savehist file." (setq kill-ring (cl-loop for item in kill-ring if (stringp item) collect (substring-no-properties item) else if item collect it))))) (use-package! saveplace ;; persistent point location in buffers :after-call after-find-file dired-initial-position-hook :config (setq save-place-file (concat doom-cache-dir "saveplace") save-place-limit 100) (defadvice! doom--recenter-on-load-saveplace-a (&rest _) "Recenter on cursor when loading a saved place." :after-while #'save-place-find-file-hook (if buffer-file-name (ignore-errors (recenter)))) (defadvice! doom--dont-prettify-saveplace-cache-a (orig-fn) "`save-place-alist-to-file' uses `pp' to prettify the contents of its cache. `pp' can be expensive for longer lists, and there's no reason to prettify cache files, so we replace calls to `pp' with the much faster `prin1'." :around #'save-place-alist-to-file (cl-letf (((symbol-function #'pp) (symbol-function #'prin1))) (funcall orig-fn))) (save-place-mode +1)) (use-package! server :when (display-graphic-p) :after-call pre-command-hook after-find-file focus-out-hook :init (when-let (name (getenv "EMACS_SERVER_NAME")) (setq server-name name)) :config (unless (server-running-p) (server-start))) ;; ;;; Packages (use-package! better-jumper :after-call pre-command-hook :preface ;; REVIEW Suppress byte-compiler warning spawning a *Compile-Log* buffer at ;; startup. This can be removed once gilbertw1/better-jumper#2 is merged. (defvar better-jumper-local-mode nil) :init (global-set-key [remap evil-jump-forward] #'better-jumper-jump-forward) (global-set-key [remap evil-jump-backward] #'better-jumper-jump-backward) (global-set-key [remap xref-pop-marker-stack] #'better-jumper-jump-backward) :config (better-jumper-mode +1) (add-hook 'better-jumper-post-jump-hook #'recenter) (defadvice! doom-set-jump-a (orig-fn &rest args) "Set a jump point and ensure ORIG-FN doesn't set any new jump points." (better-jumper-set-jump (if (markerp (car args)) (car args))) (let ((evil--jumps-jumping t) (better-jumper--jumping t)) (apply orig-fn args))) (defadvice! doom-set-jump-maybe-a (orig-fn &rest args) "Set a jump point if ORIG-FN returns non-nil." (let ((origin (point-marker)) (result (let* ((evil--jumps-jumping t) (better-jumper--jumping t)) (apply orig-fn args)))) (unless result (with-current-buffer (marker-buffer origin) (better-jumper-set-jump (if (markerp (car args)) (car args) origin)))) result)) (defun doom-set-jump-h () "Run `better-jumper-set-jump' but return nil, for short-circuiting hooks." (better-jumper-set-jump) nil) ;; Creates a jump point before killing a buffer. This allows you to undo ;; killing a buffer easily (only works with file buffers though; it's not ;; possible to resurrect special buffers). (advice-add #'kill-current-buffer :around #'doom-set-jump-a) ;; Create a jump point before jumping with imenu. (advice-add #'imenu :around #'doom-set-jump-a)) (use-package! dtrt-indent ;; Automatic detection of indent settings :when doom-interactive-mode :defer t :init (add-hook! '(change-major-mode-after-body-hook read-only-mode-hook) (defun doom-detect-indentation-h () (unless (or (not after-init-time) doom-inhibit-indent-detection doom-large-file-p (memq major-mode doom-detect-indentation-excluded-modes) (member (substring (buffer-name) 0 1) '(" " "*"))) ;; Don't display messages in the echo area, but still log them (let ((inhibit-message (not doom-debug-mode))) (dtrt-indent-mode +1))))) :config ;; Enable dtrt-indent even in smie modes so that it can update `tab-width', ;; `standard-indent' and `evil-shift-width' there as well. (setq dtrt-indent-run-after-smie t) ;; Reduced from the default of 5000 for slightly faster analysis (setq dtrt-indent-max-lines 2000) ;; always keep tab-width up-to-date (push '(t tab-width) dtrt-indent-hook-generic-mapping-list) (defvar dtrt-indent-run-after-smie) (defadvice! doom--fix-broken-smie-modes-a (orig-fn arg) "Some smie modes throw errors when trying to guess their indentation, like `nim-mode'. This prevents them from leaving Emacs in a broken state." :around #'dtrt-indent-mode (let ((dtrt-indent-run-after-smie dtrt-indent-run-after-smie)) (cl-letf* ((old-smie-config-guess (symbol-function 'smie-config-guess)) ((symbol-function 'smie-config-guess) (lambda () (condition-case e (funcall old-smie-config-guess) (error (setq dtrt-indent-run-after-smie t) (message "[WARNING] Indent detection: %s" (error-message-string e)) (message "")))))) ; warn silently (funcall orig-fn arg))))) (use-package! helpful ;; a better *help* buffer :commands helpful--read-symbol :init (define-key! [remap describe-function] #'helpful-callable [remap describe-command] #'helpful-command [remap describe-variable] #'helpful-variable [remap describe-key] #'helpful-key [remap describe-symbol] #'doom/describe-symbol) (defun doom-use-helpful-a (orig-fn &rest args) "Force ORIG-FN to use helpful instead of the old describe-* commands." (cl-letf (((symbol-function #'describe-function) #'helpful-function) ((symbol-function #'describe-variable) #'helpful-variable)) (apply orig-fn args))) (after! apropos ;; patch apropos buttons to call helpful instead of help (dolist (fun-bt '(apropos-function apropos-macro apropos-command)) (button-type-put fun-bt 'action (lambda (button) (helpful-callable (button-get button 'apropos-symbol))))) (dolist (var-bt '(apropos-variable apropos-user-option)) (button-type-put var-bt 'action (lambda (button) (helpful-variable (button-get button 'apropos-symbol))))))) ;;;###package imenu (add-hook 'imenu-after-jump-hook #'recenter) (use-package! smartparens ;; Auto-close delimiters and blocks as you type. It's more powerful than that, ;; but that is all Doom uses it for. :after-call doom-switch-buffer-hook after-find-file :commands sp-pair sp-local-pair sp-with-modes sp-point-in-comment sp-point-in-string :config ;; Load default smartparens rules for various languages (require 'smartparens-config) ;; Overlays are too distracting and not terribly helpful. show-parens does ;; this for us already, so... (setq sp-highlight-pair-overlay nil sp-highlight-wrap-overlay nil sp-highlight-wrap-tag-overlay nil) ;; But if someone does want overlays enabled, evil users will be stricken with ;; an off-by-one issue where smartparens assumes you're outside the pair when ;; you're really at the last character in insert mode. We must correct this ;; vile injustice. (setq sp-show-pair-from-inside t) ;; ...and stay highlighted until we've truly escaped the pair! (setq sp-cancel-autoskip-on-backward-movement nil) ;; The default is 100, because smartparen's scans are relatively expensive ;; (especially with large pair lists for somoe modes), we halve it, as a ;; better compromise between performance and accuracy. (setq sp-max-prefix-length 50) ;; This speeds up smartparens. No pair has any business being longer than 4 ;; characters; if they must, the modes that need it set it buffer-locally. (setq sp-max-pair-length 4) ;; This isn't always smart enough to determine when we're in a string or not. ;; See https://github.com/Fuco1/smartparens/issues/783. (setq sp-escape-quotes-after-insert nil) ;; Silence some harmless but annoying echo-area spam (dolist (key '(:unmatched-expression :no-matching-tag)) (setf (cdr (assq key sp-message-alist)) nil)) (add-hook! 'minibuffer-setup-hook (defun doom-init-smartparens-in-minibuffer-maybe-h () "Enable `smartparens-mode' in the minibuffer, during `eval-expression' or `evil-ex'." (when (memq this-command '(eval-expression evil-ex)) (smartparens-mode)))) ;; You're likely writing lisp in the minibuffer, therefore, disable these ;; quote pairs, which lisps doesn't use for strings: (sp-local-pair 'minibuffer-inactive-mode "'" nil :actions nil) (sp-local-pair 'minibuffer-inactive-mode "`" nil :actions nil) ;; Smartparens breaks evil-mode's replace state (defvar doom-buffer-smartparens-mode nil) (add-hook! 'evil-replace-state-exit-hook (defun doom-enable-smartparens-mode-maybe-h () (when doom-buffer-smartparens-mode (turn-on-smartparens-mode) (kill-local-variable 'doom-buffer-smartparens-mode)))) (add-hook! 'evil-replace-state-entry-hook (defun doom-disable-smartparens-mode-maybe-h () (when smartparens-mode (setq-local doom-buffer-smartparens-mode t) (turn-off-smartparens-mode)))) (smartparens-global-mode +1)) (use-package! so-long :after-call after-find-file :config (global-so-long-mode +1) (delq! 'font-lock-mode so-long-minor-modes) (delq! 'display-line-numbers-mode so-long-minor-modes) (appendq! so-long-minor-modes '(flycheck-mode eldoc-mode smartparens-mode highlight-numbers-mode better-jumper-local-mode ws-butler-mode auto-composition-mode undo-tree-mode highlight-indent-guides-mode hl-fill-column-mode))) (use-package! undo-tree ;; Branching & persistent undo :after-call doom-switch-buffer-hook after-find-file :config (setq undo-tree-auto-save-history t ;; Increase undo-limits by a factor of ten to avoid emacs prematurely ;; truncating the undo history and corrupting the tree. See ;; https://github.com/syl20bnr/spacemacs/issues/12110 undo-limit 800000 undo-strong-limit 12000000 undo-outer-limit 120000000 undo-tree-history-directory-alist `(("." . ,(concat doom-cache-dir "undo-tree-hist/")))) ;; Compress undo-tree history files with zstd, if available. File size isn't ;; the (only) concern here: the file IO barrier is slow for Emacs to cross; ;; reading a tiny file and piping it in-memory through zstd is *slightly* ;; faster than Emacs reading the entire undo-tree file from the get go (on ;; SSDs). Whether or not that's true in practice, we still enjoy zstd's ~80% ;; file savings (these files add up over time and zstd is so incredibly fast). (when (executable-find "zstd") (defadvice! doom--undo-tree-make-history-save-file-name-a (file) :filter-return #'undo-tree-make-history-save-file-name (concat file ".zst"))) ;; Strip text properties from undo-tree data to stave off bloat. File size ;; isn't the concern here; undo cache files bloat easily, which can cause ;; freezing, crashes, GC-induced stuttering or delays when opening files. (defadvice! doom--undo-tree-strip-text-properties-a (&rest _) :before #'undo-list-transfer-to-tree (dolist (item buffer-undo-list) (and (consp item) (stringp (car item)) (setcar item (substring-no-properties (car item)))))) (global-undo-tree-mode +1)) (use-package! ws-butler ;; a less intrusive `delete-trailing-whitespaces' on save :after-call after-find-file :config (appendq! ws-butler-global-exempt-modes '(special-mode comint-mode term-mode eshell-mode)) (ws-butler-global-mode)) (provide 'core-editor) ;;; core-editor.el ends here