Most importantly, does it always force the selection to expand on both sides? I couldn’t find any documentation outlining the algorithm it uses.
What exactly does “Expand Selection to Scope” do?
The whole syntax highlighting is based on so called scopes. Scopes are portions of text which can be addressed by color schemes to apply colors to. Besides normal scopes like keyword
, variable
, … the source code also contains so colled meta
scopes which are used to mark certain types of code constructs, which might be interesting for plugin devs, such as meta.function-call
The function “Expand Selection to Scope” does what the name says. It expands the selection to the next matching scope boundaries. Who much is selected depends on the applied syntax-file.
If the cursor is on a keyword the function will most likely select the whole word on first call and will continue to expand to the next matching scope construct such as a whole function call or whats ever under the cursor.
I think the question was meant to be more on the detail side rather than the technical.
I believe it expands a point/region to the characters that have the exact same scope. I can’t verify, however. I also believe it discards the last name when an expansion would have no effect and does an extension on that scope instead. It is on both sides.
I think there’s an off-by-one error on the end of the selection.
Example (JavaScript):
{ (foo) }
// ^ insertion point between paren and space
Expected selection after command:
{ (foo) }
// ^^^^^^^^^^^
Actual:
{ (foo) }
// ^^^
The selection actually jumps over the intervening close parenthesis.
the extract_scope
method on the View
is I imagine, what the command: expand_selection {"to": "scope"}
uses internally. The API reference explains it:
Returns the extent of the syntax scope name assigned to the character at the given point.
So from that, we know that it must expand both sides of the point.
Note that if you select the space, it works as expected.
It looks like, for whatever reason, the builtin command is always expanding the selection by at least one character on each side, even when that doesn’t make sense. Example:
function foo(bar) {}
// ^^ selected
Expected:
function foo(bar) {}
// ^^^^^^^^
Actual:
function foo(bar) {}
// ^^^^^^^^^^^^^^^^^
Whatever it’s doing, it doesn’t seem to be what I expect or want it to do most of the time.
Here’s what I’ve been trying out instead:
import sublime
import sublime_plugin
import re
class BetterExpandSelectionToScopeCommand(sublime_plugin.TextCommand):
def split_scope(self, pos):
return re.findall(
r'(?:^|\s+|\.)[^\s.]+',
self.view.scope_name(pos)
)
def find_by_selector_containing(self, selector, point):
return next(
region
for region in self.view.find_by_selector(selector)
if region.contains(point)
)
def run(self, edit):
selection = self.view.sel()
for region in selection:
expanded = self.get_expanded(region)
if expanded: selection.add(expanded)
def get_expanded(self, region):
target = self.split_scope(region.begin())
while len(target):
found = self.find_by_selector_containing(''.join(target), region.begin())
if found.contains(region) and not region.contains(found):
print(''.join(target))
return found
else:
target.pop()
Scoping Best Practices
I did some tinkering and it seems related to whether you start from a point or a selection. When I have the cursor on a point, it expands to the next scope, then the next scope, and so forth.
If I highlight a couple of characters and execute the command, it seems to just put the point at the end of the end of the largest meta scope. If I have a line like “signal my_signal : integer;” and highlight the _s out of my_signal, it’ll put the cursor after the terminator. Definitely a little strange out of the box (Stable, 3126)
Here’s another example (also from JavaScript):
var myFunc = x => x;
// ^ selection
Expected:
var myFunc = x => x;
// ^^^^^^
Actual:
var myFunc = x => x;
// ^^^^^^^^^^^^^^
It seems to be expanding to source.js meta.function.declaration.js
, but it could (should?) have instead expanded to the smaller source.js meta.function.declaration.js variable.other.readwrite.js entity.name.function.js
.
I am really not familiar with Javascript, but when I did some scope sniffing on this line, it does some really strange stuff that I don’t know if it’s valid.
var myFunc = x => x;
^^^ -> source.js storage.type.js
^ -> source.js
^^^^^^ -> source.js meta.function.declaration.js variable.other.readwrite.js entity.name.function.js
^ -> source.js meta.function.declaration.js
^ -> source.js meta.function.declaration.js keyword.operator.assignment.js
^ -> source.js meta.function.declaration.js
^ -> source.js meta.function.declaration.js variable.parameter.function.js
^ -> source.js meta.function.declaration.js
^^ -> source.js meta.function.declaration.js storage.type.function.arrow.js
^ -> source.js
^ -> source.js meta.block.js variable.other.readwrite.js
^ -> source.js punctuation.terminator.statement.js
The two issues I see is that myFunc has a number of scopes applied. I don’t know exactly how expand selection to scope works, but it seems to act as if it only pays attention to meta
scopes. The meta
scope starts at the beginning of myFunc
and extends the end of the arrow. After that, we drop back to source.js
as the only scope applied so the next iteration of expand selection to scope selects everything.
I guess the upshot is that to have expand selection to scope work you have to have a well defined syntax file with a high level of granularity.