Sublime Forum

Plugin Soft Undo

#1

In Short:

Is there any way to force Sublime Text to add individual events performed in a TextCommand plugin to its soft-undo buffer?

In Long:

I’ve written a plugin which clears multiple selections leaving only one selection, either the top or bottom one.

Most of the time I want the selections totally cleared leaving just a cursor at the sel.b position (i.e. where the actual cursor is) of the top or bottom selection but sometimes I want any selected text in the top or bottom selection to remain intact.

The plugin clears all the selections before adding just the one that is wanted so I had the idea of using soft-undo so that I can easily get to what I want. I added code to clear the selections, add the region of the top or bottom selection intact, assuming sel.size() > 0, then to clear the selections again before finally adding the sel.b position to add just the cursor. That way if I want the selected text I could just hit the soft-undo keys. i.e.

bottom_sel = sels[sels_len - 1]
sels.clear()
sels.add(bottom_sel)
sels.clear()
sels.add(sublime.Region(bottom_sel.b, bottom_sel.b))

Unfortunately when I hit the soft-undo keys Sublime Text just undoes everything my plugin did in one go.

So I thought I’d use a dedicated TextCommand, like this:

bottom_sel = sels[sels_len - 1]
if bottom_sel.a == bottom_sel.b:
    self.view.run_command("multiple_selection_clear_add_sel",
                {"sel_a": bottom_sel.a, "sel_b": bottom_sel.b})
else:
    # Selection text intact:
    self.view.run_command("multiple_selection_clear_add_sel",
                {"sel_a": bottom_sel.a, "sel_b": bottom_sel.b})

    # Selection with cursor at the non-cursor end:
    self.view.run_command("multiple_selection_clear_add_sel",
                {"sel_a": bottom_sel.a, "sel_b": bottom_sel.a})

    # Selection with cursor at the cursor end:
    self.view.run_command("multiple_selection_clear_add_sel",
                {"sel_a": bottom_sel.b, "sel_b": bottom_sel.b})

class MultipleSelectionClearAddSelCommand(sublime_plugin.TextCommand):
    def run(self, edit, sel_a, sel_b):
        sels = self.view.sel()
        sels.clear()
        sels.add(sublime.Region(sel_a, sel_b))

i.e. 1) Cursor is at the cursor end of bottom selection
     2) Hit the soft-undo keys
     3) Cursor is at the non-cursor end of bottom selection
     4) Hit the soft-undo keys
     5) Bottom selection (size > 0) with selected text intact.

I thought that by calling another TextCommand the individual states would be stored by Sublime Text and I could soft-undo through them one by one. Unfortunately Sublime Text just does a soft-undo of everything on one go and, as before, I end up with all the original selections intact.

Is there any way to force Sublime Text to add the individual events to its soft-undo buffer?

Thanks.

1 Like

#2

The edit token that a TextCommand receives is what’s being used to group edits together, so it would be my guess that the token is handed out for the “outer” or “parent” command and remains in effect until that command is finially finished, so that all edits made by the command will undo at once.

If that’s the case then logically calling multiple commands would probably cause them to either receive the same token as the parent did, or some other token that’s “grouped” with it somehow.

It may work to use set_timeout() with a timeout of 0 to chain your calls so that each is a distinct command invocation. Callbacks with an identical delay happen in the order they were added, so you could probably just throw several of them at once at the end of your command.

4 Likes

#3

@OdatNurd

EDIT:

Forget what I wrote before about the set_timeout() approach not working. I failed to define the callback as a lambda function. Once I did that the individual operations were all available with soft-undo.

Thanks v. much for your help.

2 Likes

#4

In case anyone wants to see the code which results in plugin actions being added to Sublime Text’s soft-undo buffer, here is is:

# The clear_to_selection() method is part of a TextCommand class,
# it is only called when there are multiple selections.

def clear_to_selection(self, sel):
    """
    Clears all the selections but the "sel" selection.

    By using the MultipleSelectionClearerAddSelCommand helper class
    to clear and add selections, in conjunction with set_timeout(),
    all the added regions will get added to ST's soft-undo buffer.
    After the plugin has been run the effect of this is:

    1) The cursor is placed at the cursor end of sel
       Press the soft-undo keys and...
    2) The cursor is placed at the non-cursor end of sel
       Press the soft-undo keys and...
    3) The sel selected text is restored
       Press the soft-undo keys and...
    4) All selections restored to pre-plugin state
    """

    # No selected text.
    if sel.size() == 0:
        self.view.sel().clear()
        self.view.sel().add(sel)
        return

    add_cmd = "multiple_selection_clearer_add_sel"
    sel_text_args = {"sel_a": sel.a, "sel_b": sel.b}
    cursor_a_args = {"sel_a": sel.a, "sel_b": sel.a}
    cursor_b_args = {"sel_a": sel.b, "sel_b": sel.b}
    cmd_sel_text = lambda: self.view.run_command(add_cmd, sel_text_args)
    cmd_cursor_a = lambda: self.view.run_command(add_cmd, cursor_a_args)
    cmd_cursor_b = lambda: self.view.run_command(add_cmd, cursor_b_args)
    # Items added with a zero timeout value will
    # be called in the order they are received.
    sublime.set_timeout(cmd_sel_text, 0)
    sublime.set_timeout(cmd_cursor_a, 0)
    sublime.set_timeout(cmd_cursor_b, 0)


class MultipleSelectionClearerAddSelCommand(sublime_plugin.TextCommand):
    """ Helper TextCommand to clear all selections and add a selection. """

    def run(self, edit, sel_a, sel_b):
        self.view.sel().clear()
        self.view.sel().add(sublime.Region(sel_a, sel_b))

I’ve added the whole plugin to this Github Gist but be warned the plugin has been specifically designed for me, so that I can use the same keys for it as with another plugin which I wrote called MoveTabInGroup, also uploaded to this Github Gist. Both plugins use the ctrl+shift+pageup/pagedown keys; if the number of selections is exactly one then the tab position is moved left or right, if there are zero or more than one selections then the multiple selections are cleared to the top or bottom one (to the middle visible line if zero selections).

2 Likes

#5

Thanks for the sublime.set_timeout() workaroud!

I just had almost the same problem. I was adding selections in one text command, i.e.,

  1. Call text command and add selection 1
  2. Call text command and add selection 2
  3. Call text command and add selection 3
  4. Call soft_undo, all my three selection were undone

I opened a bug report on the Core issue tracker for this:

  1. https://github.com/SublimeTextIssues/Core/issues/2924 Soft undo does not work for the same text command called multiple times

I encountered this problem while I was reimplementing the find_under_expand command fixing its bugs as:

  1. https://github.com/SublimeTextIssues/Core/issues/1113 find_under_expand_skip resets whole-word state of find_under_expand
  2. https://github.com/SublimeTextIssues/Core/issues/2091 Calling Ctrl+K, Ctrl+D to skip item, does not skip the item and selects wrong items
  3. https://github.com/SublimeTextIssues/Core/issues/1894 When I am focused on the output.exec panel, and press Ctrl+D, the find_under_expand, is performed on the unfocused view

You can find my new find_under_expand implementation on:

0 Likes