;;; editor/whitespace/config.el -*- lexical-binding: t; -*-

(defvar +whitespace-guess-excluded-modes
  '(pascal-mode
    so-long-mode
    ;; Variable-width indentation is superior in elisp. Otherwise, `dtrt-indent'
    ;; and `editorconfig' would force fixed indentation on elisp.
    emacs-lisp-mode
    ;; See #5823: indent detection is slow and inconclusive in these major modes
    ;; so they are disabled there.
    coq-mode
    ;; Automatic indent detection in org files is meaningless. Not to mention, a
    ;; non-standard `tab-width' causes an error in org-mode.
    org-mode)
  "A list of major modes where indentation shouldn't be auto-detected.")

(defvar +whitespace-guess-in-projects nil
  "If non-nil, indentation settings will be guessed in project files.

This is off by default because, generally, indent-guessing is less useful for
projects, which have many options for configuring editors (editorconfig,
.dir-locals.el, global settings, etc). While single files have fewer options and
are more likely to use varied styles (and would be a pain to accommodate on a
per-file basis).")

(defvar-local +whitespace-guess-inhibit nil
  "A buffer-local flag that indicates whether `dtrt-indent' should try to guess
indentation settings or not. This should be set by editorconfig if it
successfully sets indent_style/indent_size.")


;;
;;; Packages

(use-package! whitespace
  :defer t
  :init
  (add-hook! 'after-change-major-mode-hook :append
    (defun +whitespace-highlight-incorrect-indentation-h ()
      "Highlight whitespace at odds with `indent-tabs-mode'.

That is, highlight tabs if `indent-tabs-mode' is `nil', and highlight spaces at
the beginnings of lines if `indent-tabs-mode' is `t'. The purpose is to make
incorrect indentation in the current buffer obvious to you, so it can be noticed
and corrected.

Does nothing if `whitespace-mode' or `global-whitespace-mode' is already active
or if the current buffer is read-only or not file-visiting."
      (unless (or (eq major-mode 'fundamental-mode)
                  (bound-and-true-p global-whitespace-mode)
                  (null buffer-file-name)
                  buffer-read-only)
        (require 'whitespace)
        (set (make-local-variable 'whitespace-style)
             (cl-union (if indent-tabs-mode
                           '(indentation)
                         '(tabs tab-mark))
                       (when whitespace-mode
                         (remq 'face whitespace-active-style))))
        (cl-pushnew 'face whitespace-style) ; must be first
        (whitespace-mode +1))))

  :config
  (setq whitespace-line-column nil
        whitespace-style
        '(face indentation tabs tab-mark spaces space-mark newline newline-mark
          trailing lines-tail)
        whitespace-display-mappings
        '((tab-mark ?\t [?› ?\t])
          (newline-mark ?\n [?¬ ?\n])
          (space-mark ?\  [?·] [?.])))

  ;; HACK: `whitespace-mode' inundates child frames with whitespace markers, so
  ;;   disable it to fix all that visual noise.
  (defun +whitespace--in-parent-frame-p () (null (frame-parameter nil 'parent-frame)))
  (add-function :before-while whitespace-enable-predicate #'+whitespace--in-parent-frame-p))


(use-package! dtrt-indent
  :when (modulep! +guess)
  ;; Automatic detection of indent settings
  :unless noninteractive
  ;; I'm not using `global-dtrt-indent-mode' because it has hard-coded and rigid
  ;; major mode checks, so I implement it in `+whitespace-guess-indentation-h'.
  :hook ((change-major-mode-after-body read-only-mode) . +whitespace-guess-indentation-h)
  :config
  (defun +whitespace-guess-indentation-h ()
    (unless (or (not after-init-time)
                doom-large-file-p
                +whitespace-guess-inhibit
                (eq major-mode 'fundamental-mode)
                (member (substring (buffer-name) 0 1) '(" " "*"))
                (apply #'derived-mode-p +whitespace-guess-excluded-modes)
                buffer-read-only
                (and (not +whitespace-guess-in-projects)
                     (doom-project-root)))
      ;; Don't display messages in the echo area, but still log them
      (let ((inhibit-message (not init-file-debug)))
        (dtrt-indent-mode +1))))

  ;; 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
  (add-to-list 'dtrt-indent-hook-generic-mapping-list '(t tab-width))

  ;; Add missing language support
  ;; REVIEW: PR these upstream.
  (add-to-list 'dtrt-indent-hook-mapping-list '(gdscript-mode default gdscript-indent-offset))
  (add-to-list 'dtrt-indent-hook-mapping-list '(graphviz-mode graphviz-dot-indent-width))
  (add-to-list 'dtrt-indent-hook-mapping-list '(janet-mode janet janet-indent))

  (defadvice! +whitespace--guess-smie-modes-a (fn &optional 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))
      (letf! ((defun symbol-config--guess (beg end)
                (funcall symbol-config--guess beg (min end 10000)))
              (defun smie-config-guess ()
                (condition-case e (funcall smie-config-guess)
                  (error (setq dtrt-indent-run-after-smie t)
                         (message "[WARNING] Indent detection: %s"
                                  (error-message-string e))
                         (message ""))))) ; warn silently
        (funcall fn arg)))))


;; a less intrusive `delete-trailing-whitespaces' on save
(use-package! ws-butler
  :when (modulep! +trim)
  :hook (doom-first-buffer . ws-butler-global-mode)
  :config
  (pushnew! ws-butler-global-exempt-modes
            'special-mode
            'comint-mode
            'term-mode
            'eshell-mode
            'diff-mode))
