Sublime Forum

Split/iterate [regions]

#1

I’m using add_regions to add hidden regions to a view. I then want to extract elements from this [region], but I receive the error “Region ‘object’ is not iterable”.

edited = self.view.get_regions("edited_rgns") or ] a, b = edited-1]

Is there a way that I can coerce [region] into a standard list of tuples pl?

0 Likes

#2

[pre=#000000]import sublime
import sublime_plugin

sublime.Region.totuple =* lambda** self*: (self.a, self.b)
sublime.Region.iter =* lambda** self*: self.totuple().iter()

class TupleRegionCommand(sublime_plugin.TextCommand):

  • def* run(self,* edit*):
    regions = self.view.get_regions(“bh_round”) or ]
    if len(regions):
    a, b = regions-1]
    print a, b[/pre]

See what I did there :wink:. With this, every region has a method to get its tuple form, but if you want to iterate the Region, you have now provided the python iterator requirement.

0 Likes

#3

Except someone else patched Region.iter to return an xrange(r.begin(), r.end()) and all was hell in the world :confused:

IMO, you wanna avoid monkey patches unless really needed

0 Likes

#4

Thank you @facelessuser

[code]Except someone else patched Region.iter to return an xrange(r.begin(), r.end()) and all was hell in the world :confused:

IMO, you wanna avoid monkey patches unless really needed[/code]
I agree, and it turns out I may not need to - but it’s nice to know that I can if necessary :smile:.

Continuing this topic slightly, how can I create a RegionSet? It seems it is only possible via sel() or add_regions?

0 Likes

#5

Yeah, monkey patching is seductively cool and there are legitimate uses for it, however that doesn’t seem to be one. I’ve written heaps of plugins I can’t share cause I used a heap of monkey patches.

You can’t actually instantiate a RegionSet, only get a reference to one via view.sel(). You can make a crappy implementation in Python though.

[pre=#0C1021]import bisect
import sublime

def subtract_region(r1, r2):
if not r1.contains(r2): r2 = r1.intersection(r2)

r1s, r1e = r1.begin(), r1.end()
r2s, r2e = r2.begin(), r2.end()

if r1s == r2s and r1e == r2e:
    return ]
elif r1s == r2s:
    return [sublime.Region(r2e, r1e)]
elif r1e == r2e:
    return [sublime.Region(r1s, r2s)]
else:
    return [sublime.Region(r1s, r2s), sublime.Region(r2e, r1e)]

class PyRegionSet(list):
def init(self, l=], merge=False):
if merge:
list.init(self)
for r in l: self.add(l)
else:
list.init(self, l)

def bisect(self, r):
    ix = min(bisect.bisect(self, r), len(self) -1)
    reg = self[ix]
    if r < reg and not (reg.contains(r) or reg.intersects(r)): ix -= 1
    return max(0, ix)

def clear(self):
    del self:]

def contains(self, r):
    return self and self.closest_selection(r).contains(r)

def closest_selection(self, r):
    return selfself.bisect(r)]

def add(self, r):
    if not self: return self.append(r)

    for ix in xrange(self.bisect(r), -1, -1):
        closest = self[ix]

        if closest.contains(r) or closest.intersects(r):
            self[ix] = closest.cover(r)
            return

        elif r.contains(closest) or r.intersects(closest):
            r = r.cover(closest)
            if ix: del self[ix]
            else: self[ix] = r
        else:
            self.insert(ix+1, r)
            return

def subtract(self, r):
    ix = self.bisect(r)

    while self:
        closest = self[ix]

        if closest.contains(r) or closest.intersects(r):
            del self[ix]
            for reg in subtract_region(closest, r):
                bisect.insort(self, reg)

            if ix == len(self): break
            continue
        break[/pre]
0 Likes

#6

[quote=“castles_made_of_sand”]Except someone else patched Region.iter to return an xrange(r.begin(), r.end()) and all was hell in the world :confused:

IMO, you wanna avoid monkey patches unless really needed[/quote]

:smile: yeah, yeah. It is the same argument made with not prototyping things like array etc in javascript (which is a completely valid argument). It all depends how isolated your code will be, but yeah, if you are going to be communicating with other plugins, then you might have a problem. If you are only talking directly with the API, then you are probably okay. But the same logic could just be contained in a regular function totuplelist that you feed a RegionSet or list of Regions.

[quote=“agibsonsw”]Thank you @facelessuser

[code]Except someone else patched Region.iter to return an xrange(r.begin(), r.end()) and all was hell in the world :confused:

IMO, you wanna avoid monkey patches unless really needed[/code]
I agree, and it turns out I may not need to - but it’s nice to know that I can if necessary :smile:.

Continuing this topic slightly, how can I create a RegionSet? It seems it is only possible via sel() or add_regions?[/quote]

You pretty much answered your own question. It cannot be instantiated from Python. It is Jon’s custom class which he hasen’t really given access to. The easiest way is to bug Jon…or not use it except when accessing set() and add_regions().

0 Likes

#7

[quote]Yeah, monkey patching is seductively cool and there are legitimate uses for it, however that doesn’t seem to be one. I’ve written heaps of plugins I can’t share cause I used a heap of monkey patches.

You can’t actually instantiate a RegionSet, only get a reference to one via view.sel(). You can make a crappy implementation in Python though.[/quote]

There is that too. As long as the API functions don’t really check the types of what you send in, you could use something like that, but if they do, you would then have to convert your PyRegionSet back to a list of of Regions…unless you are only using it for convenience in your code.

0 Likes

#8

not prototyping things like array

Are you talking about JavaScript extending of prototypes?

if you are going to be communicating with other plugins, then you might have a problem

?

0 Likes

#9

As long as the API functions don’t really check the types of what you send in, you could use something like that

[code]>>> rs = PyRegionSet(list(view.sel()))

view.add_regions(‘derpa’, rs, ‘’)
view.get_regions(‘derpa’)
(2131, 2131)]
[/code]

0 Likes

#10

Are you talking about JavaScript extending of prototypes?

NM, I missed the part where you said JavaScript.

0 Likes

#11

[quote=“castles_made_of_sand”]>>> not prototyping things like array

Are you talking about JavaScript extending of prototypes?
[/quote]

Yeah, I meant Javascript. Post corrected.

[quote=“castles_made_of_sand”]

if you are going to be communicating with other plugins, then you might have a problem

?[/quote]

Edit: wrong quote, but this was referring to other people mucking with the same kind of monkey patches.

0 Likes

#12

[quote=“castles_made_of_sand”]>>> As long as the API functions don’t really check the types of what you send in, you could use something like that

[code]>>> rs = PyRegionSet(list(view.sel()))

view.add_regions(‘derpa’, rs, ‘’)
view.get_regions(‘derpa’)
(2131, 2131)]
[/code][/quote]

Good to know.

0 Likes

#13

For the record, I do wish things like iter/hash were defined and indeed, boost::python is designed explicitly for the classes to be re opened. You can’t patch normal C extension builtin objects.

0 Likes

#14

Ahh. You are correct. I had not tested that. I have not used said monkey patch, and was only showing it as a concept, but it appears to be greatly flawed if communicating with the API. C is not so forgiving of class types as python is.

0 Likes

#15

Thanks for the code @castles. I don’t need this yet but shall bear it in mind :sunglasses:

edited = self.view.get_regions("edited_rgns") or ]

I just need two things now:
I want to select from “edited_rgns” based on its index number(s) - facelessuser’s code could do this but I’m not sure I need it just yet.

How would you converge my regions in “edited_rgns”. That is, if two regions are touching (or overlap) how can I convert them into one region please? Although ST seems to occasionally do this of its own accord if one region is contained within another.

0 Likes

#16

Don’t use mine if you intend to modify the Region class. Only use it as its own function that simply converts the regionset. Boost will not like you tampering with the base class.

0 Likes

#17

Well got it working - seems to be behaving :sunglasses:. I’ve re-written my LastEditLine (overnight…) code so that it maintains hidden regions that coincide with the edited content. I can now invoke a quick panel to list and jump to the edited lines :smiley:. It would be really nice if someone gave it a quick test run (please).

{ "keys": "ctrl+alt+j"], "command": "quick_edits" }, { "keys": "ctrl+alt+k"], "command": "prev_edit_line" }, { "keys": "ctrl+alt+l"], "command": "next_edit_line" }

[code]import sublime, sublime_plugin

class PrevEditLineCommand(sublime_plugin.TextCommand):
def run(self, edit):
vid = self.view.id()
sel = self.view.sel()[0]
currA = sel.begin()
currB = sel.end()
curr_line, _ = self.view.rowcol(currA)
edited = self.view.get_regions(“edited_rgns”) or ]
edited_last = self.view.get_regions(“edited_rgn”) or ]
if not edited and not edited_last:
return
edited.extend(edited_last)
self.view.add_regions(“edited_rgns”, edited, “edits”, sublime.HIDDEN)
self.view.erase_regions(“edited_rgn”)
edited = self.view.get_regions(“edited_rgns”) or ]

    for reg in [r for r in reversed(edited) if r.begin() < currA]:
        self.view.sel().clear()
        self.view.show(reg)
        self.view.sel().add(reg)
        break

class NextEditLineCommand(sublime_plugin.TextCommand):
def run(self, edit):
vid = self.view.id()
sel = self.view.sel()[0]
currA = sel.begin()
currB = sel.end()
curr_line, _ = self.view.rowcol(currA)
edited = self.view.get_regions(“edited_rgns”) or ]
edited_last = self.view.get_regions(“edited_rgn”) or ]
if not edited and not edited_last:
return
edited.extend(edited_last)
self.view.add_regions(“edited_rgns”, edited, “edits”, sublime.HIDDEN)
self.view.erase_regions(“edited_rgn”)
edited = self.view.get_regions(“edited_rgns”) or ]

    for reg in [r for r in edited if r.begin() > currA]:
        self.view.sel().clear()
        self.view.show(reg)
        self.view.sel().add(reg)
        break

class QuickEditsCommand(sublime_plugin.TextCommand):
def run(self, edit):
window = sublime.active_window()
view = window.active_view() if window != None else None
if view is None or view.id() != self.view.id():
sublime.status_message(‘Click into the view/tab first.’)
return
edited = self.view.get_regions(“edited_rgns”) or ]
edited_last = self.view.get_regions(“edited_rgn”) or ]
if not edited and not edited_last:
sublime.status_message(‘No edits to list.’)
return
edited.extend(edited_last)
self.view.add_regions(“edited_rgns”, edited, “edits”, sublime.HIDDEN)
self.view.erase_regions(“edited_rgn”)
edited = self.view.get_regions(“edited_rgns”) or ]
the_edits = ]
for i, r in enumerate(edited):
curr_line, _ = self.view.rowcol(r.begin())
curr_text = self.view.substr®
the_edits.append(“Line: %03d %s” % ( curr_line, curr_text ))
window.show_quick_panel(the_edits, self.on_chosen)

def on_chosen(self, index):
    if index == -1: return
    edited = self.view.get_regions("edited_rgns") or ]
    for reg in [r for i, r in enumerate(edited) if i == index]:
        self.view.sel().clear()
        self.view.show(reg)
        self.view.sel().add(reg)
        break

class CaptureEditing(sublime_plugin.EventListener):
def on_modified(self, view):
# create hidden regions that mirror the edited regions
vid = view.id()
sel = view.sel()[0]
currA = sel.begin()
currB = sel.end()
self.curr_line, _ = view.rowcol(currA)
if not hasattr(self, ‘prev_line’):
self.prev_line = self.curr_line
self.lastx = currA
self.lasty = currB
self.curr_edit = sublime.Region(self.lastx, self.lasty)
view.add_regions(“edited_rgn”,[self.curr_edit], “edits”, sublime.HIDDEN)
return
if self.curr_line == self.prev_line:
self.lastx = min(currA, self.lastx)
self.lasty = max(currB, self.lasty)
self.curr_edit = sublime.Region(self.lastx, self.lasty)
view.add_regions(“edited_rgn”,[self.curr_edit], “edits”, sublime.HIDDEN)
else:
self.prev_line = self.curr_line
self.lastx = currA
self.lasty = currB
edited = view.get_regions(“edited_rgns”) or ]
edited_last = view.get_regions(“edited_rgn”) or ]
if not edited and not edited_last:
return
edited.extend(edited_last)
view.add_regions(“edited_rgns”, edited, “edits”, sublime.HIDDEN)
view.erase_regions(“edited_rgn”)
[/code]
It occasionally drops the first letter of the edited text(?) and, as mentioned, I would like to collapse two adjacent regions. Regards, Andy.
LastEditLine.zip (993 Bytes)

0 Likes

#18

I will use

if not len(curr_text.strip()): curr_text = self.view.substr(self.view.line(r)) + " (line)"
so that, if the edit is just whitespace, it will instead display the full line in the quick panel.

It does seem to connect adjoining regions (sometimes…) but I’ll need to test this a bit more. But I still need to resolve it dropping the first edit-character…

0 Likes

#19

This version is working perfectly now!

[code]import sublime, sublime_plugin

def AdjustEdits(view):
edited = view.get_regions(“edited_rgns”) or ]
edited_last = view.get_regions(“edited_rgn”) or ]
if not edited and not edited_last:
return False
edited.extend(edited_last)
view.add_regions(“edited_rgns”, edited, “edits”, sublime.HIDDEN)
view.erase_regions(“edited_rgn”)
return view.get_regions(“edited_rgns”) or ]

class PrevEditLineCommand(sublime_plugin.TextCommand):
def run(self, edit):
vid = self.view.id()
currA = self.view.sel()[0].begin()
edited = AdjustEdits(self.view)
if not edited:
sublime.status_message(‘No edits to go to.’)
return
for reg in [r for r in reversed(edited) if r.begin() < currA]:
self.view.sel().clear()
self.view.show(reg)
self.view.sel().add(reg)
break
else:
sublime.status_message(‘No edits further up.’)

class NextEditLineCommand(sublime_plugin.TextCommand):
def run(self, edit):
vid = self.view.id()
currA = self.view.sel()[0].begin()
edited = AdjustEdits(self.view)
if not edited:
sublime.status_message(‘No edits to go to.’)
return
for reg in [r for r in edited if r.begin() > currA]:
self.view.sel().clear()
self.view.show(reg)
self.view.sel().add(reg)
break
else:
sublime.status_message(‘No edits further down.’)

class QuickEditsCommand(sublime_plugin.TextCommand):
def run(self, edit):
window = sublime.active_window()
view = window.active_view() if window != None else None
if view is None or view.id() != self.view.id():
sublime.status_message(‘Click into the view/tab first.’)
return
self.vid = self.view.id()
edited = AdjustEdits(self.view)
if not edited:
sublime.status_message(‘No edits to list.’)
return
the_edits = ]
for i, r in enumerate(edited):
curr_line, _ = self.view.rowcol(r.begin())
curr_text = self.view.substr®.strip()
if not len(curr_text):
curr_text = self.view.substr(self.view.line®).strip():40]
+ " (line)"
the_edits.append(“Line: %03d %s” % ( curr_line + 1, curr_text ))
window.show_quick_panel(the_edits, self.on_chosen)

def on_chosen(self, index):
    if index == -1: return
    window = sublime.active_window()
    view = window.active_view() if window != None else None
    if view is None or view.id() != self.vid:
        sublime.status_message('You are in a different view.')
        return
    edited = self.view.get_regions("edited_rgns") or ]
    for reg in [r for i, r in enumerate(edited) if i == index]:
        self.view.sel().clear()
        self.view.show(reg)
        self.view.sel().add(reg)
        break

class CaptureEditing(sublime_plugin.EventListener):
def on_modified(self, view):
# create hidden regions that mirror the edited regions
vid = view.id()
sel = view.sel()[0]
currA, currB = (sel.begin(), sel.end())
self.curr_line, _ = view.rowcol(currA)
if not hasattr(self, ‘prev_line’):
self.prev_line = self.curr_line
if currA > 0 and sel.empty():
currA -= 1
self.lastx, self.lasty = (currA, currB)
self.curr_edit = sublime.Region(self.lastx, self.lasty)
view.add_regions(“edited_rgn”,[self.curr_edit], “edits”, sublime.HIDDEN)
return
if self.curr_line == self.prev_line:
self.lastx = min(currA, self.lastx)
self.lasty = max(currB, self.lasty)
self.curr_edit = sublime.Region(self.lastx, self.lasty)
view.add_regions(“edited_rgn”,[self.curr_edit], “edits”, sublime.HIDDEN)
else:
self.prev_line = self.curr_line
if currA > 0 and sel.empty():
currA -= 1
self.lastx, self.lasty = (currA, currB)
_ = AdjustEdits(view)[/code]
I’m not really interested in jumping between views (as some others have done) nor in ordering them by edit-time, or storing the information between sessions. However, I retain an open mind so wont ignore requests :wink: .

The only other thing I’m considering at the moment is to, perhaps, toggle highlighting of edited text.

BTW There are already alternative versions of this available on PackageControl - but not as good as mine, he, he! Andy.

Added: I might try a combine consecutive edited lines though…
AndyEdits.zip (1.1 KB)

0 Likes

#20

Wow, that didn’t take long! Consecutive edited-lines will be treated as one edit:

[code]import sublime, sublime_plugin

def AdjustEdits(view):
edited = view.get_regions(“edited_rgns”) or ]
edited_last = view.get_regions(“edited_rgn”) or ]
if not edited and not edited_last:
return False
new_edits = ]
edited.extend(edited_last)
for i, r in enumerate(edited):
if i > 0 and r.begin() == prev_end:
new_edits.append(sublime.Region(prev_begin, r.end()))
else:
new_edits.append®
prev_begin, prev_end = (r.begin(), r.end())

view.add_regions("edited_rgns", new_edits, "edits", sublime.HIDDEN)
view.erase_regions("edited_rgn")
return view.get_regions("edited_rgns") or ]

class PrevEditLineCommand(sublime_plugin.TextCommand):
def run(self, edit):
vid = self.view.id()
currA = self.view.sel()[0].begin()
edited = AdjustEdits(self.view)
if not edited:
sublime.status_message(‘No edits to go to.’)
return
for reg in [r for r in reversed(edited) if r.begin() < currA]:
self.view.sel().clear()
self.view.show(reg)
self.view.sel().add(reg)
break
else:
sublime.status_message(‘No edits further up.’)

class NextEditLineCommand(sublime_plugin.TextCommand):
def run(self, edit):
vid = self.view.id()
currA = self.view.sel()[0].begin()
edited = AdjustEdits(self.view)
if not edited:
sublime.status_message(‘No edits to go to.’)
return
for reg in [r for r in edited if r.begin() > currA]:
self.view.sel().clear()
self.view.show(reg)
self.view.sel().add(reg)
break
else:
sublime.status_message(‘No edits further down.’)

class QuickEditsCommand(sublime_plugin.TextCommand):
def run(self, edit):
window = sublime.active_window()
view = window.active_view() if window != None else None
if view is None or view.id() != self.view.id():
sublime.status_message(‘Click into the view/tab first.’)
return
self.vid = self.view.id()
edited = AdjustEdits(self.view)
if not edited:
sublime.status_message(‘No edits to list.’)
return
the_edits = ]
for i, r in enumerate(edited):
curr_line, _ = self.view.rowcol(r.begin())
curr_text = self.view.substr®.strip()
if not len(curr_text):
curr_text = self.view.substr(self.view.line®).strip():40]
+ " (line)"
the_edits.append(“Line: %03d %s” % ( curr_line + 1, curr_text ))
window.show_quick_panel(the_edits, self.on_chosen)

def on_chosen(self, index):
    if index == -1: return
    window = sublime.active_window()
    view = window.active_view() if window != None else None
    if view is None or view.id() != self.vid:
        sublime.status_message('You are in a different view.')
        return
    edited = self.view.get_regions("edited_rgns") or ]
    for reg in [r for i, r in enumerate(edited) if i == index]:
        self.view.sel().clear()
        self.view.show(reg)
        self.view.sel().add(reg)
        break

class CaptureEditing(sublime_plugin.EventListener):
def on_modified(self, view):
# create hidden regions that mirror the edited regions
vid = view.id()
sel = view.sel()[0]
currA, currB = (sel.begin(), sel.end())
self.curr_line, _ = view.rowcol(currA)
if not hasattr(self, ‘prev_line’):
self.prev_line = self.curr_line
if currA > 0 and sel.empty():
currA -= 1
self.lastx, self.lasty = (currA, currB)
self.curr_edit = sublime.Region(self.lastx, self.lasty)
view.add_regions(“edited_rgn”,[self.curr_edit], “edits”, sublime.HIDDEN)
return
if self.curr_line == self.prev_line:
self.lastx = min(currA, self.lastx)
self.lasty = max(currB, self.lasty)
self.curr_edit = sublime.Region(self.lastx, self.lasty)
view.add_regions(“edited_rgn”,[self.curr_edit], “edits”, sublime.HIDDEN)
else:
self.prev_line = self.curr_line
if currA > 0 and sel.empty():
currA -= 1
self.lastx, self.lasty = (currA, currB)
_ = AdjustEdits(view)[/code]

0 Likes