Commit 115041d4 authored by Jonas Bernoulli's avatar Jonas Bernoulli

Support one-section magit-selections

For a long time Magit supported selecting two or more sibling sections
using the region and then acting on that selection instead of only on
the current section.  Single-section selections were not supported and
a region that did not span multiple siblings was not visualized as a
selection, instead the underlying region was visualized as it is in
non-magit buffers.

The change users will notice first is that when they press C-SPC to
set the beginning of the region/magit-selection on a section heading,
then the magit-selection is visualized.  Previously not even the
region was shown after just pressing C-SPC, because the mark and the
point were identical, so there was no non-empty region to visualize.

For the time being most commands continue to behave exactly as before
when there is a one-section selection: the act on the current section
instead.  While a one-section selection is a selection that contains
nothing but the current section, this difference between the current
section and a set consisting of only the current section is still
relevant.  It affects whether and how commands ask for confirmation
and/or offer the user to act on something else instead.

The reason I decided against supporting one-section selections in the
past is that it is a bit unfortunate to visualize the selection and
then the invoked command does not actually use it.  But this is no
different from a multi-section selection being visualized and then
invoking a command that isn't magit-selection aware at all.  Or from
having the region visualized in any Emacs buffer and then invoking any
command that doesn't behave differently when the region is active.

Beginning with this commit only a handful of commands begin using a
one-section selection.  Most importantly, and this is what motivated
this change, it is now possible to use `magit-branch-spinoff' after
selecting just HEAD to create a new branch rewinding the previously
current branch by a single commit.  Previously it was only possible
to rewind it to its upstream or to rewind it by at least two commits.

The other commands that respect one-section selections are:
- magit-am-apply-patches
- magit-cherry-apply
- magit-cherry-pick
- magit-revert
- magit-revert-no-commit
- magit-stash-drop
- magit-tag-delete

The recently added commands `magit-previous-line' and
`magit-next-line' now forgo moving on the first shift-selection move
when point is on a section heading, not just when inside a hunk body.

Closes #2920.
Closes #3026.
parent edbab840
......@@ -7,11 +7,12 @@ Changes since v2.11.0
* Added new commands `magit-previous-line' and `magit-next-line' as
substitutes for `previous-line' and `next-line'. Magit's selection
mechanism is based on the region but selects an area that is larger
than the region. This causes shift selection to select two lines on
the first invocation when using the vanilla commands. When invoked
inside the body of a hunk the new Magit-specific variants don't move
point on the first invocation and thereby they only selects a single
line. Which inconsistency you prefer is a matter of preference. #2912
than the region. This causes shift-selection to select two lines on
the first invocation when using the vanilla commands. On section
headings and inside hunk bodies the new magit-specific variants
don't move point on the first invocation and thereby they only
select a single section or line. Which inconsistency you prefer
is a matter of preference. #2912
To use the Magit-specific variants add this to your init file:
......@@ -110,6 +111,20 @@ Changes since v2.11.0
`create-branch' variant and the branch exists, then it offers to
simply checking it out instead of resetting it first. #3009
* For a long time Magit has supported selecting two or more sibling
sections using the region and then acting on that selection instead
of only on the current section. Single-section selections were not
supported and a region that did not span multiple siblings was not
visualized as a selection. Not every section-aware command was
adjusted to take single-section selections into account because in
many cases that would have lead to undesired changes in behavior.
#3026
* The command `magit-branch-spinoff' now spins off just HEAD, when
that constitutes the single-section selection. Previously one could
only spin off all commits that aren't in the upstream yet or at least
two commits. #2920
Fixes since v2.11.0
-------------------
......
......@@ -3693,8 +3693,7 @@ features are available from separate popups.
upstream. Interactively, FROM is only ever non-nil, if the
region selects some commits, and among those commits, FROM is
the commit that is the fewest commits ahead of the source
branch. (It not yet possible to spin off a single commit,
unless it is the only unpushed commit. See #2920.)
branch.
The commit at the other end of the selection actually does not
matter, all commits between FROM and ~HEAD~ are moved to the new
......
......@@ -175,7 +175,7 @@ so causes the change to be applied to the index as well."
(magit-refresh))))
(defun magit-apply--get-selection ()
(or (magit-region-sections 'hunk 'file)
(or (magit-region-sections '(hunk file) t)
(let ((section (magit-current-section)))
(pcase (magit-section-type section)
((or `hunk `file) section)
......@@ -208,7 +208,7 @@ at point, stage the file but not its content."
(`(unstaged hunk) (magit-apply-hunk it "--cached"))
(`(unstaged hunks) (magit-apply-hunks it "--cached"))
(`(unstaged file) (magit-stage-1 "-u" (list (magit-section-value it))))
(`(unstaged files) (magit-stage-1 "-u" (magit-region-values)))
(`(unstaged files) (magit-stage-1 "-u" (magit-region-values nil t)))
(`(unstaged list) (magit-stage-1 "-u"))
(`(staged ,_) (user-error "Already staged"))
(`(committed ,_) (user-error "Cannot stage committed changes"))
......@@ -260,7 +260,7 @@ ignored) files.
(let* ((section (magit-current-section))
(files (pcase (magit-diff-scope)
(`file (list (magit-section-value section)))
(`files (magit-region-values))
(`files (magit-region-values nil t))
(`list (magit-untracked-files))))
plain repos)
(dolist (file files)
......@@ -301,7 +301,7 @@ ignored) files.
(`(staged hunk) (magit-apply-hunk it "--reverse" "--cached"))
(`(staged hunks) (magit-apply-hunks it "--reverse" "--cached"))
(`(staged file) (magit-unstage-1 (list (magit-section-value it))))
(`(staged files) (magit-unstage-1 (magit-region-values)))
(`(staged files) (magit-unstage-1 (magit-region-values nil t)))
(`(staged list) (magit-unstage-all))
(`(committed ,_) (if magit-unstage-committed
(magit-reverse-in-index)
......
......@@ -304,8 +304,7 @@ to `FROM~', instead of to the last commit it shares with its
upstream. Interactively, FROM is only ever non-nil, if the
region selects some commits, and among those commits, FROM is
the commit that is the fewest commits ahead of the source
branch. (It not yet possible to spin off a single commit,
unless it is the only unpushed commit. See #2920.)
branch.
The commit at the other end of the selection actually does not
matter, all commits between FROM and `HEAD' are moved to the new
......@@ -390,7 +389,7 @@ defaulting to the branch at point."
;; a bit of extra functionality into this one. And once it's there,
;; you cannot remove it anymore. (I tried, it causes protests.)
(interactive
(let ((branches (magit-region-values 'branch))
(let ((branches (magit-region-values 'branch t))
(force current-prefix-arg))
(if (if (> (length branches) 1)
(magit-confirm t nil "Delete %i branches" branches)
......
......@@ -729,7 +729,7 @@ The information can be in three forms:
If no DWIM context is found, nil is returned."
(cond
((--when-let (magit-region-values 'commit 'branch)
((--when-let (magit-region-values '(commit branch) t)
(deactivate-mark)
(concat (car (last it)) ".." (car it))))
(magit-buffer-refname
......@@ -774,7 +774,7 @@ If no DWIM context is found, nil is returned."
If MBASE is non-nil, prompt for which rev to place at the end of
a \"revA...revB\" range. Otherwise, always construct
\"revA..revB\" range."
(--if-let (magit-region-values 'commit 'branch)
(--if-let (magit-region-values '(commit branch) t)
(let ((revA (car (last it)))
(revB (car it)))
(deactivate-mark)
......@@ -2124,7 +2124,7 @@ is `region'. If the region is empty after a mouse click, then
If optional STRICT is non-nil, then return nil if the diff type of
the section at point is `untracked' or the section at point is not
actually a `diff' but a `diffstat' section."
(let ((siblings (and (not ssection) (magit-region-sections))))
(let ((siblings (and (not ssection) (magit-region-sections nil t))))
(setq section (or section (car siblings) (magit-current-section)))
(when (and section
(or (not strict)
......
......@@ -190,12 +190,13 @@ is a matter of preference."
"use `forward-line' with negative argument instead."))
(interactive "p\np")
(unless arg (setq arg 1))
(let ((hunkp (magit-diff-inside-hunk-body-p)))
(if (and hunkp (= arg 1) (magit--turn-on-shift-select-mode-p))
(let ((stay (or (magit-diff-inside-hunk-body-p)
(magit-section-position-in-heading-p))))
(if (and stay (= arg 1) (magit--turn-on-shift-select-mode-p))
(push-mark nil nil t)
(with-no-warnings
(handle-shift-selection)
(previous-line (if hunkp (max (1- arg) 1) arg) try-vscroll)))))
(previous-line (if stay (max (1- arg) 1) arg) try-vscroll)))))
;;;###autoload
(defun magit-next-line (&optional arg try-vscroll)
......@@ -211,12 +212,13 @@ prefer is a matter of preference."
(declare (interactive-only forward-line))
(interactive "p\np")
(unless arg (setq arg 1))
(let ((hunkp (magit-diff-inside-hunk-body-p)))
(if (and hunkp (= arg 1) (magit--turn-on-shift-select-mode-p))
(let ((stay (or (magit-diff-inside-hunk-body-p)
(magit-section-position-in-heading-p))))
(if (and stay (= arg 1) (magit--turn-on-shift-select-mode-p))
(push-mark nil nil t)
(with-no-warnings
(handle-shift-selection)
(next-line (if hunkp (max (1- arg) 1) arg) try-vscroll)))))
(next-line (if stay (max (1- arg) 1) arg) try-vscroll)))))
;;; Clean
......
......@@ -371,7 +371,7 @@ If FILE isn't tracked in Git, fallback to using `rename-file'."
With a prefix argument FORCE do so even when the files have
staged as well as unstaged changes."
(interactive (list (or (--if-let (magit-region-values 'file)
(interactive (list (or (--if-let (magit-region-values 'file t)
(progn
(or (magit-file-tracked-p (car it))
(user-error "Already untracked"))
......@@ -387,7 +387,7 @@ staged as well as unstaged changes."
With a prefix argument FORCE do so even when the files have
uncommitted changes. When the files aren't being tracked in
Git, then fallback to using `delete-file'."
(interactive (list (--if-let (magit-region-values 'file)
(interactive (list (--if-let (magit-region-values 'file t)
(or (magit-confirm-files 'delete it "Delete")
(user-error "Abort"))
(list (magit-read-file "Delete file")))
......
......@@ -1517,7 +1517,7 @@ Return a list of two integers: (A>B B>A)."
(defun magit-read-range-or-commit (prompt &optional secondary-default)
(magit-read-range
prompt
(or (--when-let (magit-region-values 'commit 'branch)
(or (--when-let (magit-region-values '(commit branch) t)
(deactivate-mark)
(concat (car (last it)) ".." (car it)))
(magit-branch-or-commit-at-point)
......
......@@ -891,7 +891,7 @@ changes introduced by that commit (unlike 'git format-patch'
which creates patches for all commits that are reachable from
`HEAD' but not from the specified commit)."
(interactive
(list (-if-let (revs (magit-region-values 'commit))
(list (-if-let (revs (magit-region-values 'commit t))
(concat (car (last revs)) "^.." (car revs))
(let ((range (magit-read-range-or-commit "Format range or commit")))
(if (string-match-p "\\.\\." range)
......
......@@ -898,7 +898,8 @@ This function is necessary to ensure that a representation of
such a region is visible. If neither of these functions were
part of the hook variable, then such a region would be
invisible."
(when selection
(when (and selection
(not (and (eq this-command 'mouse-drag-region))))
(--each selection
(magit-section-make-overlay (magit-section-start it)
(or (magit-section-content it)
......@@ -1045,7 +1046,7 @@ excluding SECTION itself."
(`next (cdr (member section siblings)))
(_ (remq section siblings))))))
(defun magit-region-values (&rest types)
(defun magit-region-values (&optional types multiple)
"Return a list of the values of the selected sections.
Also see `magit-region-sections' whose doc-string explains when a
......@@ -1054,18 +1055,21 @@ or is not a valid section selection, then return nil. If optional
TYPES is non-nil then the selection not only has to be valid; the
types of all selected sections additionally have to match one of
TYPES, or nil is returned."
(mapcar 'magit-section-value (apply 'magit-region-sections types)))
(mapcar #'magit-section-value (magit-region-sections types multiple)))
(defun magit-region-sections (&rest types)
(defun magit-region-sections (&optional types multiple)
"Return a list of the selected sections.
When the region is active and constitutes a valid section
selection, then return a list of all selected sections. This is
the case when the region begins in the heading of a section and
ends in the heading of a sibling of that first section. When
the selection is not valid then return nil. Most commands that
can act on the selected sections, then instead just act on the
current section, the one point is in.
ends in the heading of the same section or in that of a sibling
section. If optional MULTIPLE is non-nil, then the region cannot
begin and end in the same section.
When the selection is not valid, then return nil. In this case,
most commands that can act on the selected sections will instead
act on the section at point.
When the region looks like it would in any other buffer then
the selection is invalid. When the selection is valid then the
......@@ -1075,15 +1079,19 @@ here if the region looks like it usually does, then that's not
a valid selection as far as this function is concerned.
If optional TYPES is non-nil, then the selection not only has to
be valid; the types of all selected sections additionally have to
match one of TYPES, or nil is returned."
(when (use-region-p)
be valid; the types of all selected sections additionally have
to match one of TYPES, or nil is returned. TYPES can also be a
single type, instead of a list of types."
(when (region-active-p)
(let* ((rbeg (region-beginning))
(rend (region-end))
(sbeg (get-text-property rbeg 'magit-section))
(send (get-text-property rend 'magit-section)))
(unless (memq send (list sbeg magit-root-section nil))
(let ((siblings (magit-section-siblings sbeg 'next)) sections)
(when (and send
(not (eq send magit-root-section))
(not (and multiple (eq send sbeg))))
(let ((siblings (cons sbeg (magit-section-siblings sbeg 'next)))
sections)
(when (and (memq send siblings)
(magit-section-position-in-heading-p sbeg rbeg)
(magit-section-position-in-heading-p send rend))
......@@ -1091,14 +1099,23 @@ match one of TYPES, or nil is returned."
(push (car siblings) sections)
(when (eq (pop siblings) send)
(setq siblings nil)))
(setq sections (cons sbeg (nreverse sections)))
(setq sections (nreverse sections))
(when (and types (symbolp types))
(setq types (list types)))
(when (or (not types)
(--all-p (memq (magit-section-type it) types) sections))
sections)))))))
(defun magit-section-position-in-heading-p (section pos)
"Return t if POSITION is inside the heading of SECTION."
(and (>= pos (magit-section-start section))
(defun magit-section-position-in-heading-p (&optional section pos)
"Return t if POSITION is inside the heading of SECTION.
POSITION defaults to point and SECTION defaults to the
current section."
(unless section
(setq section (magit-current-section)))
(unless pos
(setq pos (point)))
(and section
(>= pos (magit-section-start section))
(< pos (or (magit-section-content section)
(magit-section-end section)))
t))
......
......@@ -217,10 +217,11 @@ and forgo removing the stash."
(defun magit-stash-drop (stash)
"Remove a stash from the stash list.
When the region is active offer to drop all contained stashes."
(interactive (list (--if-let (magit-region-values 'stash)
(or (magit-confirm t nil "Drop %i stashes" it)
(user-error "Abort"))
(magit-read-stash "Drop stash"))))
(interactive
(list (--if-let (magit-region-values 'stash)
(or (magit-confirm t "Drop %s" "Drop %i stashes" it)
(user-error "Abort"))
(magit-read-stash "Drop stash"))))
(dolist (stash (if (listp stash)
(nreverse (prog1 stash (setq stash (car stash))))
(list stash)))
......
......@@ -457,10 +457,11 @@ If the region marks multiple tags (and nothing else), then offer
to delete those, otherwise prompt for a single tag to be deleted,
defaulting to the tag at point.
\n(git tag -d TAGS)"
(interactive (list (--if-let (magit-region-values 'tag)
(or (magit-confirm t nil "Delete %i tags" it)
(user-error "Abort"))
(magit-read-tag "Delete tag" t))))
(interactive
(list (--if-let (magit-region-values 'tag)
(or (magit-confirm t "Delete %s" "Delete %i tags" it)
(user-error "Abort"))
(magit-read-tag "Delete tag" t))))
(magit-run-git "tag" "-d" tags))
(defun magit-tag-prune (tags remote-tags remote)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment