Sublime Forum

How best to implement "Insert/delete line before without moving caret"?

#1

the kind of behavior i have in mind here is extremely straightforward. i’ll illustrate it with a gif:

basically, i want a variant of ‘insert line before/delete line before’ that keeps the caret position unaffected (well, its absolute position in the file has to be affected since there’s now one more/less newline before it, so it’s the relative position within the line that i want to preserve). you can think of it as a kind of ‘text dragging’ but with keyboard and not mouse.

as you can see from the gif, i already made user macros that try to realize this pair of operations. i’ll paste the macros below, but i don’t recommend anyone use it. if you actually take a look at it, you’ll see that it’s … hackish in a very bad way and not at all how you might imagine a straightforward implementation of this feature might look like (long story short: it involves temporarily breaking the line at caret position into two lines, call then A and B, then insert/delete a blank line before A, then somehow navigate back to end of A, and glue A and B back together). and in fact the macro only produces the desired behavior in about 95% of cases. in the remaining cases usually the line containing the caret is broken in half. i’m reluctant to try to patch up the macro though, because given how complicated it is as it stands it seems likely that the patched up version will fail in some cases where the current macro works.

it seems that for a simple functionality like this there should be some simpler way to realize it —— some simpler way to write the macro, or maybe a simple plugin. i was wondering if anyone has any ideas?

thanks in advance

delete_line_before_without_moving_cursor.sublime-macro

[
    {
        "args":
        {
            "characters": "\n"
        },
        "command": "insert"
    },
    {
        "args":
        {
            "file": "res://Packages/Default/Delete to Hard BOL.sublime-macro"
        },
        "command": "run_macro_file"
    },
    {
        "args":
        {
            "by": "lines",
            "forward": false
        },
        "command": "move"
    },
    {
        "args":
        {
            "by": "lines",
            "forward": false
        },
        "command": "move"
    },
    {
        "args":
        {
            "file": "res://Packages/Default/Delete Line.sublime-macro"
        },
        "command": "run_macro_file"
    },
    {
        "args":
        {
            "by": "lines",
            "forward": true
        },
        "command": "move"
    },
    {
        "args":
        {
            "extend": false,
            "to": "eol"
        },
        "command": "move_to"
    },
    {
        "args":
        {
            "to": "line"
        },
        "command": "expand_selection"
    },
    {
        "args":
        {
            "by": "characters",
            "forward": false
        },
        "command": "move"
    },
    {
        "args":
        {
            "by": "lines",
            "extend": true,
            "forward": false
        },
        "command": "move"
    },
    {
        "args":
        {
            "extend": true,
            "to": "eol"
        },
        "command": "move_to"
    },
    {
        "args": null,
        "command": "left_delete"
    }

]

insert_line_before_without_moving_cursor.sublime-macro

[
    {
        "args":
        {
            "characters": "\n"
        },
        "command": "insert"
    },
    {
        "args":
        {
            "by": "lines",
            "forward": false
        },
        "command": "move"
    },
    {
        "args":
        {
            "characters": "\n"
        },
        "command": "insert"
    },
    {
        "args":
        {
            "by": "lines",
            "forward": true
        },
        "command": "move"
    },
    {
        "args":
        {
            "by": "lines",
            "extend": true,
            "forward": false
        },
        "command": "move"
    },
    {
        "args":
        {
            "extend": true,
            "to": "eol"
        },
        "command": "move_to"
    },
    {
        "args": null,
        "command": "left_delete"
    }
]
0 Likes

#2

Something like the following plugin may do what you want, or at least get you started in the correct direction. To give it a try you can select Tools > Developer > New Plugin... from the menu, replace the entire buffer with the code, and then save it as e.g. insert_delete_line.py or something similar:

import sublime
import sublime_plugin

class InsertLineBeforeCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        for sel in self.view.sel():
            line = self.view.rowcol(sel.begin())[0]
            self.view.insert(edit, self.view.text_point(line, 0), "\n")

class DeleteLineBeforeCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        for sel in self.view.sel():
            line = self.view.rowcol(sel.begin())[0] - 1
            if line >= 0:
                kill = self.view.full_line(self.view.text_point(line, 0))
                self.view.replace(edit, kill, "")

You didn’t specifically mention in your question, but just for completeness (in case anyone else is interested in something similar) both of the implemented commands support multiple cursors and will adjust all of them simultaneously (this is demonstrated in the GIF below).

This may not have the desired effect if there is a selection, or at least what is modified may not be what you expect. You can use key bindings similar to the following to ensure that the command only triggers when there is no selection. I used Ctrl+Up and Ctrl+Down here for illustration purposes.

{ "keys": ["ctrl+up"], "command": "insert_line_before", "context":
    [
        { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true },
    ]
},
{ "keys": ["ctrl+down"], "command": "delete_line_before", "context":
    [
        { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true },
    ]
},

Alternatively, if you don’t want the command to trigger at all unless there is only a single selection, you can also add an extra context entry such as:

{ "key": "num_selections", "operator": "equal", "operand": 1 }

Here’s an example of the commands in action:

5 Likes