Skip to content

Range-select #3196

Closed
Closed
@jesseduffield

Description

@jesseduffield

Problem

Often you want to perform an action on multiple items in a list view, and the only way to do that is to navigate to each item in sequence and perform the action for each.

Example 1: Squashing a range of commits

A common example for me is having a bunch of WIP commits that I want to squash (fixup) into one. Currently the fastest way to do this is:

  1. Navigate to the parent commit to start an interactive rebase with 'e'
  2. move up the list, marking each commit to be squashed with 'up', 'f', 'up', 'f', etc
  3. continue the rebase.

(You can also do this without an explicit interactive rebase but I find it slower due to each action requiring its own rebase)

It would be much better if you could just select the range of commits first, and then perform the action i.e.:

  1. press 'v'
  2. move the cursor down to the last commit that you want to squash
  3. press 'f'
  4. confirm

I'm using the 'v' key for starting the range because that's what we're already doing in the staging view when selecting a range of lines, and I'm pretty sure we chose that key because it's a vim thing.

Example 2: Staging a range of files

This is especially painful on windows which has an extra penalty per git invocation. Same as the example with squashing commits, if you want to stage multiple files you need to press space, down, space, down, etc. Much easier to start a range with 'v', move the cursor to the last file you want to stage, and press space.

Example 3: Moving multiple commits

This is something that I do not need often, but I coincidentally needed it yesterday. With the other examples, selecting a range spares you from having to visit each item and perform an action on it, but in this case it's even more laborious without a multi-select feature. I had a heap of WIP commits with a couple of specific commits spread-out which I wanted to bring down to the base of my feature and squash down. I'd have liked to move down the top commit until it collided with the next one and then move the two in tandem to the bottom, but I instead had to go one-by-one.

Proposal

I propose that in any list view, you can press 'v' to start selecting a range of items, such that when you move your cursor, the items between the cursor and the start item are highlighted and are part of the selection. All that needs to be stored in state is the index of the start item.

Then, if you perform an action that supports multi-select, the action applies to the range. If you attempt to perform an action that does not support multi-select (e.g. 'create a new branch' is meaningless in the context of multi-select) you'll see an error. Each action will need to specify whether or not it supports multi select.

Each action that supports multi-select decides whether the selection is cleared by the action. Most actions should clear the selection, but there are exceptions like when moving multiple commits (it would be annoying to have to re-select the range of commits each time you move them down by one).

If you've got a selected range, pressing escape will clear the selection. The escape key is used for various things but clearing a selection should be the highest-priority before anything else. We should also do this for selecting a range of lines which currently only cancels the range if you press 'v' again. In all cases, pressing 'v' should clear the selection if one already exists.

The selection is also cleared if you leave the current view (unless you're going to a popup)

Contiguous items only?

A crucial part of this proposal is that multi-select will only be for contiguous items, so if you have items A,B,C,D,E in a list view, you can't select A,B,C, and E and then perform an action. You'll instead need to perform the action for A,B,C in one go and then for E separately. I'm open to changing the design to support non-contiguous multi-select but the reasons I want to stick with contiguous are:

  • UX complexity. For non-contiguous, you'd need a key for starting a range, stopping a range, adding an individual item to the selection
  • Code complexity: Moving a contiguous commits up/down is straightforward to implement, but what does it mean to move a non-contiguous set of commits up/down?
  • If we know selection ranges are always contiguous, we could easily use the feature for other things like selecting two commits to diff or selecting the bounds of a git bisect.

The main benefit of non-contiguous selection that comes to mind is being able to select files for staging, in a situation where it's expensive to do so. But I don't know if it justifies the complexity. There are already situations where non-contiguous multi-select is basically built in e.g. including files in a custom patch and copying commits to cherry-pick.

Multi-select in file trees

What does it mean when your selected range includes both files and directories, and you hit the space key to stage them? For a given directory in the range, should you stage all of it's files, or only the ones that happen to be included in your selection? Or should you just ignore directories and only deal with files? This doesn't matter so much staging because it's low-stakes, but it matters a lot more with discarding changes (if we want to support that).

Clear confirmations

When a range is selected, and a confirmation popup appears for some action, we'll need to be clear about which items are included in the range so that there's no surprises for the user. This is especially important for destructive actions.

Cherry-picking

The 'v' key is currently used in the commits view to paste copied commits (i.e. cherry-pick). It's nice having 'c' and 'v' for that because they correspond to copy/paste on our computers. Nonetheless, if we can't find a better key than 'v' for starting the multi-select, I propose we change the commit paste keybinding to ctrl+v. Obviously if we do this, we can't upgrade 'c' to ctrl+c because everybody expects ctrl+c to quit the application.

Funnily, it's always bugged me that we have two different ways of doing multi-select in the app now: it's the 'v' approach for selecting a range of lines to stage, but with cherry-picking in order to copy a range of commits you press shift+c to copy from the last copied commit to the currently selected commit. I much prefer the former approach so we can scrap the shift+c keybinding in favour of pressing 'v' to select the range and then 'c' to copy the commits.

Implementation

I haven't got the implementation worked out but some things that come to mind:

  • I'm pretty sure we've got gocui doing the highlighting of selected items, so we'll need to make it support selection ranges. We're using a hacky solution outside of gocui for highlighting ranges in the staging view and the hackiness has made integration testing harder
  • I'm not sure where to store the selection range state i.e. globally or on the context. I'm thinking on the context at the moment
  • Should we use the DisabledReason field on our keybindings to disable all actions that don't support multi-select when we're in multi-select mode? And if so, how do we do that without a tonne of boilerplate? An opt-in approach sounds cleaner.
  • I'm interested in exploring having completely separate handlers for multi-select vs single-select actions, where we define which is used at the keybinding level. This will likely spare us some inline conditional logic in the handlers, and it'll let us set different labels/tooltips depending on what mode we're in (e.g. 'fixup commit' vs 'fixup commits'). But I'm not fully sold on the idea.
  • We should make it so that custom commands do not work with multi-select. We can change this in the future but right now better to keep it simple

More use cases

I've gone through every action in the keybindings cheatsheet and worked out every place where we could use multi-select. I've categorised them into use cases I would actually use vs use cases of more dubious value.

Use cases I can see myself using

  • mark range of TODO commits as pick/edit/drop/squash/fixup
  • copy range of commits for cherry-pick
  • fixup/squash multiple commits (outside interactive rebase)
  • drop multiple commits (outside interactive rebase)
  • move multiple commits (in and out of interactive rebase)
  • adding a range of commit files to a custom patch
  • delete multiple commit files
  • discard multiple files in files view
  • stage multiple files

Use cases that are possible but less needed

Destructive actions lend themselves well to multi-select (I've included a couple above that I'd use). But for the following use cases, do we really need them?

  • dropping multiple stashes
  • delete multiple tags
  • delete multiple worktrees
  • delete multiple branches
  • delete multiple remote branches
  • delete multiple remotes
  • delete multiple submodules

Other actions:

  • revert a range of commits
  • checkout multiple commit files at once
  • copy to clipboard e.g. copying multiple SHAs to clipboard, newline separated
  • amend commit attribute (e.g. setting the author for a range of commits)
  • open selected commits in browser
  • ignore multiple files
  • open/edit multiple files
  • fast-forward multiple branches
  • fetch multiple remotes
  • initialize, update submodules
  • stash multiple files. Currently I stash multiple files by staging them and then doing a 'stash staged changes'. This feels like an abuse of the staging feature, however it does allow non-contiguous selections and it lets me include parts of a file to stash so I actually think multi-select won't be very useful in comparison.

Use cases that exploit multi-select for the sake of specifying two terminal items

  • diffing two commits. You could select a range of commits, then press enter, and see the commit files for the range of commits. I think this could be more ergonomic than having to explicitly mark the first commit before navigating to the next one (it wouldn't replace that feature), but it needs more thought.
  • start git bisect using range: more ergonomic than current approach of explicitly marking good and bad commit, though after the bisect is started, you're back to marking individual commits as good/bad anyway.

If more use cases along these lines come up we can consider them, but at this stage I think we should hold off on multi-select-for-terminals for now just because it doesn't bring much advantage over existing approaches.

Open questions

  • Should we support non-contiguous selections
  • Should we name this feature 'multi-select' or 'selection-ranges' or something else?
  • does 'v' still make sense as a key for this?
  • does ctrl+v make sense for pasting commits, given that we can't use the same key for both actions?
  • how do we specify which actions support multi-select?
  • how do we handle multi-select in file trees?

Conclusion

What do people think? Keen to get your feedback.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions