;;; tools/magit/config.el -*- lexical-binding: t; -*-

(defvar +magit-open-windows-in-direction 'right
  "What direction to open new windows from the status buffer.
For example, diffs and log buffers. Accepts `left', `right', `up', and `down'.")

(defvar +magit-fringe-size '(13 . 1)
  "Size of the fringe in magit-mode buffers.

Can be an integer or a cons cell whose CAR and CDR are integer widths for the
left and right fringe.

Only has an effect in GUI Emacs.")

(defvar +magit-auto-revert 'local
  "If non-nil, revert associated buffers after Git operations with side-effects.

These buffers are auto-reverted immediately if they're visible or reverted next
time they're switched to. This is intended to be a much more efficient
replacement for `magit-auto-revert-mode' and `global-auto-revert-mode', and
should not be used together with them! Set this to `nil' if you plan to use the
above.

Accepts one of three values OR a predicate function:

t
  Revert any associated buffers.
local
  Same as `t', except remote (TRAMP) buffers are ignored.
nil
  Don't do any auto-reverting at all.
FUNCTION
  If given a function, it will be passed a buffer associated with the current
  Magit session and must return non-nil to signal this is buffer is safe to
  revert (now or later, when switched to).")


;;
;;; Packages

(use-package! magit
  :commands magit-file-delete
  :defer-incrementally (dash f s with-editor git-commit package eieio transient)
  :init
  (setq magit-auto-revert-mode nil)  ; we do this ourselves further down
  ;; Must be set early to prevent ~/.config/emacs/transient from being created
  (setq transient-levels-file  (concat doom-data-dir "transient/levels")
        transient-values-file  (concat doom-data-dir "transient/values")
        transient-history-file (concat doom-data-dir "transient/history"))
  :config
  (set-debug-variable! 'magit-refresh-verbose)

  (setq transient-default-level 5
        magit-diff-refine-hunk t ; show granular diffs in selected hunk
        ;; Don't autosave repo buffers. This is too magical, and saving can
        ;; trigger a bunch of unwanted side-effects, like save hooks and
        ;; formatters. Trust the user to know what they're doing.
        magit-save-repository-buffers nil
        ;; Don't display parent/related refs in commit buffers; they are rarely
        ;; helpful and only add to runtime costs.
        magit-revision-insert-related-refs nil
        ;; If two projects have the same project name (e.g. A/src and B/src will
        ;; both resolve to the name "src"), Magit will treat them as the same
        ;; project and destructively hijack each other's magit buffers. This is
        ;; especially problematic if you use workspaces and have magit open in
        ;; each, and the two projects happen to have the same name! By unsetting
        ;; `magit-uniquify-buffer-names', magit uses the project's full path as
        ;; its name, preventing such naming collisions.
        magit-uniquify-buffer-names nil)

  ;; Turn ref links into clickable buttons.
  (add-hook 'magit-process-mode-hook #'goto-address-mode)

  ;; Since the project likely now contains new files, purge the projectile cache
  ;; so `projectile-find-file' et all don't produce an stale file list.
  (add-hook! 'magit-refresh-buffer-hook
    (defun +magit-invalidate-projectile-cache-h ()
      ;; Only invalidate the hot cache and nothing else (everything else is
      ;; expensive busy work, and we don't want to slow down magit's
      ;; refreshing).
      (let (projectile-require-project-root
            projectile-enable-caching
            projectile-verbose)
        (letf! ((#'recentf-cleanup #'ignore))
          (projectile-invalidate-cache nil)))))
  ;; Use a more efficient strategy to auto-revert buffers whose git state has
  ;; changed: refresh the visible buffers immediately...
  (add-hook 'magit-post-refresh-hook #'+magit-mark-stale-buffers-h)
  ;; ...then refresh the rest only when we switch to them or refocus the active
  ;; frame, not all at once.
  (add-hook 'doom-switch-buffer-hook #'+magit-revert-buffer-maybe-h)
  (add-hook 'doom-switch-frame-hook #'+magit-mark-stale-buffers-h)

  ;; Prevent sudden window position resets when staging/unstaging/discarding/etc
  ;; hunks in `magit-status-mode' buffers. It's disorienting, especially on
  ;; larger projects.
  (defvar +magit--refreshed-buffer nil)
  (add-hook! 'magit-pre-refresh-hook
    (defun +magit--set-window-state-h ()
      (setq-local +magit--refreshed-buffer
                  (list (current-buffer) (point) (window-start)))))
  (add-hook! 'magit-post-refresh-hook
    (defun +magit--restore-window-state-h ()
      (cl-destructuring-bind (&optional buf pt beg) +magit--refreshed-buffer
        (when (and buf (eq (current-buffer) buf))
          (goto-char pt)
          (set-window-start nil beg t)
          (kill-local-variable '+magit--refreshed-buffer)))))

  ;; Magit uses `magit-display-buffer-traditional' to display windows, by
  ;; default, which is a little primitive. `+magit-display-buffer' marries
  ;; `magit-display-buffer-fullcolumn-most-v1' with
  ;; `magit-display-buffer-same-window-except-diff-v1', except:
  ;;
  ;; 1. Magit sub-buffers (like `magit-log') that aren't spawned from a status
  ;;    screen are opened as popups.
  ;; 2. The status screen isn't buried when viewing diffs or logs from the
  ;;    status screen.
  (setq magit-display-buffer-function #'+magit-display-buffer-fn
        magit-bury-buffer-function #'magit-mode-quit-window)
  ;; Pop up transient windows at the bottom of the window where it was invoked.
  ;; This is more ergonomic for users with large displays or many splits.
  (setq transient-display-buffer-action
        '(display-buffer-below-selected
          (dedicated . t)
          (inhibit-same-window . t))
        transient-show-during-minibuffer-read t)

  (set-popup-rule! "^\\(?:\\*magit\\|magit:\\| \\*transient\\*\\)" :ignore t)

  ;; The mode-line isn't useful in these popups and take up valuable screen
  ;; estate, so free it up.
  (add-hook 'magit-popup-mode-hook #'hide-mode-line-mode)

  ;; Add additional switches that seem common enough
  (transient-append-suffix 'magit-fetch "-p"
    '("-t" "Fetch all tags" ("-t" "--tags")))
  (transient-append-suffix 'magit-pull "-r"
    '("-a" "Autostash" "--autostash"))

  ;; so magit buffers can be switched to (except for process buffers)
  (add-hook! 'doom-real-buffer-functions
    (defun +magit-buffer-p (buf)
      (with-current-buffer buf
        (and (derived-mode-p 'magit-mode)
             (not (eq major-mode 'magit-process-mode))))))

  ;; Clean up after magit by killing leftover magit buffers and reverting
  ;; affected buffers (or at least marking them as need-to-be-reverted).
  (define-key magit-mode-map "q" #'+magit/quit)
  (define-key magit-mode-map "Q" #'+magit/quit-all)

  ;; Close transient with ESC
  (define-key transient-map [escape] #'transient-quit-one)

  (add-hook! 'magit-section-mode-hook
    (add-hook! 'window-configuration-change-hook :local
      (defun +magit-enlargen-fringe-h ()
        "Make fringe larger in magit."
        (and (display-graphic-p)
             (derived-mode-p 'magit-section-mode)
             +magit-fringe-size
             (let ((left  (or (car-safe +magit-fringe-size) +magit-fringe-size))
                   (right (or (cdr-safe +magit-fringe-size) +magit-fringe-size)))
               (set-window-fringes nil left right))))))

  (add-hook! 'magit-diff-visit-file-hook
    (defun +magit-reveal-point-if-invisible-h ()
      "Reveal the point if in an invisible region."
      (if (derived-mode-p 'org-mode)
          (org-reveal '(4))
        (require 'reveal)
        (reveal-post-command)))))


(use-package! forge
  :when (modulep! +forge)
  ;; We defer loading even further because forge's dependencies will try to
  ;; compile emacsql, which is a slow and blocking operation.
  :after-call magit-status
  :commands forge-create-pullreq forge-create-issue
  :preface
  (setq forge-database-file (concat doom-data-dir "forge/forge-database.sqlite"))
  (setq forge-add-default-bindings (not (modulep! :editor evil +everywhere)))
  :init
  (after! ghub-graphql
    ;; Killing recreating the status buffer prevents progress updates from being
    ;; relayed through the modeline. Use `message' instead.
    (setq ghub-graphql-message-progress t))
  :config
  ;; All forge list modes are derived from `forge-topic-list-mode'
  (map! :map forge-topic-list-mode-map :n "q" #'kill-current-buffer)
  (when (not forge-add-default-bindings)
    (map! :map magit-mode-map [remap magit-browse-thing] #'forge-browse
          :map magit-remote-section-map [remap magit-browse-thing] #'forge-browse-remote
          :map magit-branch-section-map [remap magit-browse-thing] #'forge-browse-branch))
  (set-popup-rule! "^\\*?[0-9]+:\\(?:new-\\|[0-9]+$\\)" :size 0.45 :modeline t :ttl 0 :quit nil)
  (set-popup-rule! "^\\*\\(?:[^/]+/[^ ]+ #[0-9]+\\*$\\|Issues\\|Pull-Requests\\|forge\\)" :ignore t))


(use-package! code-review
  :when (modulep! +forge)
  :after magit
  :init
  ;; TODO This needs to either a) be cleaned up or better b) better map things
  ;; to fit
  (after! evil-collection-magit
    (dolist (binding evil-collection-magit-mode-map-bindings)
      (pcase-let* ((`(,states _ ,evil-binding ,fn) binding))
        (dolist (state states)
          (evil-collection-define-key state 'code-review-mode-map evil-binding fn))))
    (evil-set-initial-state 'code-review-mode evil-default-state))
  (setq code-review-db-database-file (concat doom-data-dir "code-review/code-review-db-file.sqlite")
        code-review-log-file (concat doom-data-dir "code-review/code-review-error.log")
        code-review-download-dir (concat doom-data-dir "code-review/"))
  :config
  (transient-append-suffix 'magit-merge "d"
    '("y" "Review pull request" +magit/start-code-review))
  (after! forge
    (transient-append-suffix 'forge-dispatch "c u"
      '("c r" "Review pull request" +magit/start-code-review))))


(use-package! evil-collection-magit
  :when (modulep! :editor evil +everywhere)
  :defer t
  :init (defvar evil-collection-magit-use-z-for-folds t)
  :config
  ;; q is enough; ESC is way too easy for a vimmer to accidentally press,
  ;; especially when traversing modes in magit buffers.
  (evil-define-key* 'normal magit-status-mode-map [escape] nil)

  (after! code-review
    (map! :map code-review-mode-map
          :n "r" #'code-review-transient-api
          :n "RET" #'code-review-comment-add-or-edit))

  ;; Some extra vim-isms I thought were missing from upstream
  (evil-define-key* '(normal visual) magit-mode-map
    "*"  #'magit-worktree
    "zt" #'evil-scroll-line-to-top
    "zz" #'evil-scroll-line-to-center
    "zb" #'evil-scroll-line-to-bottom
    "g=" #'magit-diff-default-context
    "gi" #'forge-jump-to-issues
    "gm" #'forge-jump-to-pullreqs)

  ;; Fix these keybinds because they are blacklisted
  ;; REVIEW There must be a better way to exclude particular evil-collection
  ;;        modules from the blacklist.
  (map! (:map magit-mode-map
         :nv "q" #'+magit/quit
         :nv "Q" #'+magit/quit-all
         :nv "]" #'magit-section-forward-sibling
         :nv "[" #'magit-section-backward-sibling
         :nv "gr" #'magit-refresh
         :nv "gR" #'magit-refresh-all)
        (:map magit-status-mode-map
         :nv "gz" #'magit-refresh)
        (:map magit-diff-mode-map
         :nv "gd" #'magit-jump-to-diffstat-or-diff)
        ;; Don't open recursive process buffers
        (:map magit-process-mode-map
         :nv "`" #'ignore))

  ;; A more intuitive behavior for TAB in magit buffers:
  (define-key! 'normal
    (magit-status-mode-map
     magit-stash-mode-map
     magit-revision-mode-map
     magit-process-mode-map
     magit-diff-mode-map)
    [tab] #'magit-section-toggle)

  (after! git-rebase
    (dolist (key '(("M-k" . "gk") ("M-j" . "gj")))
      (when-let (desc (assoc (car key) evil-collection-magit-rebase-commands-w-descriptions))
        (setcar desc (cdr key))))
    (evil-define-key* evil-collection-magit-state git-rebase-mode-map
      "gj" #'git-rebase-move-line-down
      "gk" #'git-rebase-move-line-up)))


(use-package! evil-collection-magit-section
  :when (modulep! :editor evil +everywhere)
  :defer t
  :init
  (defvar evil-collection-magit-section-use-z-for-folds evil-collection-magit-use-z-for-folds)
  :config
  (defadvice! +magit--override-evil-collection-defaults-a (&rest _)
    :after #'evil-collection-magit-section-setup
    ;; These numbered keys mask the numerical prefix keys. Since they've already
    ;; been replaced with z1, z2, z3, etc (and 0 with g=), there's no need to
    ;; keep them around:
    (undefine-key! magit-section-mode-map "M-1" "M-2" "M-3" "M-4" "1" "2" "3" "4" "0")
    ;; `evil-collection-magit-section' binds these redundant keys.
    (map! :map magit-section-mode-map :n "1" nil :n "2" nil :n "3" nil :n "4" nil)))


(use-package! git-commit
  :hook (doom-first-file . global-git-commit-mode)
  :config
  (set-yas-minor-mode! 'git-commit-mode)

  ;; Enforce git commit conventions.
  ;; See https://chris.beams.io/posts/git-commit/
  (setq git-commit-summary-max-length 50
        git-commit-style-convention-checks '(overlong-summary-line non-empty-second-line))
  (setq-hook! 'git-commit-mode-hook fill-column 72)

  (add-hook! 'git-commit-setup-hook
    (defun +vc-start-in-insert-state-maybe-h ()
      "Start git-commit-mode in insert state if in a blank commit message,
otherwise in default state."
      (when (and (bound-and-true-p evil-local-mode)
                 (not (evil-emacs-state-p))
                 (bobp) (eolp))
        (evil-insert-state)))))
