Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve return values of some actions + some improvements #3352

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

dmaluka
Copy link
Collaborator

@dmaluka dmaluka commented Jun 15, 2024

Currently most actions always return true, regardless of whether they succeeded to do their job or not. This means that for most actions the user cannot really take advantage of action chaining with | or &.

So, to facilitate efficient usage of action chaining, improve return values of some actions, namely:

  • Undo
  • Redo
  • Deselect
  • UnhighlightSearch
  • ClearInfo
  • ClearStatus
  • NextTab
  • PreviousTab
  • NextSplit
  • PreviousSplit
  • RemoveMultiCursor
  • RemoveAllMultiCursors

The rules are straightforward: Undo returns false if there is nothing to undo, Deselect return false if there is nothing selected, NextTab returns false if there is just one tab, and so on.

Additionally:

  • Some fixes and improvements in the behavior of RemoveMultiCursor, RemoveAllMultiCursors and SkipMultiCursor actions.
  • Removed ClearStatus action as redundant (since ClearInfo does the same things). I've changed my mind and restored it (compatibility concerns) but documented that it is an alias for ClearInfo.
  • Refactored action execution code, to make it both simpler and more correct.

Return false if there is nothing to undo/redo.

This also fixes false "Undid action" and "Redid actions" infobar
messages in the case when no action was actually undone or redone.
@dmaluka
Copy link
Collaborator Author

dmaluka commented Jun 15, 2024

Besides the actions improved in this PR, there are also lots of cursor movement and selection actions (CursorUp, SelectWordLeft and so on) which also always return false and could be similarly improved. For example, CursorUp could return false if the cursor is already at the first line of the buffer.

But that is more complicated and debatable. For instance, CursorUp actually not just moves the cursor up one line, it also does the following:

  • If the cursor is already at the first line, move the cursor to the beginning of the first line.
  • If the cursor is already at the beginning of the first line, reset its LastVisualX value to 0 (which affects where will the next CursorDown move).

All this makes it not obvious what should be its return value in what cases.

So leave those actions as they are for now, to avoid complicating things prematurely.

ClearInfo and ClearStatus actions do exactly the same thing. Let's keep
them both, for compatibility reasons (who knows how many users are using
either of the two), but at least document that there is no difference
between the two.
When there is no selection (i.e. selection is empty), SkipMultiCursor
searches for the empty text, "finds" it as the beginning of the buffer,
and as a result, jumps to the beginning of the buffer, which confuses
the user. Fix it.
If the original selection was not done by the user manually but as a
result of the initial SpawnMultiCursor, deselect this original selection
if we execute RemoveMultiCursor and there is no multicursor to remove
(i.e. the original spawned cursor is the only one). This improves user
experience by making RemoveMultiCursor behavior nicely symmetrical to
SpawnMultiCursor.
Use Deselect() in order to place the cursor at the beginning of the
selection, not at the end of it, and to refresh its LastVisualX.
- SpawnMultiCursor and RemoveMultiCursor actions change the set of
  cursors, so we cannot assume that it stays the same. So refresh the
  `cursors` list after executing every action in the chain.

- If execAction() did not execute an action since it is not a
  multicursor, it should return true, not false, to not prevent
  executing next actions in the chain.
Instead of calling execAction() and then letting it check whether it
should actually execute this action, do this check before calling
execAction(), to make the code clear and straightforward.

Precisely: for multicursor actions, call execAction() in a loop for
every cursor, but for non-multicursor actions, call execAction() just
once, without a loop. This, in particular, allows to get rid of the
hacky "c == nil" check, since we no longer iterate a slice that may
change in the meantime (since SpawnMultiCursor and RemoveMultiCursor
are non-multicursor actions and thus are no longer executed while
iterating the slice).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing refactoring! It is way clearer now what the code is suppose to do. And you squashed a bug along the way. In the original implementation the correct cursors reference gets lost after switching to a different BufPane.

@@ -150,29 +150,31 @@ func BufMapEvent(k Event, action string) {
actionfns = append(actionfns, afn)
}
bufAction := func(h *BufPane, te *tcell.EventMouse) bool {
cursors := h.Buf.GetCursors()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cursors assignment would need to be moved 2 lines down into the actions loop. Otherwise you would still use the cursors of the original pane even though you switched to a new one in a next iteration. Line 170 updates the reference to h. 👍

@masmu
Copy link
Contributor

masmu commented Jul 4, 2024

I verified all the following methods to work correctly.
- Undo
- Redo
- Deselect
- UnhighlightSearch
- ClearInfo
- ClearStatus
- NextTab
- PreviousTab
- NextSplit
- PreviousSplit
- RemoveMultiCursor
- RemoveAllMultiCursors
- SkipMultiCursor

I noticed that there is no counterpart for SkipMultiCursor. And since this action just changes selections (not text) this cannot be undone via Undo.

Not sure if this is the right time, but I would suggest a change for NextTab, PreviousTab, NextSplit and PreviousSplit.
Currently those methods are topologically designed as rings. So once you reached the last (lets say a) tab, the next action gets you back to the first tab and vice versa.

My suggestion is change this topologically to a line for all mentioned methods. So e.g. in case you want to switch to the next tab, but you are already on it, return false.

After adding the following new actions:

// FirstTab switches to the first tab in the tab list
func (h *BufPane) FirstTab() bool {
    if Tabs.Active() == 0 {
        return false
    }
    Tabs.SetActive(0)
    return true
}

// LastTab switches to the last tab in the tab list
func (h *BufPane) LastTab() bool {
    lastTabIndex := len(Tabs.List) - 1
    if Tabs.Active() == lastTabIndex {
        return false
    }
    Tabs.SetActive(lastTabIndex)
    return true
}

// FirstSplit changes the view to the first split
func (h *BufPane) FirstSplit() bool {
    if h.tab.active == 0 {
        return false
    }
    h.tab.SetActive(0)
    return true
}

// LastSplit changes the view to the last split
func (h *BufPane) LastSplit() bool {
    lastTabIndex := len(h.tab.Panes) - 1
    if h.tab.active == lastTabIndex {
        return false
    }
    h.tab.SetActive(lastTabIndex)
    return true
}

you can restore the old behavior by the following settings:

    // this would be my recommendation for the defaults
    "Alt-,": "PreviousTab|LastTab",
    "Alt-.": "NextTab|FirstTab",

    // just for the sake of completeness
    "CtrlPageUp":     "PreviousTab|LastTab",
    "CtrlPageDown":   "NextTab|FirstTab",
    "Ctrl-w":         "NextSplit|FirstSplit",

But why breaking those actions into smaller actions in the first place?
It adds a lot of flexibility for the user. You can use combinations of splits and tabs, like:

    "Alt-,": "PreviousSplit|PreviousTab|LastTab",
    "Alt-.": "NextSplit|NextTab|FirstTab",

Is this a breaking change? Potentially yes, but IMHO a small one. It only affects people having bindings for NextTab, PreviousTab, NextSplit and PreviousSplit in their own bindings.json.

@dmaluka
Copy link
Collaborator Author

dmaluka commented Jul 7, 2024

I noticed that there is no counterpart for SkipMultiCursor. And since this action just changes selections (not text) this cannot be undone via Undo.

Yeah, it might be good to add SkipMultiCursorBack or something like that.

Not sure if this is the right time, but I would suggest a change for NextTab, PreviousTab, NextSplit and PreviousSplit.

[...]

It adds a lot of flexibility for the user. You can use combinations of splits and tabs, like:

[...]

Is this a breaking change? Potentially yes, but IMHO a small one. It only affects people having bindings for NextTab, PreviousTab, NextSplit and PreviousSplit in their own bindings.json.

Yeah, sounds good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants