Sublime Forum

Problem with xpos

#1

I’m trying to add a functionality to “bubble” the current selection up/down/left/right through the text via the keyboard.

I’ve surprised myself by getting everything to work, except that I don’t know how to preserve the xpos value of my region as I bubble it around the text, which is necessary for full functionality. I tried this way, but it’s not working and I don’t know if the fault is with my program logic or with my misusing the API (the following code assumes a single nonempty selection):

def paste_and_select(view, xpos):
    start = view.sel()[0].begin()
    view.run_command("paste")
    pasted_region = sublime.Region(start, view.sel()[0].b)
    pasted_region.xpos = xpos
    view.sel().add(pasted_region)


class GrabUp(sublime_plugin.TextCommand):
    def run(self, edit):
        xpos = self.view.sel()[0].xpos
        self.view.run_command("cut")
        self.view.run_command("move", {"by": "lines", "forward": False})
        paste_and_select(self.view, xpos)


class GrabDown(sublime_plugin.TextCommand):
    def run(self, edit):
        xpos = regions[0].xpos
        self.view.run_command("cut")
        self.view.run_command("move", {"by": "lines", "forward": True})
        paste_and_select(view, xpos)


class GrabLeft(sublime_plugin.TextCommand):
    def run(self, edit):
        xpos = self.view.sel()[0].xpos
        self.view.run_command("cut")
        self.view.run_command("move", {"by": "characters", "forward": False})
        paste_and_select(self.view, xpos)


class GrabRight(sublime_plugin.TextCommand):
    def run(self, edit):
        xpos = self.view.sel()[0].xpos
        self.view.run_command("cut")
        self.view.run_command("move", {"by": "characters", "forward": True})
        paste_and_select(self.view, xpos)

Currently it seems to completely ignore my efforts to set xpos

1 Like

#2

did you try setting the xpos after the cut command, before the move command?

0 Likes

#3

Upon thought, sublime should be able to do its own thing when it moves the caret. (I.e., the xpos should be created/maintained by sublime at that point. No need to worry.) It’s when we paste the old region back that we need to make sure the xpos is set to what it used to be, before the paste.

I’ve also discovered that xpos is set via the sublime.Region constructor, as opposed to directly set. Also discovered that xpos is a coordinate, not a character number.

This has left me with the following code, whose print statements seem to vindicate the code’s logic:

def paste_and_select(view):
    start = view.sel()[0].begin()
    xpos_after_move_before_paste = view.sel()[0].xpos
    print("xpos_after_move_before_paste:", xpos_after_move_before_paste)
    view.run_command("paste")
    pasted_region = sublime.Region(start, view.sel()[0].b, xpos_after_move_before_paste)
    print("pasted_region a, b, xpos:", pasted_region.a, pasted_region.b, pasted_region.xpos)
    view.sel().add(pasted_region)
    print("view.sel()[0] a, b, xpos:", view.sel()[0].a, view.sel()[0].b, view.sel()[0].xpos)


class GrabUp(sublime_plugin.TextCommand):
    def run(self, edit):
        self.view.run_command("cut")
        b = self.view.sel()[0].b
        print("empirically measured xpos:", self.view.text_to_layout(b)[0])
        self.view.run_command("move", {"by": "lines", "forward": False})
        paste_and_select(self.view)

A sample run of the above results in the following printout:

empirically measured xpos: 704.0
xpos_after_move_before_paste: 704.0
pasted_region a, b, xpos: 55478 55483 704.0
view.sel()[0] a, b, xpos: 55478 55483 -1.0

The only thing that goes wrong is at the very last print statement. Somehow, when we run

view.sel().add(pasted_region)

the xpos attribute of pasted_region is wiped. (I verified this also by printing pasted_region.xpos directly: it’s set to -1.0 after the .add command.)

This seems silly… why would the sublime.Region constructor give me the option of setting xpos, if regions.add goes ahead and wipes the value?

1 Like

#4

Looking at the documentation https://www.sublimetext.com/docs/3/api_reference.html

Region(a, b) Creates a Region with initial values a and b.

It does not show that Region takes a third parameter. Either the documentation is incomplete or it is a bug to use the xpos parameter. Open a issue on https://github.com/SublimeTextIssues/Core/issues reporting this behavior. Searching there I could find these two issues mentioning xpos
https://github.com/SublimeTextIssues/Core/issues/2290
https://github.com/SublimeTextIssues/Core/issues/2202

By reading the documentation I could not understand what this xpos means and why I should use. This will probably not be the last time you will have problems with the API, if you keep working with Sublime Text. Your options now are to open an issue on the issue tracker I pointed you and hope the get from them some answer/fix, or start looking by yourself, for some kind of hack/workaround you can manage to do and overcome this situation (but only if you are luck if find one). I wrote a little about how much happy I am working with Sublime Text API. If you are interested, you can read and also share your experiences on the following link:

0 Likes

#5

This is the comment from Jon about this property:

You can find on the same topic a link to my plugin that use it with success.

1 Like

#6

Nice catch, this should explain why it was reset to -1. But that explanation did not seem make it way to the documentation, so far.

0 Likes

#7

Hi. I don’t get it.

I took a look at the source code of your plugin, and it also seems to use the constructor in the form sublime.Region(a, b, xpos) and then to use view.sel().add() to add the contructed region.

I guess I don’t understand the thing about “If it’s not -1 when the caret is moved down a row, then it specifies the column (in layout coords) for the caret to be placed in”. (addons_zz seems to get it; I don’t)

Here’s my code again, cleaned up again to remove the print statements—can you clue me in how to fix it?

def paste_and_select(view):
    start = view.sel()[0].begin()
    xpos = view.sel()[0].xpos
    view.run_command("paste")
    view.sel().add(sublime.Region(start, view.sel()[0].b, xpos))
    print(view.sel()[0].xpos)


class GrabUp(sublime_plugin.TextCommand):
    def run(self, edit):
        self.view.run_command("cut")
        self.view.run_command("move", {"by": "lines", "forward": False})
        paste_and_select(self.view)

“Success” would be achieved if the last print statement printed something else than -1.0…

0 Likes

#8
def paste_and_select(view):
    start = view.sel()[0].begin()
    xpos = view.sel()[0].xpos
    view.run_command("paste")
    region = sublime.Region(start, view.sel()[0].b)

    // Calling this changes the buffer and as jps said on the other topic, 
    // changing the buffer resets `.xpos` to `-1`
    view.sel().add(region)

    // Then, on my understanding we should be able to hold a reference to 
    // `sublime.Region()`, and set the `.xpos` after the buffer had been 
    // changed by `view.sel().add()`
    region.xpos = xpos
    print(view.sel()[0].xpos)

Alternatively, if the last attempted solution does not work for some unknown reason, then, we can try this instead:

def paste_and_select(view):
    start = view.sel()[0].begin()
    xpos = view.sel()[0].xpos
    view.run_command("paste")

    // Calling this changes the buffer and as jps said on the other topic, 
    // changing the buffer resets `.xpos` to `-1`
    region = sublime.Region(start, view.sel()[0].b)
    view.sel().add(region)

    // Then, if we are not able to hold a reference to `sublime.Region()`, then,
    // we should at least be able to recreate that reference and 
    // set the `.xpos` after the buffer had been changed by `view.sel().add()`
    region = sublime.Region(start, view.sel()[0].b)
    region.xpos = xpos
    print(view.sel()[0].xpos)

If none of them work, a new issue on the Core issue tracker should be opened because I already ran out of imagination https://github.com/SublimeTextIssues/Core/issues

0 Likes

#9

OK, I tried both solution and neither worked.

If no further help comes from higher authority (@bizoo?) I will open an issue on core, and hope to get some help from there.

(For reference, the second proposed makes no sense to me. Why should creating a separate Region object, independent of view.sel(), somehow affect view.sel()?)

Also: It really seems the only way to set xpos is via the constructor, not via foo.xpos = ...

0 Likes

#10

That is how some times I get things done with Sublime Text. Trying hack and do insane things in hope they will actually have some meaning and get things done. By thinking outside the box on most times will get you no results, but few selected times, they will actually have some meaning. Other than that, you can just open a issue on the Core issue tracker and wait until they do something about it.

0 Likes

#11

Sorry, I’m very short on time and as I didn’t do any ST plugin development in last few years, my knowledge are rusty.

First, didn’t what you try to do the same as this package ?

I suppose things get screwed in your code when you call internal command (cut, move and paste), maybe you better doing the cut and paste by yourself (get the selection, delete it from buffer and insert it to target).

If I remember well, yes, xpos can only be set with Region constructor.

0 Likes

#12

Hi Bizoo, thanks for the pointer to that package. It’s indeed what I’m trying to accomplish and I wasn’t aware of it.

Unfortunately the package seems to fail the same place I’m failing: it doesn’t succeed in keeping the column of vertically moved text. It has some ad-hoc mechanism using view.command_history to set the column to the right place, but it’s basically a hack and doesn’t work in all cases. No attempt is made to use xpos.

I suppose things get screwed in your code when you call internal command (cut, move and paste), maybe you better doing the cut and paste by yourself (get the selection, delete it from buffer and insert it to target).

I could delete the selection myself and insert it myself, instead of calling cut and paste. But if I insert I insert a string into the buffer, not a region. After inserting the string, I will have to select it by manually adding a region. That region needs to have a certain desired value of xpos. I don’t know how to do this except by instantiating sublime.Region(a, b, xpos) and then calling view.sel().add(...), as above. And the latter call seems to wipe out the xpos value. So yeah… I don’t see how avoiding the native cut and paste commands is going to help me :frowning:

0 Likes

#13

OK. I figured out the bug.

If/when

view.sel().add(sublime.Region(a, b, xpos))

is called and a region already exists that will intersect with the new region, the xpos value is wiped from the newly
added region, even if the newly added region subsumes the previously existing region. So one should call view.sel().clear() before adding the new region with the desired value of xpos. (Assuming there is only one region present.)

Also, the cut operation happens to kill xpos. So xpos should be saved/restored before/after the cut operation.

My code is below. I’ve added some bells and whistles to replicate the behavior of swap_line_up, swap_line_down.

===

def expand_empty_selection_to_line(view):
    if view.sel()[0].size() == 0:
        pos = view.sel()[0].a
        view.run_command("expand_line_selection_down")
        beg = view.sel()[0].a
        return pos - beg
    return None


def paste_and_select(view):
    start = view.sel()[0].begin()
    xpos = view.sel()[0].xpos
    view.run_command("paste")
    end = view.sel()[0].end()
    view.sel().clear()
    view.sel().add(sublime.Region(start, end, xpos))


def restore_xpos(view, xpos):
    if xpos >= 0:
        a = view.sel()[0].a
        view.sel().clear()
        view.sel().add(sublime.Region(a, a, xpos))


def move_text(view, by="lines", forward=True, num_times=1):
    assert by in ['lines', 'characters']
    if len(view.sel()) != 1:
        print("multi-selection text moving not supported")
        assert False

    delta = expand_empty_selection_to_line(view)
    if delta is not None and by == 'characters':
        print("horizontal text moving not supported on empty selection!")
        assert False

    xpos = view.sel()[0].xpos
    view.run_command("cut")
    restore_xpos(view, xpos)
    for i in range(num_times):
        view.run_command("move", {"by": by, "forward": forward})
    paste_and_select(view)

    if delta is not None:
        pos = view.sel()[0].a + delta
        view.sel().clear()
        view.sel().add(sublime.Region(pos, pos))


class GrabUp(sublime_plugin.TextCommand):
    def run(self, edit):
        move_text(self.view, by="lines", forward=False)


class GrabDown(sublime_plugin.TextCommand):
    def run(self, edit):
        move_text(self.view, by="lines", forward=True)


class GrabLeft(sublime_plugin.TextCommand):
    def run(self, edit):
        move_text(self.view, by="characters", forward=False)


class GrabRight(sublime_plugin.TextCommand):
    def run(self, edit):
        move_text(self.view, by="characters", forward=True)
2 Likes