diff options
Diffstat (limited to 'elpa/magit-4.3.1/magit-mode.el')
-rw-r--r-- | elpa/magit-4.3.1/magit-mode.el | 1522 |
1 files changed, 1522 insertions, 0 deletions
diff --git a/elpa/magit-4.3.1/magit-mode.el b/elpa/magit-4.3.1/magit-mode.el new file mode 100644 index 0000000..572381b --- /dev/null +++ b/elpa/magit-4.3.1/magit-mode.el @@ -0,0 +1,1522 @@ +;;; magit-mode.el --- Create and refresh Magit buffers -*- lexical-binding:t -*- + +;; Copyright (C) 2008-2025 The Magit Project Contributors + +;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> +;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;; Magit 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 3 of the License, or +;; (at your option) any later version. +;; +;; Magit 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 Magit. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This library implements the abstract major-mode `magit-mode' from +;; which almost all other Magit major-modes derive. The code in here +;; is mostly concerned with creating and refreshing Magit buffers. + +;;; Code: + +(require 'magit-base) +(require 'magit-git) + +(require 'benchmark) +(require 'browse-url) +(require 'format-spec) +(require 'help-mode) + +(require 'transient) + +(defvar bookmark-make-record-function) + +(eval-when-compile (require 'elp)) +(declare-function elp-reset-all "elp" ()) +(declare-function elp-instrument-package "elp" (prefix)) +(declare-function elp-results "elp" ()) +(declare-function elp-restore-all "elp" ()) + +(defvar magit--wip-inhibit-autosave) +(defvar magit-wip-after-save-local-mode) +(declare-function magit-wip-get-ref "magit-wip" ()) +(declare-function magit-wip-commit-worktree "magit-wip" (ref files msg)) + +;;; Options + +(defcustom magit-mode-hook + (list #'magit-load-config-extensions) + "Hook run when entering a mode derived from Magit mode." + :package-version '(magit . "3.0.0") + :group 'magit-modes + :type 'hook + :options (list #'magit-load-config-extensions + #'bug-reference-mode)) + +(defcustom magit-setup-buffer-hook + (list #'magit-maybe-save-repository-buffers + 'magit-set-buffer-margin) ; from magit-margin.el + "Hook run by `magit-setup-buffer'. + +This is run right after displaying the buffer and right before +generating or updating its content. `magit-mode-hook' and other, +more specific, `magit-mode-*-hook's on the other hand are run +right before displaying the buffer. Usually one of these hooks +should be used instead of this one." + :package-version '(magit . "2.3.0") + :group 'magit-modes + :type 'hook + :options (list #'magit-maybe-save-repository-buffers + 'magit-set-buffer-margin)) + +(defcustom magit-pre-refresh-hook + (list #'magit-maybe-save-repository-buffers) + "Hook run before refreshing in `magit-refresh'. + +This hook, or `magit-post-refresh-hook', should be used +for functions that are not tied to a particular buffer. + +To run a function with a particular buffer current, use +`magit-refresh-buffer-hook' and use `derived-mode-p' +inside your function." + :package-version '(magit . "2.4.0") + :group 'magit-refresh + :type 'hook + :options (list #'magit-maybe-save-repository-buffers)) + +(defcustom magit-post-refresh-hook + ;; Do not function-quote to avoid circular dependencies. + '(magit-auto-revert-buffers + magit-run-post-commit-hook + magit-run-post-stage-hook + magit-run-post-unstage-hook) + "Hook run after refreshing in `magit-refresh'. + +This hook, or `magit-pre-refresh-hook', should be used +for functions that are not tied to a particular buffer. + +To run a function with a particular buffer current, use +`magit-refresh-buffer-hook' and use `derived-mode-p' +inside your function." + :package-version '(magit . "2.4.0") + :group 'magit-refresh + :type 'hook + :options '(magit-auto-revert-buffers + magit-run-post-commit-hook + magit-run-post-stage-hook + magit-run-post-unstage-hook)) + +(defcustom magit-display-buffer-function #'magit-display-buffer-traditional + "The function used to display a Magit buffer. + +All Magit buffers (buffers whose major-modes derive from +`magit-mode') are displayed using `magit-display-buffer', +which in turn uses the function specified here." + :package-version '(magit . "2.3.0") + :group 'magit-buffers + :type `(radio (function-item ,#'magit-display-buffer-traditional) + (function-item ,#'magit-display-buffer-same-window-except-diff-v1) + (function-item ,#'magit-display-buffer-fullframe-status-v1) + (function-item ,#'magit-display-buffer-fullframe-status-topleft-v1) + (function-item ,#'magit-display-buffer-fullcolumn-most-v1) + (function-item ,#'display-buffer) + (function :tag "Function"))) + +(defcustom magit-pre-display-buffer-hook + (list #'magit-save-window-configuration) + "Hook run by `magit-display-buffer' before displaying the buffer." + :package-version '(magit . "2.3.0") + :group 'magit-buffers + :type 'hook + :get #'magit-hook-custom-get + :options (list #'magit-save-window-configuration)) + +(defcustom magit-post-display-buffer-hook (list #'magit-maybe-set-dedicated) + "Hook run by `magit-display-buffer' after displaying the buffer." + :package-version '(magit . "2.3.0") + :group 'magit-buffers + :type 'hook + :get #'magit-hook-custom-get + :options (list #'magit-maybe-set-dedicated)) + +(defcustom magit-generate-buffer-name-function + #'magit-generate-buffer-name-default-function + "The function used to generate the name for a Magit buffer." + :package-version '(magit . "2.3.0") + :group 'magit-buffers + :type `(radio (function-item ,#'magit-generate-buffer-name-default-function) + (function :tag "Function"))) + +(defcustom magit-buffer-name-format "%x%M%v: %t%x" + "The format string used to name Magit buffers. + +The following %-sequences are supported: + +`%m' The name of the major-mode, but with the `-mode' suffix + removed. + +`%M' Like \"%m\" but abbreviate `magit-status-mode' as `magit'. + +`%v' The value the buffer is locked to, in parentheses, or an + empty string if the buffer is not locked to a value. + +`%V' Like \"%v\", but the string is prefixed with a space, unless + it is an empty string. + +`%t' The top-level directory of the working tree of the + repository, or if `magit-uniquify-buffer-names' is non-nil + an abbreviation of that. + +`%x' If `magit-uniquify-buffer-names' is nil \"*\", otherwise the + empty string. Due to limitations of the `uniquify' package, + buffer names must end with the path. + +The value should always contain \"%m\" or \"%M\", \"%v\" or \"%V\", and +\"%t\". If `magit-uniquify-buffer-names' is non-nil, then the +value must end with \"%t\" or \"%t%x\". See issue #2841. + +This is used by `magit-generate-buffer-name-default-function'. +If another `magit-generate-buffer-name-function' is used, then +it may not respect this option, or on the contrary it may +support additional %-sequences." + :package-version '(magit . "2.12.0") + :group 'magit-buffers + :type 'string) + +(defcustom magit-uniquify-buffer-names t + "Whether to uniquify the names of Magit buffers." + :package-version '(magit . "2.3.0") + :group 'magit-buffers + :type 'boolean) + +(defcustom magit-bury-buffer-function #'magit-mode-quit-window + "The function used to bury or kill the current Magit buffer." + :package-version '(magit . "3.2.0") + :group 'magit-buffers + :type `(radio (function-item ,#'quit-window) + (function-item ,#'magit-mode-quit-window) + (function-item ,#'magit-restore-window-configuration) + (function :tag "Function"))) + +(defcustom magit-prefix-use-buffer-arguments 'selected + "Whether certain prefix commands reuse arguments active in relevant buffer. + +This affects the transient prefix commands `magit-diff', +`magit-log' and `magit-show-refs'. + +Valid values are: + +`always': Always use the set of arguments that is currently + active in the respective buffer, provided that buffer exists + of course. +`selected': Use the set of arguments from the respective + buffer, but only if it is displayed in a window of the current + frame. This is the default. +`current': Use the set of arguments from the respective buffer, + but only if it is the current buffer. +`never': Never use the set of arguments from the respective + buffer. + +For more information see info node `(magit)Transient Arguments +and Buffer Variables'." + :package-version '(magit . "3.0.0") + :group 'magit-buffers + :group 'magit-commands + :group 'magit-diff + :group 'magit-log + :type '(choice + (const :tag "Always use args from buffer" always) + (const :tag "Use args from buffer if displayed in frame" selected) + (const :tag "Use args from buffer if it is current" current) + (const :tag "Never use args from buffer" never))) + +(defcustom magit-direct-use-buffer-arguments 'selected + "Whether certain commands reuse arguments active in relevant buffer. + +This affects certain commands such as `magit-show-commit' that +are suffixes of the diff or log transient prefix commands, but +only if they are invoked directly, i.e., *not* as a suffix. + +Valid values are: + +`always': Always use the set of arguments that is currently + active in the respective buffer, provided that buffer exists + of course. +`selected': Use the set of arguments from the respective + buffer, but only if it is displayed in a window of the current + frame. This is the default. +`current': Use the set of arguments from the respective buffer, + but only if it is the current buffer. +`never': Never use the set of arguments from the respective + buffer. + +For more information see info node `(magit)Transient Arguments +and Buffer Variables'." + :package-version '(magit . "3.0.0") + :group 'magit-buffers + :group 'magit-commands + :group 'magit-diff + :group 'magit-log + :type '(choice + (const :tag "Always use args from buffer" always) + (const :tag "Use args from buffer if displayed in frame" selected) + (const :tag "Use args from buffer if it is current" current) + (const :tag "Never use args from buffer" never))) + +(defcustom magit-region-highlight-hook + '(magit-diff-update-hunk-region) ; from magit-diff.el + "Functions used to highlight the region. + +Each function is run with the current section as only argument +until one of them returns non-nil. If all functions return nil, +then fall back to regular region highlighting." + :package-version '(magit . "2.1.0") + :group 'magit-refresh + :type 'hook + :options '(magit-diff-update-hunk-region)) + +(defcustom magit-create-buffer-hook nil + "Normal hook run while creating a new `magit-mode' buffer. +Runs before the buffer is populated with sections. Also see +`magit-post-create-buffer-hook'." + :package-version '(magit . "2.90.0") + :group 'magit-refresh + :type 'hook) + +(defcustom magit-post-create-buffer-hook nil + "Normal hook run after creating a new `magit-mode' buffer. +Runs after the buffer is populated with sections for the first +time. Also see `magit-create-buffer-hook' (which runs earlier) +and `magit-refresh-buffer-hook' (which runs on every refresh)." + :package-version '(magit . "4.0.0") + :group 'magit-refresh + :type 'hook) + +(defcustom magit-refresh-buffer-hook nil + "Normal hook for `magit-refresh-buffer' to run after refreshing." + :package-version '(magit . "2.1.0") + :group 'magit-refresh + :type 'hook) + +(defcustom magit-refresh-status-buffer t + "Whether the status buffer is refreshed after running git. + +When this is non-nil, then the status buffer is automatically +refreshed after running git for side-effects, in addition to the +current Magit buffer, which is always refreshed automatically. + +Only set this to nil after exhausting all other options to +improve performance." + :package-version '(magit . "2.4.0") + :group 'magit-refresh + :group 'magit-status + :type 'boolean) + +(defcustom magit-refresh-verbose nil + "Whether to revert Magit buffers verbosely." + :package-version '(magit . "2.1.0") + :group 'magit-refresh + :type 'boolean) + +(defcustom magit-save-repository-buffers t + "Whether to save file-visiting buffers when appropriate. + +If non-nil, then all modified file-visiting buffers belonging +to the current repository may be saved before running Magit +commands and before creating or refreshing Magit buffers. +If `dontask', then this is done without user intervention, for +any other non-nil value the user has to confirm each save. + +The default is t to avoid surprises, but `dontask' is the +recommended value." + :group 'magit-essentials + :group 'magit-buffers + :type '(choice (const :tag "Never" nil) + (const :tag "Ask" t) + (const :tag "Save without asking" dontask))) + +;;; Key Bindings + +(defvar-keymap magit-mode-map + :doc "Parent keymap for all keymaps of modes derived from `magit-mode'." + :parent magit-section-mode-map + ;; Don't function-quote but make sure all commands are autoloaded. + "C-<return>" 'magit-visit-thing + "RET" 'magit-visit-thing + "M-TAB" 'magit-dired-jump + "M-<tab>" 'magit-section-cycle-diffs + "SPC" 'magit-diff-show-or-scroll-up + "S-SPC" 'magit-diff-show-or-scroll-down + "DEL" 'magit-diff-show-or-scroll-down + "+" 'magit-diff-more-context + "-" 'magit-diff-less-context + "0" 'magit-diff-default-context + "a" 'magit-cherry-apply + "A" 'magit-cherry-pick + "b" 'magit-branch + "B" 'magit-bisect + "c" 'magit-commit + "C" 'magit-clone + "d" 'magit-diff + "D" 'magit-diff-refresh + "e" 'magit-ediff-dwim + "E" 'magit-ediff + "f" 'magit-fetch + "F" 'magit-pull + "g" 'magit-refresh + "G" 'magit-refresh-all + "h" 'magit-dispatch + "?" 'magit-dispatch + "H" 'magit-describe-section + "i" 'magit-gitignore + "I" 'magit-init + "j" 'magit-status-quick + "J" 'magit-display-repository-buffer + "k" 'magit-delete-thing + "K" 'magit-file-untrack + "l" 'magit-log + "L" 'magit-log-refresh + "m" 'magit-merge + "M" 'magit-remote + ;; "n" magit-section-forward in magit-section-mode-map + ;; "N" forge-dispatch, added by forge package + "o" 'magit-submodule + "O" 'magit-subtree + ;; "p" magit-section-backward in magit-section-mode-map + "P" 'magit-push + "q" 'magit-mode-bury-buffer + "Q" 'magit-git-command + ":" 'magit-git-command + "r" 'magit-rebase + "R" 'magit-file-rename + "s" 'magit-stage-file + "S" 'magit-stage-modified + "t" 'magit-tag + "T" 'magit-notes + "u" 'magit-unstage-file + "U" 'magit-unstage-all + "v" 'magit-revert-no-commit + "V" 'magit-revert + "w" 'magit-am + "W" 'magit-patch + "x" 'magit-reset-quickly + "X" 'magit-reset + "y" 'magit-show-refs + "Y" 'magit-cherry + "z" 'magit-stash + "Z" 'magit-worktree + "%" 'magit-worktree + "$" 'magit-process-buffer + "!" 'magit-run + ">" 'magit-sparse-checkout + "C-c C-c" 'magit-dispatch + "C-c C-r" 'magit-next-reference + "C-c C-e" 'magit-edit-thing + "C-c C-o" 'magit-browse-thing + "C-c C-w" 'magit-copy-thing + "C-w" 'magit-copy-section-value + "M-w" 'magit-copy-buffer-revision + "<remap> <back-to-indentation>" 'magit-back-to-indentation + "<remap> <previous-line>" 'magit-previous-line + "<remap> <next-line>" 'magit-next-line + "<remap> <evil-previous-line>" 'evil-previous-visual-line + "<remap> <evil-next-line>" 'evil-next-visual-line) + +(defun magit-delete-thing () + "This is a placeholder command, which signals an error if called. +Where applicable, other keymaps remap this command to another, +which actually deletes the thing at point." + (interactive) + (user-error "There is no thing at point that could be deleted")) +;; Starting with Emacs 28.1 we could use (declare (completion ignore)). +(put 'magit-delete-thing 'completion-predicate #'ignore) + +(defun magit-visit-thing () + "This is a placeholder command, which may signal an error if called. +Where applicable, other keymaps remap this command to another, +which actually visits the thing at point." + (interactive) + (if (eq transient-current-command 'magit-dispatch) + (call-interactively (key-binding (this-command-keys))) + (if-let ((url (thing-at-point 'url t))) + (browse-url url) + (user-error "There is no thing at point that could be visited")))) +(put 'magit-visit-thing 'completion-predicate #'ignore) + +(defun magit-edit-thing () + "This is a placeholder command, which may signal an error if called. +Where applicable, other keymaps remap this command to another, +which actually lets you edit the thing at point, likely in another +buffer." + (interactive) + (if (eq transient-current-command 'magit-dispatch) + (call-interactively (key-binding (this-command-keys))) + (user-error "There is no thing at point that could be edited"))) +(put 'magit-edit-thing 'completion-predicate #'ignore) + +(defun magit-browse-thing () + "This is a placeholder command, which may signal an error if called. +Where applicable, other keymaps remap this command to another, +which actually visits thing at point using `browse-url'." + (interactive) + (if-let ((url (thing-at-point 'url t))) + (browse-url url) + (user-error "There is no thing at point that could be browsed"))) +(put 'magit-browse-thing 'completion-predicate #'ignore) + +(defun magit-copy-thing () + "This is a placeholder command, which signals an error if called. +Where applicable, other keymaps remap this command to another, +which actually copies some representation of the thing at point +to the kill ring." + (interactive) + (user-error "There is no thing at point that we know how to copy")) +(put 'magit-copy-thing 'completion-predicate #'ignore) + +;;;###autoload +(defun magit-info () + "Visit the Magit manual." + (interactive) + (info "magit")) + +(defvar bug-reference-map) +(with-eval-after-load 'bug-reference + (keymap-set bug-reference-map "<remap> <magit-visit-thing>" + 'bug-reference-push-button)) + +(easy-menu-define magit-mode-menu magit-mode-map + "Magit menu." + ;; Similar to `magit-dispatch' but exclude: + ;; - commands that are available from context menus: + ;; apply, reverse, discard, stage, unstage, + ;; cherry-pick, revert, reset, + ;; describe-section + ;; - commands that are available from submenus: + ;; git-command, ediff-dwim + ;; - and: refresh-all, status-jump, status-quick. + '("Magit" + "---" "Inspect" + [" Bisect..." magit-bisect t] + [" Cherries..." magit-cherry t] + [" Diff..." magit-diff t] + [" Ediff..." magit-ediff t] + [" Log..." magit-log t] + [" References..." magit-show-refs t] + "---" "Manipulate" + [" Commit..." magit-commit t] + [" Stash..." magit-stash t] + [" Tag..." magit-tag t] + "---" + [" Branch..." magit-branch t] + [" Remote..." magit-remote t] + "---" + [" Merge..." magit-merge t] + [" Rebase..." magit-rebase t] + "---" "Transfer" + [" Fetch..." magit-fetch t] + [" Pull..." magit-pull t] + [" Push..." magit-push t] + "---" "Setup" + [" Clone..." magit-clone t] + [" Ignore..." magit-gitignore t] + [" Init..." magit-init t] + "---" + ("Advanced" + ["Run..." magit-run t] + "---" + ["Apply patches..." magit-am t] + ["Format patches..." magit-patch t] + "---" + ["Note..." magit-notes t] + "---" + ["Submodule..." magit-submodule t] + ["Subtree..." magit-subtree t] + ["Worktree..." magit-worktree t]) + "---" + ["Show command dispatcher..." magit-dispatch t] + ["Show manual" magit-info t] + ["Show another buffer" magit-display-repository-buffer t] + "---" + ("Change buffer arguments" + ["Diff arguments" magit-diff-refresh t] + ["Log arguments" magit-log-refresh t]) + ["Refresh buffer" magit-refresh t] + ["Bury buffer" magit-mode-bury-buffer t])) + +;;; Mode + +(defun magit-load-config-extensions () + "Load Magit extensions that are defined at the Git config layer." + (dolist (ext (magit-get-all "magit.extension")) + (let ((sym (intern (format "magit-%s-mode" ext)))) + (when (fboundp sym) + (funcall sym 1))))) + +(define-derived-mode magit-mode magit-section-mode "Magit" + "Parent major mode from which Magit major modes inherit. + +Magit is documented in info node `(magit)'." + :interactive nil + :group 'magit + (magit-hack-dir-local-variables) + (face-remap-add-relative 'header-line 'magit-header-line) + (setq mode-line-process (magit-repository-local-get 'mode-line-process)) + (setq-local revert-buffer-function #'magit-refresh-buffer) + (setq-local bookmark-make-record-function #'magit--make-bookmark) + (setq-local imenu-create-index-function #'magit--imenu-create-index) + (setq-local imenu-default-goto-function #'magit--imenu-goto-function) + (setq-local isearch-filter-predicate #'magit-section--open-temporarily)) + +(defun magit-hack-dir-local-variables () + "Like `hack-dir-local-variables-non-file-buffer' but ignore some variables." + (let ((ignored-local-variables + `(show-trailing-whitespace + ,@ignored-local-variables))) + (hack-dir-local-variables-non-file-buffer))) + +;;; Local Variables + +(defvar-local magit-buffer-arguments nil) +(defvar-local magit-buffer-diff-type nil) +(defvar-local magit-buffer-diff-args nil) +(defvar-local magit-buffer-diff-files nil) +(defvar-local magit-buffer-diff-files-suspended nil) +(defvar-local magit-buffer-file-name nil) +(defvar-local magit-buffer-files nil) +(defvar-local magit-buffer-log-args nil) +(defvar-local magit-buffer-log-files nil) +(defvar-local magit-buffer-range nil) +(defvar-local magit-buffer-range-hashed nil) +(defvar-local magit-buffer-refname nil) +(defvar-local magit-buffer-revision nil) +(defvar-local magit-buffer-revision-hash nil) +(defvar-local magit-buffer-revisions nil) +(defvar-local magit-buffer-typearg nil) +(defvar-local magit-buffer-upstream nil) + +;; These variables are also used in file-visiting buffers. +;; Because the user may change the major-mode, they have +;; to be permanent buffer-local. +(put 'magit-buffer-file-name 'permanent-local t) +(put 'magit-buffer-refname 'permanent-local t) +(put 'magit-buffer-revision 'permanent-local t) +(put 'magit-buffer-revision-hash 'permanent-local t) + +;; `magit-status' re-enables mode function but its refresher +;; function does not reinstate this. +(put 'magit-buffer-diff-files-suspended 'permanent-local t) + +(cl-defgeneric magit-buffer-value () + "Return the value of the current buffer. +The \"value\" identifies what is being displayed in the buffer. +The buffer's major-mode should derive from `magit-section-mode'." + nil) + +(defvar-local magit-previous-section nil) +(put 'magit-previous-section 'permanent-local t) + +;;; Setup Buffer + +(defmacro magit-setup-buffer (mode &optional locked &rest bindings) + (declare (indent 2)) + `(magit-setup-buffer-internal + ,mode ,locked + ,(cons 'list (mapcar (pcase-lambda (`(,var ,form)) + `(list ',var ,form)) + bindings)))) + +(defun magit-setup-buffer-internal ( mode locked bindings + &optional buffer-or-name directory) + (let* ((value (and locked + (with-temp-buffer + (pcase-dolist (`(,var ,val) bindings) + (set (make-local-variable var) val)) + (let ((major-mode mode)) + (magit-buffer-value))))) + (buffer (if buffer-or-name + (get-buffer-create buffer-or-name) + (magit-get-mode-buffer mode value))) + (section (and buffer (magit-current-section))) + (created (not buffer))) + (unless buffer + (setq buffer (magit-generate-new-buffer mode value))) + (with-current-buffer buffer + (setq magit-previous-section section) + (when directory + (setq default-directory directory)) + (funcall mode) + (magit-xref-setup #'magit-setup-buffer-internal bindings) + (pcase-dolist (`(,var ,val) bindings) + (set (make-local-variable var) val)) + (when created + (run-hooks 'magit-create-buffer-hook))) + (magit-display-buffer buffer) + (with-current-buffer buffer + (run-hooks 'magit-setup-buffer-hook) + (magit-refresh-buffer) + (when created + (run-hooks 'magit-post-create-buffer-hook))) + buffer)) + +;;; Display Buffer + +(defvar magit-display-buffer-noselect nil + "If non-nil, then `magit-display-buffer' doesn't call `select-window'.") + +(defun magit-display-buffer (buffer &optional display-function) + "Display BUFFER in some window and maybe select it. + +If optional DISPLAY-FUNCTION is non-nil, then use that to display +the buffer. Otherwise use `magit-display-buffer-function', which +is the normal case. + +Then, unless `magit-display-buffer-noselect' is non-nil, select +the window which was used to display the buffer. + +Also run the hooks `magit-pre-display-buffer-hook' +and `magit-post-display-buffer-hook'." + (with-current-buffer buffer + (run-hooks 'magit-pre-display-buffer-hook)) + (let ((window (funcall (or display-function magit-display-buffer-function) + buffer))) + (unless magit-display-buffer-noselect + (let* ((old-frame (selected-frame)) + (new-frame (window-frame window))) + (select-window window) + (unless (eq old-frame new-frame) + (select-frame-set-input-focus new-frame))))) + (with-current-buffer buffer + (run-hooks 'magit-post-display-buffer-hook))) + +(defun magit-display-buffer-traditional (buffer) + "Display BUFFER the way this has traditionally been done." + (display-buffer + buffer (if (and (derived-mode-p 'magit-mode) + (not (memq (with-current-buffer buffer major-mode) + '(magit-process-mode + magit-revision-mode + magit-diff-mode + magit-stash-mode + magit-status-mode)))) + '(display-buffer-same-window) + nil))) ; display in another window + +(defun magit-display-buffer-same-window-except-diff-v1 (buffer) + "Display BUFFER in the selected window except for some modes. +If a buffer's `major-mode' derives from `magit-diff-mode' or +`magit-process-mode', display it in another window. Display all +other buffers in the selected window." + (display-buffer + buffer (if (with-current-buffer buffer + (derived-mode-p 'magit-diff-mode 'magit-process-mode)) + '(nil (inhibit-same-window . t)) + '(display-buffer-same-window)))) + +(defun magit--display-buffer-fullframe (buffer alist) + (when-let ((window (or (display-buffer-reuse-window buffer alist) + (display-buffer-same-window buffer alist) + (display-buffer-pop-up-window buffer alist) + (display-buffer-use-some-window buffer alist)))) + (delete-other-windows window) + window)) + +(defun magit-display-buffer-fullframe-status-v1 (buffer) + "Display BUFFER, filling entire frame if BUFFER is a status buffer. +Otherwise, behave like `magit-display-buffer-traditional'." + (if (eq (with-current-buffer buffer major-mode) + 'magit-status-mode) + (display-buffer buffer '(magit--display-buffer-fullframe)) + (magit-display-buffer-traditional buffer))) + +(defun magit--display-buffer-topleft (buffer alist) + (or (display-buffer-reuse-window buffer alist) + (when-let ((window2 (display-buffer-pop-up-window buffer alist))) + (let ((window1 (get-buffer-window)) + (buffer1 (current-buffer)) + (buffer2 (window-buffer window2)) + (w2-quit-restore (window-parameter window2 'quit-restore))) + (set-window-buffer window1 buffer2) + (set-window-buffer window2 buffer1) + (select-window window2) + ;; Swap some window state that `magit-mode-quit-window' and + ;; `quit-restore-window' inspect. + (set-window-prev-buffers window2 (cdr (window-prev-buffers window1))) + (set-window-prev-buffers window1 nil) + (set-window-parameter window2 'magit-dedicated + (window-parameter window1 'magit-dedicated)) + (set-window-parameter window1 'magit-dedicated t) + (set-window-parameter window1 'quit-restore + (list 'window 'window + (nth 2 w2-quit-restore) + (nth 3 w2-quit-restore))) + (set-window-parameter window2 'quit-restore nil) + window1)))) + +(defun magit-display-buffer-fullframe-status-topleft-v1 (buffer) + "Display BUFFER, filling entire frame if BUFFER is a status buffer. +When BUFFER derives from `magit-diff-mode' or +`magit-process-mode', try to display BUFFER to the top or left of +the current buffer rather than to the bottom or right, as +`magit-display-buffer-fullframe-status-v1' would. Whether the +split is made vertically or horizontally is determined by +`split-window-preferred-function'." + (display-buffer + buffer + (cond ((eq (with-current-buffer buffer major-mode) + 'magit-status-mode) + '(magit--display-buffer-fullframe)) + ((with-current-buffer buffer + (derived-mode-p 'magit-diff-mode 'magit-process-mode)) + '(magit--display-buffer-topleft)) + (t + '(display-buffer-same-window))))) + +(defun magit--display-buffer-fullcolumn (buffer alist) + (when-let ((window (or (display-buffer-reuse-window buffer alist) + (display-buffer-same-window buffer alist) + (display-buffer-below-selected buffer alist)))) + (delete-other-windows-vertically window) + window)) + +(defun magit-display-buffer-fullcolumn-most-v1 (buffer) + "Display BUFFER using the full column except in some cases. +For most cases where BUFFER's `major-mode' derives from +`magit-mode', display it in the selected window and grow that +window to the full height of the frame, deleting other windows in +that column as necessary. However, display BUFFER in another +window if 1) BUFFER's mode derives from `magit-process-mode', or +2) BUFFER's mode derives from `magit-diff-mode', provided that +the mode of the current buffer derives from `magit-log-mode' or +`magit-cherry-mode'." + (display-buffer + buffer + (cond ((and (or (bound-and-true-p git-commit-mode) + (derived-mode-p 'magit-log-mode + 'magit-cherry-mode + 'magit-reflog-mode)) + (with-current-buffer buffer + (derived-mode-p 'magit-diff-mode))) + nil) + ((with-current-buffer buffer + (derived-mode-p 'magit-process-mode)) + nil) + (t + '(magit--display-buffer-fullcolumn))))) + +(defun magit-maybe-set-dedicated () + "Mark the selected window as dedicated if appropriate. + +If a new window was created to display the buffer, then remember +that fact. That information is used by `magit-mode-quit-window', +to determine whether the window should be deleted when its last +Magit buffer is buried." + (let ((window (get-buffer-window (current-buffer)))) + (when (and (window-live-p window) + (not (window-prev-buffers window))) + (set-window-parameter window 'magit-dedicated t)))) + +;;; Get Buffer + +(defvar-local magit--default-directory nil + "Value of `default-directory' when buffer is generated. +This exists to prevent a let-bound `default-directory' from +tricking `magit-get-mode-buffer' or `magit-mode-get-buffers' +into thinking a buffer belongs to a repo that it doesn't.") +(put 'magit--default-directory 'permanent-local t) + +(defun magit-mode-get-buffers () + (let ((topdir (magit-toplevel))) + (seq-filter (##with-current-buffer % + (and (derived-mode-p 'magit-mode) + (equal magit--default-directory topdir))) + (buffer-list)))) + +(defvar-local magit-buffer-locked-p nil) +(put 'magit-buffer-locked-p 'permanent-local t) + +(defun magit-get-mode-buffer (mode &optional value frame) + "Return buffer belonging to the current repository whose major-mode is MODE. + +If no such buffer exists then return nil. Multiple buffers with +the same major-mode may exist for a repository but only one can +exist that hasn't been locked to its value. Return that buffer +\(or nil if there is no such buffer) unless VALUE is non-nil, in +which case return the buffer that has been locked to that value. + +If FRAME is nil or omitted, then consider all buffers. Otherwise + only consider buffers that are displayed in some live window + on some frame. +If `all', then consider all buffers on all frames. +If `visible', then only consider buffers on all visible frames. +If `selected' or t, then only consider buffers on the selected + frame. +If a frame, then only consider buffers on that frame." + (let ((topdir (magit--toplevel-safe))) + (cl-flet* ((b (buffer) + (with-current-buffer buffer + (and (eq major-mode mode) + (equal magit--default-directory topdir) + (if value + (and magit-buffer-locked-p + (equal (magit-buffer-value) value)) + (not magit-buffer-locked-p)) + buffer))) + (w (window) + (b (window-buffer window))) + (f (frame) + (seq-some #'w (window-list frame 'no-minibuf)))) + (pcase-exhaustive frame + ('nil (seq-some #'b (buffer-list))) + ('all (seq-some #'f (frame-list))) + ('visible (seq-some #'f (visible-frame-list))) + ((or 'selected 't) (seq-some #'w (window-list (selected-frame)))) + ((guard (framep frame)) (seq-some #'w (window-list frame))))))) + +(defun magit-generate-new-buffer (mode &optional value directory) + (let* ((default-directory (or directory (magit--toplevel-safe))) + (name (funcall magit-generate-buffer-name-function mode value)) + (buffer (generate-new-buffer name))) + (with-current-buffer buffer + (setq magit--default-directory default-directory) + (setq magit-buffer-locked-p (and value t)) + (magit-restore-section-visibility-cache mode)) + (when magit-uniquify-buffer-names + (cl-pushnew mode uniquify-list-buffers-directory-modes) + (with-current-buffer buffer + (setq list-buffers-directory (abbreviate-file-name default-directory))) + (let ((uniquify-buffer-name-style + (if (memq uniquify-buffer-name-style '(nil forward)) + 'post-forward-angle-brackets + uniquify-buffer-name-style))) + (uniquify-rationalize-file-buffer-names + name (file-name-directory (directory-file-name default-directory)) + buffer))) + buffer)) + +(defun magit-generate-buffer-name-default-function (mode &optional value) + "Generate buffer name for a MODE buffer in the current repository. +The returned name is based on `magit-buffer-name-format' and +takes `magit-uniquify-buffer-names' and VALUE, if non-nil, into +account." + (let ((m (substring (symbol-name mode) 0 -5)) + (v (and value (format "%s" (ensure-list value)))) + (n (if magit-uniquify-buffer-names + (file-name-nondirectory + (directory-file-name default-directory)) + (abbreviate-file-name default-directory)))) + (format-spec + magit-buffer-name-format + `((?m . ,m) + (?M . ,(if (eq mode 'magit-status-mode) "magit" m)) + (?v . ,(or v "")) + (?V . ,(if v (concat " " v) "")) + (?t . ,n) + (?x . ,(if magit-uniquify-buffer-names "" "*")))))) + +;;; Buffer Lock + +(defun magit-toggle-buffer-lock () + "Lock the current buffer to its value or unlock it. + +Locking a buffer to its value prevents it from being reused to +display another value. The name of a locked buffer contains its +value, which allows telling it apart from other locked buffers +and the unlocked buffer. + +Not all Magit buffers can be locked to their values, for example +it wouldn't make sense to lock a status buffer. + +There can only be a single unlocked buffer using a certain +major-mode per repository. So when a buffer is being unlocked +and another unlocked buffer already exists for that mode and +repository, then the former buffer is instead deleted and the +latter is displayed in its place." + (interactive) + (if magit-buffer-locked-p + (if-let ((unlocked (magit-get-mode-buffer major-mode))) + (let ((locked (current-buffer))) + (switch-to-buffer unlocked nil t) + (kill-buffer locked)) + (setq magit-buffer-locked-p nil) + (rename-buffer (funcall magit-generate-buffer-name-function + major-mode))) + (if-let ((value (magit-buffer-value))) + (if-let ((locked (magit-get-mode-buffer major-mode value))) + (let ((unlocked (current-buffer))) + (switch-to-buffer locked nil t) + (kill-buffer unlocked)) + (setq magit-buffer-locked-p t) + (rename-buffer (funcall magit-generate-buffer-name-function + major-mode value))) + (user-error "Buffer has no value it could be locked to")))) + +;;; Bury Buffer + +(defun magit-mode-bury-buffer (&optional kill-buffer) + "Bury or kill the current buffer. + +Use `magit-bury-buffer-function' to bury the buffer when called +without a prefix argument or to kill it when called with a single +prefix argument. + +With two prefix arguments, always kill the current and all other +Magit buffers, associated with this repository." + (interactive "P") + (if (>= (prefix-numeric-value kill-buffer) 16) + (mapc #'kill-buffer (magit-mode-get-buffers)) + (funcall magit-bury-buffer-function kill-buffer))) + +(defun magit-mode-quit-window (kill-buffer) + "Quit the selected window and bury its buffer. + +This behaves similar to `quit-window', but when the window +was originally created to display a Magit buffer and the +current buffer is the last remaining Magit buffer that was +ever displayed in the selected window, then delete that +window." + (if (or (one-window-p) + (seq-find (pcase-lambda (`(,buffer)) + (and (not (eq buffer (current-buffer))) + (buffer-live-p buffer) + (or (not (window-parameter nil 'magit-dedicated)) + (with-current-buffer buffer + (derived-mode-p 'magit-mode + 'magit-process-mode))))) + (window-prev-buffers))) + (quit-window kill-buffer) + (let ((window (selected-window))) + (quit-window kill-buffer) + (when (window-live-p window) + (delete-window window))))) + +;;; Refresh Buffers + +(defvar magit-inhibit-refresh nil) + +(defun magit-refresh () + "Refresh some buffers belonging to the current repository. + +Refresh the current buffer if its major mode derives from +`magit-mode', and refresh the corresponding status buffer. + +Run hooks `magit-pre-refresh-hook' and `magit-post-refresh-hook'." + (interactive) + (unless magit-inhibit-refresh + (unwind-protect + (let ((start (current-time)) + (magit--refresh-cache (or magit--refresh-cache + (list (cons 0 0))))) + (when magit-refresh-verbose + (message "Refreshing magit...")) + (magit-run-hook-with-benchmark 'magit-pre-refresh-hook) + (cond ((derived-mode-p 'magit-mode) + (magit-refresh-buffer)) + ((derived-mode-p 'tabulated-list-mode) + (revert-buffer))) + (when-let ((buffer (and magit-refresh-status-buffer + (not (derived-mode-p 'magit-status-mode)) + (magit-get-mode-buffer 'magit-status-mode)))) + (with-current-buffer buffer + (magit-refresh-buffer))) + (magit-run-hook-with-benchmark 'magit-post-refresh-hook) + (when magit-refresh-verbose + (let* ((c (caar magit--refresh-cache)) + (a (+ c (cdar magit--refresh-cache)))) + (message "Refreshing magit...done (%.3fs, cached %s/%s (%.0f%%))" + (float-time (time-since start)) + c a (* (/ c (* a 1.0)) 100))))) + (run-hooks 'magit-unwind-refresh-hook)))) + +(defun magit-refresh-all () + "Refresh all buffers belonging to the current repository. + +Refresh all Magit buffers belonging to the current repository, +and revert buffers that visit files located inside the current +repository. + +Run hooks `magit-pre-refresh-hook' and `magit-post-refresh-hook'." + (interactive) + (magit-run-hook-with-benchmark 'magit-pre-refresh-hook) + (dolist (buffer (magit-mode-get-buffers)) + (with-current-buffer buffer (magit-refresh-buffer))) + (magit-run-hook-with-benchmark 'magit-post-refresh-hook)) + +(defvar-local magit-refresh-start-time nil) + +(defun magit-refresh-buffer (&rest _ignore) + "Refresh the current Magit buffer." + (interactive) + (setq magit-refresh-start-time (current-time)) + (let ((refresh (intern (format "%s-refresh-buffer" + (substring (symbol-name major-mode) 0 -5)))) + (magit--refresh-cache (or magit--refresh-cache (list (cons 0 0))))) + (when (functionp refresh) + (when magit-refresh-verbose + (message "Refreshing buffer `%s'..." (buffer-name))) + (let* ((buffer (current-buffer)) + (windows (mapcan + (lambda (window) + (with-selected-window window + (with-current-buffer buffer + (and-let* ((section (magit-section-at))) + `(( ,window + ,section + ,@(magit-section-get-relative-position + section))))))) + ;; If it qualifies, then the selected window + ;; comes first, but we want to handle it last + ;; so that its `magit-section-movement-hook' + ;; run can override the effects of other runs. + (or (nreverse (get-buffer-window-list buffer nil t)) + (list (selected-window)))))) + (deactivate-mark) + (setq magit-section-pre-command-section nil) + (setq magit-section-highlight-overlays nil) + (setq magit-section-highlighted-sections nil) + (setq magit-section-unhighlight-sections nil) + (let ((inhibit-read-only t)) + (erase-buffer) + (save-excursion + (funcall refresh))) + (pcase-dolist (`(,window . ,args) windows) + (if (eq buffer (window-buffer window)) + (with-selected-window window + (apply #'magit-section-goto-successor args)) + (with-current-buffer buffer + (let ((magit-section-movement-hook nil)) + (apply #'magit-section-goto-successor args))))) + (run-hooks 'magit-refresh-buffer-hook) + (magit-section-update-highlight) + (set-buffer-modified-p nil)) + (when magit-refresh-verbose + (message "Refreshing buffer `%s'...done (%.3fs)" (buffer-name) + (float-time (time-since magit-refresh-start-time))))))) + +(defun magit-profile-refresh-buffer () + "Profile refreshing the current Magit buffer." + (interactive) + (require (quote elp)) + (elp-reset-all) + (message "Profiling Magit and Forge...") + (elp-instrument-package "magit-") + (elp-instrument-package "forge-") + (magit-refresh-buffer) + (message "Profiling Magit and Forge...done") + (elp-results) + (elp-reset-all)) + +(defun magit-toggle-profiling () + "Start profiling Magit, or if in progress, stop and display the results." + (interactive) + (require (quote elp)) + (cond ((catch 'in-progress + (mapatoms (lambda (symbol) + (and (get symbol elp-timer-info-property) + (throw 'in-progress t))))) + (message "Stop profiling and display results...") + (elp-results) + (elp-restore-all)) + (t + (message "Start profiling Magit and Forge...") + (elp-reset-all) + (elp-instrument-package "magit-") + (elp-instrument-package "forge-")))) + +;;; Save File-Visiting Buffers + +(defvar magit--disable-save-buffers nil) + +(defun magit-pre-command-hook () + (setq magit--disable-save-buffers nil)) +(add-hook 'pre-command-hook #'magit-pre-command-hook) + +(defvar magit-after-save-refresh-buffers nil) + +(defun magit-after-save-refresh-buffers () + (dolist (buffer magit-after-save-refresh-buffers) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (magit-refresh-buffer)))) + (setq magit-after-save-refresh-buffers nil) + (remove-hook 'post-command-hook #'magit-after-save-refresh-buffers)) + +(defun magit-after-save-refresh-status () + "Refresh the status buffer of the current repository. + +This function is intended to be added to `after-save-hook'. + +If the status buffer does not exist or the file being visited in +the current buffer isn't inside the working tree of a repository, +then do nothing. + +Note that refreshing a Magit buffer is done by re-creating its +contents from scratch, which can be slow in large repositories. +If you are not satisfied with Magit's performance, then you +should obviously not add this function to that hook." + (when-let (((and (not magit--disable-save-buffers) + (magit-inside-worktree-p t))) + (buf (ignore-errors (magit-get-mode-buffer 'magit-status-mode)))) + (cl-pushnew buf magit-after-save-refresh-buffers) + (add-hook 'post-command-hook #'magit-after-save-refresh-buffers))) + +(defun magit-maybe-save-repository-buffers () + "Maybe save file-visiting buffers belonging to the current repository. +Do so if `magit-save-repository-buffers' is non-nil. You should +not remove this from any hooks, instead set that variable to nil +if you so desire." + (when (and magit-save-repository-buffers + (not magit--disable-save-buffers)) + (setq magit--disable-save-buffers t) + (let ((msg (current-message))) + (magit-save-repository-buffers + (eq magit-save-repository-buffers 'dontask)) + (when (and msg + (current-message) + (not (equal msg (current-message)))) + (message "%s" msg))))) + +(defvar-local magit-inhibit-refresh-save nil) + +(defun magit-save-repository-buffers (&optional arg) + "Save file-visiting buffers belonging to the current repository. +After any buffer where `buffer-save-without-query' is non-nil +is saved without asking, the user is asked about each modified +buffer which visits a file in the current repository. Optional +argument (the prefix) non-nil means save all with no questions." + (interactive "P") + (when-let ((topdir (magit-rev-parse-safe "--show-toplevel"))) + (let ((remote (file-remote-p default-directory)) + (save-some-buffers-action-alist + `((?Y (lambda (buffer) + (with-current-buffer buffer + (setq buffer-save-without-query t) + (save-buffer))) + "to save the current buffer and remember choice") + (?N (lambda (buffer) + (with-current-buffer buffer + (setq magit-inhibit-refresh-save t))) + "to skip the current buffer and remember choice") + ,@save-some-buffers-action-alist)) + (topdirs nil) + (unwiped nil) + (magit--wip-inhibit-autosave t)) + (unwind-protect + (save-some-buffers + arg + (lambda () + ;; If the current file is modified and resides inside + ;; a repository, and a let-binding is in effect, which + ;; places us in another repository, then this binding + ;; is needed to prevent that file from being saved. + (and-let* ((default-directory + (and buffer-file-name + (file-name-directory buffer-file-name)))) + (and + ;; Check whether the repository still exists. + (file-exists-p default-directory) + ;; Check whether refreshing is disabled. + (not magit-inhibit-refresh-save) + ;; Check whether the visited file is either on the + ;; same remote as the repository, or both are on + ;; the local system. + (equal (file-remote-p buffer-file-name) remote) + ;; Delayed checks that are more expensive for remote + ;; repositories, due to the required network access. + ;; + ;; Check whether the file is inside the repository. + (equal (or (cdr (assoc default-directory topdirs)) + (let ((top (magit-rev-parse-safe "--show-toplevel"))) + (push (cons default-directory top) topdirs) + top)) + topdir) + ;; Check whether the file is actually writable. + (file-writable-p buffer-file-name) + (prog1 t + ;; Schedule for wip commit, if appropriate. + (when magit-wip-after-save-local-mode + (push (expand-file-name buffer-file-name) unwiped))))))) + (when unwiped + (let ((default-directory topdir)) + (magit-wip-commit-worktree + (magit-wip-get-ref) + unwiped + (if (cdr unwiped) + (format "autosave %s files after save" (length unwiped)) + (format "autosave %s after save" + (file-relative-name (car unwiped))))))))))) + +;;; Restore Window Configuration + +(defvar magit-inhibit-save-previous-winconf nil) + +(defvar-local magit-previous-window-configuration nil) +(put 'magit-previous-window-configuration 'permanent-local t) + +(defun magit-save-window-configuration () + "Save the current window configuration. + +Later, when the buffer is buried, it may be restored by +`magit-restore-window-configuration'." + (if magit-inhibit-save-previous-winconf + (when (eq magit-inhibit-save-previous-winconf 'unset) + (setq magit-previous-window-configuration nil)) + (unless (get-buffer-window (current-buffer) (selected-frame)) + (setq magit-previous-window-configuration + (current-window-configuration))))) + +(defun magit-restore-window-configuration (&optional kill-buffer) + "Bury or kill the current buffer and restore previous window configuration." + (let ((winconf magit-previous-window-configuration) + (buffer (current-buffer)) + (frame (selected-frame))) + (quit-window kill-buffer (selected-window)) + (when (and winconf (equal frame (window-configuration-frame winconf))) + (set-window-configuration winconf) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (setq magit-previous-window-configuration nil))) + (set-buffer (with-selected-window (selected-window) + (current-buffer)))))) + +;;; Buffer History + +(defun magit-go-backward () + "Move backward in current buffer's history." + (interactive) + (if help-xref-stack + (help-xref-go-back (current-buffer)) + (user-error "No previous entry in buffer's history"))) + +(defun magit-go-forward () + "Move forward in current buffer's history." + (interactive) + (if help-xref-forward-stack + (help-xref-go-forward (current-buffer)) + (user-error "No next entry in buffer's history"))) + +(defun magit-insert-xref-buttons () + "Insert xref buttons." + (when (and (not magit-buffer-locked-p) + (or help-xref-stack help-xref-forward-stack)) + (when help-xref-stack + (magit-xref-insert-button help-back-label 'magit-xref-backward)) + (when help-xref-forward-stack + (when help-xref-stack + (insert " ")) + (magit-xref-insert-button help-forward-label 'magit-xref-forward)))) + +(defun magit-xref-insert-button (label type) + (magit-insert-section (button label) + (insert-text-button label 'type type + 'help-args (list (current-buffer))))) + +(define-button-type 'magit-xref-backward + :supertype 'help-back + 'mouse-face 'magit-section-highlight + 'help-echo (purecopy "mouse-2, RET: go back to previous history entry")) + +(define-button-type 'magit-xref-forward + :supertype 'help-forward + 'mouse-face 'magit-section-highlight + 'help-echo (purecopy "mouse-2, RET: go back to next history entry")) + +(defvar magit-xref-modes + ;; Do not function-quote to avoid circular dependencies. + '(magit-log-mode + magit-reflog-mode + magit-diff-mode + magit-revision-mode) + "List of modes for which to insert navigation buttons.") + +(defun magit-xref-setup (fn args) + (when (memq major-mode magit-xref-modes) + (when help-xref-stack-item + (push (cons (point) help-xref-stack-item) help-xref-stack) + (setq help-xref-forward-stack nil)) + (when-let ((tail (nthcdr 30 help-xref-stack))) + (setcdr tail nil)) + (setq help-xref-stack-item + (list 'magit-xref-restore fn default-directory args)))) + +(defun magit-xref-restore (fn dir args) + (setq default-directory dir) + (funcall fn major-mode nil args) + (magit-refresh-buffer)) + +;;; Repository-Local Cache + +(defvar magit-repository-local-cache nil + "Alist mapping `magit-toplevel' paths to alists of key/value pairs.") + +(defun magit-repository-local-repository () + "Return the key for the current repository." + (or (bound-and-true-p magit--default-directory) + (magit-toplevel))) + +(defun magit-repository-local-set (key value &optional repository) + "Set the repository-local VALUE for KEY. + +Unless specified, REPOSITORY is the current buffer's repository. + +If REPOSITORY is nil (meaning there is no current repository), +then the value is not cached, and we return nil." + (let* ((repokey (or repository (magit-repository-local-repository))) + (cache (assoc repokey magit-repository-local-cache))) + ;; Don't cache values for a nil REPOSITORY, as the 'set' and 'get' + ;; calls for some KEY may happen in unrelated contexts. + (when repokey + (if cache + (let ((keyvalue (assoc key (cdr cache)))) + (if keyvalue + ;; Update pre-existing value for key. + (setcdr keyvalue value) + ;; No such key in repository-local cache. + (push (cons key value) (cdr cache)))) + ;; No cache for this repository. + (push (cons repokey (list (cons key value))) + magit-repository-local-cache))))) + +(defun magit-repository-local-exists-p (key &optional repository) + "Non-nil when a repository-local value exists for KEY. + +Return a (KEY . VALUE) cons cell. + +The KEY is matched using `equal'. + +Unless specified, REPOSITORY is the current buffer's repository." + (and-let* ((cache (assoc (or repository + (magit-repository-local-repository)) + magit-repository-local-cache))) + (assoc key (cdr cache)))) + +(defun magit-repository-local-get (key &optional default repository) + "Return the repository-local value for KEY. + +Return DEFAULT if no value for KEY exists. + +The KEY is matched using `equal'. + +Unless specified, REPOSITORY is the current buffer's repository." + (if-let ((keyvalue (magit-repository-local-exists-p key repository))) + (cdr keyvalue) + default)) + +(defun magit-repository-local-delete (key &optional repository) + "Delete the repository-local value for KEY. + +Unless specified, REPOSITORY is the current buffer's repository. +If REPOSITORY is `all', then delete the value for KEY for all +repositories." + (if (eq repository 'all) + (dolist (cache magit-repository-local-cache) + (setf cache (compat-call assoc-delete-all key cache))) + (when-let ((cache (assoc (or repository + (magit-repository-local-repository)) + magit-repository-local-cache))) + (setf cache (compat-call assoc-delete-all key cache))))) + +(defmacro magit--with-repository-local-cache (key &rest body) + (declare (indent 1) (debug (form body))) + (let ((k (gensym))) + `(let ((,k ,key)) + (if-let ((kv (magit-repository-local-exists-p ,k))) + (cdr kv) + (let ((v ,(macroexp-progn body))) + (magit-repository-local-set ,k v) + v))))) + +(defun magit-preserve-section-visibility-cache () + (when (derived-mode-p 'magit-status-mode 'magit-refs-mode) + (magit-repository-local-set + (cons major-mode 'magit-section-visibility-cache) + magit-section-visibility-cache))) + +(defun magit-restore-section-visibility-cache (mode) + (setq magit-section-visibility-cache + (magit-repository-local-get + (cons mode 'magit-section-visibility-cache)))) + +(defun magit-zap-caches (&optional all) + "Zap caches for the current repository. + +Remove the repository's entry from `magit-repository-local-cache', +remove the host's entry from `magit--host-git-version-cache', and +set `magit-section-visibility-cache' to nil for all Magit buffers +of the repository. + +With a prefix argument or if optional ALL is non-nil, discard the +mentioned caches completely." + (interactive) + (cond (all + (setq magit-repository-local-cache nil) + (setq magit--host-git-version-cache nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (derived-mode-p 'magit-mode) + (setq magit-section-visibility-cache nil))))) + (t + (magit-with-toplevel + (setq magit-repository-local-cache + (cl-delete default-directory + magit-repository-local-cache + :key #'car :test #'equal)) + (setq magit--host-git-version-cache + (cl-delete (file-remote-p default-directory) + magit--host-git-version-cache + :key #'car :test #'equal))) + (dolist (buffer (magit-mode-get-buffers)) + (with-current-buffer buffer + (setq magit-section-visibility-cache nil)))))) + +;;; Utilities + +(defun magit-toggle-verbose-refresh () + "Toggle whether Magit refreshes buffers verbosely. +Enabling this helps figuring out which sections are bottlenecks. +The additional output can be found in the *Messages* buffer." + (interactive) + (setq magit-refresh-verbose (not magit-refresh-verbose)) + (message "%s verbose refreshing" + (if magit-refresh-verbose "Enabled" "Disabled"))) + +(defun magit-run-hook-with-benchmark (hook) + (cond + ((not hook)) + (magit-refresh-verbose + (message "Running %s..." hook) + (message "Running %s...done (%.3fs)" hook + (benchmark-elapse + (run-hook-wrapped + hook + (lambda (fn) + (message " %-50s %f" fn (benchmark-elapse (funcall fn)))))))) + ((run-hooks hook)))) + +(defun magit-file-region-line-numbers () + "Return the bounds of the region as line numbers. +The returned value has the form (BEGINNING-LINE END-LINE). If +the region end at the beginning of a line, do not include that +line. Avoid including the line after the end of the file." + (and (or magit-buffer-file-name buffer-file-name) + (region-active-p) + (not (= (region-beginning) (region-end) (1+ (buffer-size)))) + (let ((beg (region-beginning)) + (end (min (region-end) (buffer-size)))) + (list (line-number-at-pos beg t) + (line-number-at-pos (if (= (magit--bol-position end) end) + (max beg (1- end)) + end) + t))))) + +;;; _ +(provide 'magit-mode) +;;; magit-mode.el ends here |