Search code examples
jsonrmacrossublimetext3sublimerepl

Making a sublime text 3 macro to evaluate a line and then move the cursor to the next line


My beloved gedit R plugin is no longer maintained, and the forced downgrade solution that I had been using suddenly stopped working last time I updated ubuntu. The writing has been on the wall for a while, and I'm exploring sublime text. I've installed REPL for sending code to the command line, and I'm trying to set up the keybindings like I'm used to them.

In particular, I'm trying to make it such that CTRL+SHIFT+R sends a line of code to the console, runs it, and then moves the cursor in the editor down to the next line. That way I could hit CTRL+SHIFT+R multiple times to run a script line by line. (This is similar to Rstudio's behavior with CTRL+Enter)

Googling, I found this (old) solution. It doesn't work, probably becuase it was written for sublime text 2. More googling, and I figured out how to make it almost work:

//This is a macro for evaluate and move down
[
 {"command": "repl_transfer_current", "args": {"scope": "lines"}}
// {"command": "move", "args": {"mode": "lines", "amount": 1}}
]

Here is what I add to my default keymap:

{ "keys": ["ctrl+shift+r"], "command": "run_macro_file", "args": {"file": "Packages/User/geditlike_lineeval.sublime-macro"}}

As it stands above, sublime text sends my line of code to the terminal and runs it. The second line is supposed to send the cursor down, but it doesn't work, and the macro fails when I uncomment it.

I can't seem to find much documentation for sublime text's commands. Here is the best I can find for move. Is my syntax wrong? How can I make this work?

BONUS: How can I make sublime text run the line, then skip down to the next non-empty, non-commented line. That seems harder -- I see that a lot of the keybindings involve regular expressions and such.

EDIT My problem was that I was missing the comma between the JSON lines. AND the code linked on that github page DOES work, if you add a comma between the lines.


Solution

  • The solution you're linking to should work in both Sublime 2 and 3 (assuming the plugin providing the repl_transfer_current command works for both), but isn't working because the macro is not properly formatted.

    [Edit] The move command provided natively by Sublime doesn't take the arguments your Macro is using. Presumably that's something that is also being provided by some package if this works for you. If so, you may need to adjust the sample code below accordingly. [/Edit]

    As it stands, the issue is that almost (but not all) configuration files in Sublime are in the JSON format (slightly relaxed to allow comments), and the macro code as outlined above and the linked solution isn't valid JSON because the first and second command in it aren't separated by a comma.

    Something like the following should work:

    [
       {"command": "repl_transfer_current", "args": {"scope": "lines"}},
       {"command": "move", "args": {"mode": "lines", "amount": 1}}
    ]
    

    I think the documentation for Sublime that you linked to above is for Sublime 2. A good resource is the Unofficial Documentation, which also contains a list of commands (plus a lot of other good stuff).

    In order to do something like this and have it keep moving downward until it gets to the first non-blank, non-comment line you would need a simple plugin

    Specifically it would have to move the cursor down (using the existing move command) and then examine the current line to see if it is blank or a comment, and if so move again. You could then use that command in place of the move command in your macro.


    For bonus marks, here's an example of a plugin that does something like this. It's more verbose than it needs to be so that it's more instructive, and may require additional tweaking (R is not one of the languages I use/know) but it should get you started.

    For more information on how this works, you can check out the API Reference to see all of the internal commands you can use in a plugin.

    To use this, select Tools > Developer > New Plugin... from the menu, then replace the entirety of the stub code displayed with the plugin code as presented here, and save it with a .py extension (name is unimportant):

    import sublime
    import sublime_plugin
    import re
    
    # A regex that matches a line that's blank or contains a comment.
    # Adjust as needed
    _r_blank = re.compile("^\s*(#.*)?$")
    
    class RAdvanceNextCommand(sublime_plugin.TextCommand):
        def run(self, edit):
            # Get the count of lines in the buffer so we know when to stop
            last_line = self.line_at(self.view.size())
    
            while True:
                # Move to the next line
                self.view.run_command("move", {"by": "lines", "forward": True})
    
                # Get the current cursor position in the file
                caret = self.view.sel()[0].begin()
    
                # Get the new current line number
                cur_line = self.line_at(caret)
    
                # Get the contents of the current line
                content = self.view.substr(self.view.line(caret))
    
                # If the current line is the last line, or the contents of
                # the current line does not match the regex, break out now.
                if cur_line == last_line or not _r_blank.match(content):
                    break
    
            # Jump to the start of the line
            self.view.run_command("move_to", {"to": "bol"})
    
        # Convert a 0 based offset into the file into a 0 based line in
        # the file.
        def line_at(self, point):
            return self.view.rowcol(point)[0]
    

    This implements a new command named r_advance_next which moves the cursor downwards through the file, skipping over lines that are wholly white space or contain a line comment (assuming my regex is up to snuff).

    With this in place, your macro would look like this:

    [
        {"command": "repl_transfer_current", "args": {"scope": "lines"}},
        {"command": "r_advance_next"}
    ]
    

    In addition, you can use a key binding such as the following. Since you mention RStudio using Control+Enter, that's what I used here. This binding has a context applied to it so that it only applies when the current file is an R file, so that it doesn't trigger when it's not appropriate.

    { "keys": ["ctrl+enter"], "command": "run_macro_file",
      "args": {"file": "Packages/User/geditlike_lineeval.sublime-macro"},
      "context": [
        { "key": "selector", "operator": "equal", "operand": "source.r"}
      ]
    }
    

    For BONUS bonus marks, you can run the repl_transfer_current command directly from within the plugin command presented here, in which case you don't need to use a macro at all and you could just bind the key directly to the command from the plugin. You would probably want to name the class differently in that case (e.g. RTransferAndAdvanceCommand or some such) so that the command name makes more sense.