Skip to content

Commit

Permalink
Support multicursor abbreviation expand
Browse files Browse the repository at this point in the history
commit 0cecb6ad65f0baf49d583fb94b77c0ad1f3b6c02
Merge: fbd67a7 01a9552
Author: Sergey Chikuyonok <serge.che@gmail.com>
Date:   Sun Oct 25 16:01:54 2020 +0300

    Merge branch 'master' into muticursor

    # Conflicts:
    #	lib/abbreviation.py
    #	main.py

commit fbd67a7bd4270786b29dc23e2d13d6b376dbd7c8
Author: Sergey Chikuyonok <serge.che@gmail.com>
Date:   Sun Oct 25 15:56:58 2020 +0300

    Working implementation of multicursor abbreviation tracker

commit a67d9878bda45ad97ec7c4e46dcb27018776c0fb
Author: Sergey Chikuyonok <serge.che@gmail.com>
Date:   Sun Sep 6 18:19:17 2020 +0300

    Experimenting with multicursor expand
  • Loading branch information
sergeche committed Oct 25, 2020
1 parent 01a9552 commit 6876fac
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 26 deletions.
15 changes: 14 additions & 1 deletion Default (Linux).sublime-keymap
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,26 @@
// "command": "emmet_rename_tag"
// },

// Tab key handler for single cursor
{
"keys": ["tab"],
"command": "emmet_expand_abbreviation",
"args": { "tab": true },
"context": [
{ "key": "emmet_abbreviation" },
{ "key": "emmet_tab_expand" }
{ "key": "emmet_tab_expand" },
{ "key": "num_selections", "operand": 1 }
]
},

// Tab key handler for multiple cursors
{
"keys": ["tab"],
"command": "emmet_expand_abbreviation",
"context": [
{ "key": "emmet_activation_scope" },
{ "key": "emmet_multicursor_tab_expand" },
{ "key": "num_selections", "operator": "not_equal", "operand": 1 }
]
},
{
Expand Down
15 changes: 14 additions & 1 deletion Default (OSX).sublime-keymap
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,26 @@
// "command": "emmet_rename_tag"
// },

// Tab key handler for single cursor
{
"keys": ["tab"],
"command": "emmet_expand_abbreviation",
"args": { "tab": true },
"context": [
{ "key": "emmet_abbreviation" },
{ "key": "emmet_tab_expand" }
{ "key": "emmet_tab_expand" },
{ "key": "num_selections", "operand": 1 }
]
},

// Tab key handler for multiple cursors
{
"keys": ["tab"],
"command": "emmet_expand_abbreviation",
"context": [
{ "key": "emmet_activation_scope" },
{ "key": "emmet_multicursor_tab_expand" },
{ "key": "num_selections", "operator": "not_equal", "operand": 1 }
]
},
{
Expand Down
15 changes: 14 additions & 1 deletion Default (Windows).sublime-keymap
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,26 @@
// "command": "emmet_rename_tag"
// },

// Tab key handler for single cursor
{
"keys": ["tab"],
"command": "emmet_expand_abbreviation",
"args": { "tab": true },
"context": [
{ "key": "emmet_abbreviation" },
{ "key": "emmet_tab_expand" }
{ "key": "emmet_tab_expand" },
{ "key": "num_selections", "operand": 1 }
]
},

// Tab key handler for multiple cursors
{
"keys": ["tab"],
"command": "emmet_expand_abbreviation",
"context": [
{ "key": "emmet_activation_scope" },
{ "key": "emmet_multicursor_tab_expand" },
{ "key": "num_selections", "operator": "not_equal", "operand": 1 }
]
},
{
Expand Down
10 changes: 9 additions & 1 deletion Emmet.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,15 @@
"ignore_scopes": [],

// Expand Emmet abbreviation with Tab key when in abbreviation marker
"tab_expand": true,
"tab_expand": true,

// Expand Emmet abbreviation with Tab key with multiple cursors in editor.
// Currently, this mode is less restricted that single-cursor Tab: it doesn’t
// require abbreviation to be immediately typed and expanded by user, it may
// expand existing abbreviation.
// As a side-effect, you may not be able to insert tab character after words
// if this option is enabled
"multicursor_tab": true,

// Emmet syntaxes (keys of "syntax_scopes" dictionary) where Tab expander
// is limited to known snippets only.
Expand Down
57 changes: 37 additions & 20 deletions lib/abbreviation.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@

class AbbreviationTracker:
__slots__ = ('region', 'abbreviation', 'forced', 'forced', 'offset',
'last_pos', 'last_length', 'config', 'simple', 'preview', 'error', 'valid_candidate')
'last_pos', 'config', 'simple', 'preview', 'line',
'error', 'valid_candidate')
def __init__(self, abbreviation: str, region: sublime.Region, config: Config, params: dict = None):
self.abbreviation = abbreviation
"Range in editor for abbreviation"
Expand All @@ -56,12 +57,14 @@ def __init__(self, abbreviation: str, region: sublime.Region, config: Config, pa
Used to handle prefixes in abbreviation
"""

self.line = sublime.Region(0, 0)
"""
Region of text line that contains tracked abbreviation
"""

self.last_pos = 0
"Last character location in editor"

self.last_length = 0
"Last editor size"

self.valid_candidate = True
"Indicates that current abbreviation is a valid candidate to expand"

Expand Down Expand Up @@ -180,9 +183,7 @@ def start_tracking(editor: sublime.View, start: int, pos: int, params: dict = No
tracker = create_tracker(editor, sublime.Region(start, pos), tracker_params)

if tracker:
_trackers[editor.id()] = tracker
mark(editor, tracker)
return tracker
return set_active_tracker(editor, tracker)

_dispose_tracker(editor)

Expand All @@ -208,6 +209,14 @@ def stop_tracking(editor: sublime.View, params: dict = {}):
_dispose_tracker(editor)


def set_active_tracker(editor: sublime.View, tracker: AbbreviationTracker) -> AbbreviationTracker:
"Sets currently active tracker"
_dispose_tracker(editor)
_trackers[editor.id()] = tracker
mark(editor, tracker)
return tracker


def create_tracker(editor: sublime.View, region: sublime.Region, params: dict) -> AbbreviationTracker:
"""
Creates abbreviation tracker for given range in editor. Parses contents
Expand All @@ -222,6 +231,13 @@ def create_tracker(editor: sublime.View, region: sublime.Region, params: dict) -
# Invalid range
return

line_a = editor.line(region.a)
line_b = editor.line(region.b)

if line_a != line_b:
# Mulitline regions are not supported
return

abbreviation = editor.substr(region)
if offset:
abbreviation = abbreviation[offset:]
Expand All @@ -234,8 +250,8 @@ def create_tracker(editor: sublime.View, region: sublime.Region, params: dict) -
tracker_params = {
'forced': forced,
'offset': offset,
'line': line_a,
'last_pos': region.end(),
'last_length': editor.size(),
}

try:
Expand All @@ -258,7 +274,7 @@ def create_tracker(editor: sublime.View, region: sublime.Region, params: dict) -
except Exception as err:
if hasattr(err, 'message') and hasattr(err, 'pos'):
tracker_params['error'] = {
'message': err.message,
'message': err.message.split('\n')[0],
'pos': err.pos,
'pointer': '%s^' % ('-' * err.pos, ) if err.pos is not None else ''
}
Expand Down Expand Up @@ -294,10 +310,8 @@ def restore_tracker(editor: sublime.View, pos: int) -> AbbreviationTracker:
# actually trying to restore tracker
return None

_trackers[editor.id()] = tracker
mark(editor, tracker)
tracker.last_length = editor.size()
return tracker
tracker.line = editor.line(tracker.region.a)
return set_active_tracker(editor, tracker)

return None

Expand All @@ -314,14 +328,16 @@ def suggest_abbreviation_tracker(view: sublime.View, pos: int) -> AbbreviationTr
stop_tracking(view)
trk = None

if not trk:
if not trk and allow_tracking(view, pos):
# Try to extract abbreviation from current location
config = get_activation_context(view, pos)
if config:
abbr = extract_abbreviation(view, pos, config)
if abbr:
offset = abbr.location - abbr.start
return start_tracking(view, abbr.start, abbr.end, {'config': config, 'offset': offset})
trk = start_tracking(view, abbr.start, abbr.end, {'config': config, 'offset': offset})

return trk


def handle_change(editor: sublime.View, pos: int) -> AbbreviationTracker:
Expand All @@ -338,14 +354,15 @@ def handle_change(editor: sublime.View, pos: int) -> AbbreviationTracker:

last_pos = tracker.last_pos
region = tracker.region
line = editor.line(pos)

if last_pos < region.begin() or last_pos > region.end():
# Updated content outside abbreviation: reset tracker
if last_pos < region.begin() or last_pos > region.end() or line.begin() != tracker.line.begin():
# Updated content outside abbreviation or tracker spans multiple lines:
# reset tracker
stop_tracking(editor)
return None

length = editor.size()
delta = length - tracker.last_length
delta = line.size() - tracker.line.size()
region = sublime.Region(region.a, region.b)

# Modify region and validate it: if it leads to invalid abbreviation, reset tracker
Expand Down Expand Up @@ -491,7 +508,7 @@ def allow_tracking(editor: sublime.View, pos: int) -> bool:
return False


def is_enabled(view: sublime.View, pos: int, skip_selector=False) -> bool:
def is_enabled(view: sublime.View, pos: int) -> bool:
"Check if Emmet abbreviation tracking is enabled"
auto_mark = get_settings('auto_mark', False)

Expand Down
87 changes: 85 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,18 @@ def wrapper(self, view):


class EmmetExpandAbbreviation(sublime_plugin.TextCommand):
def run(self, edit, tab=False):
def run(self, edit, **kwargs):
if len(self.view.sel()) > 1 or kwargs.get('force', False):
# Multicaret is less restricted mode: it doesn’t require
# abbreviation to be typed in order to expand it.
# Users can pass `force: true` argument to `emmet_expand_abbreviation`
# command to expand single-cursor abbreviations as well
self.multiple_caret(edit)
else:
self.single_caret(edit, kwargs.get('tab', False))


def single_caret(self, edit, tab=False):
caret = get_caret(self.view)
trk = abbreviation.get_tracker(self.view)

Expand All @@ -63,6 +74,41 @@ def run(self, edit, tab=False):
abbreviation.stop_tracking(self.view, {'force': not tab})


def multiple_caret(self, edit):
sels = []
doc_size = self.view.size()
expanded = None
cur_tracker = abbreviation.get_tracker(self.view)

for sel in reversed(list(self.view.sel())):
trk = abbreviation.suggest_abbreviation_tracker(self.view, sel.end())
if trk:
abbreviation.expand_tracker(self.view, edit, trk)
expanded = trk.config.syntax

# Update locations of existing regions
next_size = self.view.size()
delta = next_size - doc_size
for r in sels:
r.a += delta
r.b += delta

doc_size = next_size
sels += list(self.view.sel())
else:
sels.append(sel)
abbreviation.stop_tracking(self.view, {'force': True})

s = self.view.sel()
s.clear()
s.add_all(sels)

if expanded:
if cur_tracker:
abbreviation.store_tracker(self.view, cur_tracker)
track_action('Expand Multiple Abbreviations', expanded)


class EmmetEnterAbbreviation(sublime_plugin.TextCommand):
def run(self, edit):
trk = abbreviation.get_tracker(self.view)
Expand Down Expand Up @@ -432,6 +478,17 @@ def on_query_context(self, view: sublime.View, key: str, *args):
if key == 'emmet_tab_expand':
return get_settings('tab_expand', False)

if key == 'emmet_multicursor_tab_expand':
return allow_multicursor_abbr(view)

if key == 'emmet_activation_scope':
# For each cursor check if it’s in allowed context
for sel in view.sel():
if not sel.empty() or not syntax.in_activation_scope(view, sel.end()):
return False

return True

if key == 'has_emmet_abbreviation_mark':
return bool(abbreviation.get_tracker(view))

Expand Down Expand Up @@ -486,7 +543,8 @@ def on_post_text_command(self, editor: sublime.View, command_name: str, args: li
trk = abbreviation.get_stored_tracker(editor)
if trk and isinstance(trk, abbreviation.AbbreviationTrackerValid) and \
editor.substr(trk.region) == trk.abbreviation:
abbreviation.restore_tracker(editor, get_caret(editor))
for s in editor.sel():
abbreviation.restore_tracker(editor, s.end())
else:
# Undo may restore editor marker, remove it
abbreviation.unmark(editor)
Expand Down Expand Up @@ -517,3 +575,28 @@ def on_modified_async(self, view: sublime.View):
def on_post_text_command(self, view, command_name, args):
if command_name != 'emmet_select_item':
select_item.reset_model(view)


def allow_multicursor_abbr(view: sublime.View):
"Check if multicursor abbreviation expand is allowed"
if not get_settings('multicursor_tab', False):
return False

# Check that any of the cursors touches valid Emmet abbreviation
tracker = abbreviation.get_tracker(view)
if tracker:
for s in view.sel():
if tracker.line.contains(s):
return True

# Fast check failed: try to extract abbreviation for any of cursors
for s in view.sel():
trk = abbreviation.suggest_abbreviation_tracker(view, s.end())
if trk:
return True

# Restore previous tracker, if any
if tracker:
abbreviation.set_active_tracker(view, tracker)

return False

0 comments on commit 6876fac

Please sign in to comment.