Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.8
6 changes: 6 additions & 0 deletions License.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

Debouncing code license
Copyright (c) 2012 John Isaacks
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
185 changes: 153 additions & 32 deletions elastic_tabstops.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
UPDATE
Use this command to reprocess the whole file. Shouldn't be necessary except
in specific cases.

{ "keys": ["super+j"], "command": "elastic_tabstops_update"},
"""

Expand All @@ -11,6 +11,8 @@
import sublime_plugin
import re
import sys
import time

if sys.version_info[0] < 3:
from edit import Edit
from itertools import izip, izip_longest
Expand All @@ -20,6 +22,22 @@
from ElasticTabstops.edit import Edit
from itertools import zip_longest

# binary representation of all ST events
NEW = 1
CLONE = 2
LOAD = 4
PRE_SAVE = 8
POST_SAVE = 16
MODIFIED = 32
SELECTION_MODIFIED = 64
ACTIVATED = 128
DEACTIVATED = 256

try:
set_timeout = sublime.set_timeout_async
except AttributeError:
set_timeout = sublime.set_timeout

def lines_in_buffer(view):
row, col = view.rowcol(view.size())
#"row" is the index of the last row; need to add 1 to get number of rows
Expand Down Expand Up @@ -72,7 +90,7 @@ def cell_widths_for_row(view, row):

def find_cell_widths_for_block(view, row):
cell_widths = []

#starting row and backward
row_iter = row
while row_iter >= 0:
Expand All @@ -82,7 +100,7 @@ def find_cell_widths_for_block(view, row):
cell_widths.insert(0, widths)
row_iter -= 1
first_row = row_iter + 1

#forward (not including starting row)
row_iter = row
num_rows = lines_in_buffer(view)
Expand All @@ -92,7 +110,7 @@ def find_cell_widths_for_block(view, row):
if len(widths) == 0:
break
cell_widths.append(widths)

return cell_widths, first_row

def adjust_row(view, glued, row, widths):
Expand All @@ -101,14 +119,14 @@ def adjust_row(view, glued, row, widths):
return glued
bias = 0
location = -1

for w, it in zip(widths,row_tabs):
location += 1 + w
it += bias
difference = location - it
if difference == 0:
continue

end_tab_point = view.text_point(row, it)
partial_line = view.substr(view.line(end_tab_point))[0:it]
stripped_partial_line = partial_line.rstrip()
Expand Down Expand Up @@ -156,7 +174,7 @@ def process_rows(view, rows):
for row in rows:
if row in checked_rows:
continue

cell_widths_by_row, row_index = find_cell_widths_for_block(view, row)
set_block_cell_widths_to_max(cell_widths_by_row)
for widths in cell_widths_by_row:
Expand All @@ -178,46 +196,149 @@ def fix_view(view):
return view

class ElasticTabstopsListener(sublime_plugin.EventListener):
selected_rows_by_view = {}
running = False

def on_modified(self, view):
def __init__(self):
self.view_events = {}

def debounce(self, view, event_id):
"""Invoke evaluation of changes after some idle time
view (View): The view to perform evaluation for
event_id (int) : The event identifier """
# based on https://github.com/jisaacks/GitGutter/blob/master/modules/events.py
key = view.id()
try:
self.view_events[key].push(event_id)
except KeyError:
if view.buffer_id():
new_listener = ViewEventListener(view)
new_listener.push(event_id)
self.view_events[key] = new_listener
for vid in [vid for vid, listener in self.view_events.items() # collect garbage
if listener.view.buffer_id() == 0]:
del self.view_events[vid]

# TODO: check if activated/selection asyncs bug and need to revert to the blocking version
def on_activated_async (self, view):
self.debounce(view,ACTIVATED)
def on_selection_modified_async(self, view):
self.debounce(view,SELECTION_MODIFIED)
def on_modified (self, view):
# user editing during the line-by-line tabstop fixing operation can bug by deleting/inserting a symbol; use the sync API to block it
self.debounce(view, MODIFIED)

class ViewEventListener(object):
"""Queues and forwards view events to Commands
A ViewEventListener object queues all events received from a view and starts a single Sublime timer to forward the event to Commands after some idle time. Prevents bloating Sublime API due to dozens of timers running for debouncing events
"""
def __init__(self, view):
"""Initialize ViewEventListener object
view (View): The view the object is created for """

self.view = view
self.settings = view.settings() #
self.busy = False # flag: timer is running
self.running = False # flag: modification is running
self.events = 0 # a binary combination of above events
self.latest_time = 0.0 # latest time of append() call
_delay = self.settings.get('eltab_debounce_delay',1000) # config: debounce delay
self.delay = max(200,_delay if _delay is not None else 0) # debounce delay in milliseconds
self.selected_rows_by_view = {}

def push(self, event_id):
"""Push the event to the queue and start idle timer.
Add the event identifier to 'events' and update the 'latest_time'. This marks an event to be received rather than counting the number of received events. The idle timer is started only, if no other one is already in flight.
event_id (int): One of the event identifiers"""
self.latest_time = time.time()
self.events |= event_id
if not self.busy:
self.start_timer(self.delay)

def start_timer(self, delay):
"""Run commands after some idle time
If no events received during the idle time → run the commands
Else → restart timer to check later
Timer is stopped without calling the commands if a view is not visible to save some resources. Evaluation will be triggered by activating the view next time
delay (int): The delay in milliseconds to wait until probably
forward the events, if no other event was received in the meanwhile"""
start_time = self.latest_time

def worker():
"""The function called after some idle time."""
if start_time < self.latest_time:
self.start_timer(self.delay)
return
self.busy = False
if not self.is_view_visible():
return
# bitwise AND comparison to see which events were triggered during the delay
if ACTIVATED == ACTIVATED & self.events:
self.activated()
if SELECTION_MODIFIED == SELECTION_MODIFIED & self.events:
self.selection_modified()
if MODIFIED == MODIFIED & self.events:
self.modified()
self.events = 0

self.busy = True
if MODIFIED == MODIFIED & self.events:
sublime.set_timeout(worker, delay) # force sync timeout
else:
set_timeout(worker, delay) # async timeout (if exists)

def is_view_visible(self):
"""Determine if the view is visible
Only an active view of a group is visible
Returns: bool: True if the view is visible in any window """
window = self.view.window()
if window:
view_id = self.view.id()
for group in range(window.num_groups()):
active_view = window.active_view_in_group(group)
if active_view and active_view.id() == view_id:
return True
return False

def activated(self):
view = self.view
view = fix_view(view)
self.selected_rows_by_view[view.id()] = get_selected_rows(view)

def selection_modified(self):
view = self.view
view = fix_view(view)
self.selected_rows_by_view[view.id()] = get_selected_rows(view)

def modified(self):
view = self.view
if self.running:
return

view = fix_view(view)

history_item = view.command_history(1)[1]
if history_item:
if history_item.get('name') == "ElasticTabstops":
if history_item.get ('name') == "ElasticTabstops":
return
if history_item.get('commands') and history_item['commands'][0][1].get('name') == "ElasticTabstops":
if history_item.get('commands') and \
history_item['commands'][0][1].get('name') == "ElasticTabstops":
return

selected_rows = self.selected_rows_by_view.get(view.id(), set())
selected_rows = selected_rows.union(get_selected_rows(view))

try:
self.running = True
translate = False
if view.settings().get("translate_tabs_to_spaces"):
if self.settings.get("translate_tabs_to_spaces"):
translate = True
view.settings().set("translate_tabs_to_spaces", False)
self.settings.set("translate_tabs_to_spaces", False)

process_rows(view, selected_rows)

finally:
self.running = False
if translate:
view.settings().set("translate_tabs_to_spaces",True)

def on_selection_modified(self, view):
view = fix_view(view)
self.selected_rows_by_view[view.id()] = get_selected_rows(view)

def on_activated(self, view):
view = fix_view(view)
self.selected_rows_by_view[view.id()] = get_selected_rows(view)
self.settings.set("translate_tabs_to_spaces",True)


class ElasticTabstopsUpdateCommand(sublime_plugin.TextCommand):
def run(self, edit):
Expand Down Expand Up @@ -246,9 +367,9 @@ def run(self, edit, direction, extend):
else:
raise Exception("invalid direction")
next_tab_col = s.b

b = self.view.text_point(row, next_tab_col)

if extend:
new_regions.append(sublime.Region(s.a, b))
else:
Expand Down