Snippets should be triggerable from keystrokes #8
Description
Have you checked for existing feature requests?
- Completed
Summary
There are a few gaps in support between Pulsar snippets and those of other editors. One of them is the lack of variables like $TM_SELECTED_TEXT
.
In my opinion, the main reason there has been no urgency to add support for $TM_SELECTED_TEXT
is that there is only one official way of triggering a snippet — by typing its prefix and pressing Tab. For this reason, it’s not been possible for any text to be selected at the time of a snippet’s invocation. This stands in contrast to TextMate itself (which always allowed snippets to be invoked either by tab trigger or by arbitrary keystroke) and VSCode (which, as far as I can tell, allows for triggering of snippets via the command palette).
I’d like to add support for the whole range of variables mentioned in the LSP specification, but it’d be premature unless there were a way to invoke a snippet with a keystroke.
Proposal
The snippet config file should be enhanced. Rather than give it the ability to map to hotkeys directly (a job which should belong only to keymap.cson
), I’d propose the ability to give a particular snippet a command name, which would expose it as a command that could be invoked like any other.
As an example:
'.text.html':
'wrap in HTML tag':
body: '<${1:p}>${0:TM_SELECTED_TEXT}</$1>'
command: 'wrap-in-html-tag'
Here, a prefix
property is omitted, and a command
property is specified. The format of the command conforms to what is expected by atom.commands.add
.
Command names must follow the
namespace:action
pattern, where namespace will typically be the name of your package, and action describes the behavior of your command. If either part consists of multiple words, these must be separated by hyphens. E.g.awesome-package:turn-it-up-to-eleven
. All words should be lowercased.
Since the package name is snippets
, the user would define the part after the colon.
So that’s how a user would register a command that simply invokes a snippet. When the snippets
package parses a user’s snippets.cson
, it would:
- Enforce the presence of either
prefix
orcommand
(but it’s OK for both to be specified); - If command is present, ensure (a) that the value conforms to kebab-kase, (b) that the
snippets
package has not registered another command with the same identifier (whether it be built-in or already registered by an earlier snippet); - Register the command name and associate it with a handler that will programmatically trigger the given snippet.
With no further configuration, this would allow a snippet to be triggered from the command palette. But a user can go further and map a snippet’s command to a keystroke via their keymap.cson
:
'atom-pane atom-text-editor[data-grammar~="html"]':
'cmd-shift-w': 'snippets:wrap-in-html-tag'
Challenges
There are a few challenges that I can envision, none of which are complicated.
Snippets provided by packages?
If a user defines their own snippet, that command should use the snippets
command namespace by default.
But if a snippet is provided by a package, and it defines a command
, I think that command should be given the package’s own namespace. Otherwise it’d be too easy for different packages to step on each others’ toes.
Autocomplete?
I don’t use autocomplete-plus
, but I imagine a lot of people do, and also rely on autocomplete-snippets
to make snippets available as possible completions. I imagine that autocomplete-snippets
expects all snippets to have a prefix
.
I think this is fine, though. If a snippet defines command
but not prefix
, that strikes me as a clear signal that the snippet should never be suggested by autocomplete-snippets
, since prefix
is the only logical token that snippet suggestions can use.
Settings view?
The settings view for a particular package lists all the snippets provided by that package, and right now imagines that each one will have a prefix
property (though the settings view calls it “trigger”). These snippets are listed so that you can figure out which package supplies a given snippet, and so that you can turn them off wholesale if you think they’re too invasive. (You are also able to copy the CSON for individual snippets so that you can pick and choose which ones you want to add back to your snippets.cson
.)
We’d want to coordinate a change with settings-view
so that
- snippets without a
prefix
are still represented in this table, - snippets with a
command
have that name displayed in a new column, whether or not they have a prefix; - the Copy button preserves any
command
property so that it gets retained when pasted into someone’ssnippets.cson
.
And I would argue that the “Enable” checkbox, when unchecked, should prevent certain snippets from being triggered by prefix — but I think any snippet with a defined command name should still be triggerable via the command palette.
Why? The purpose of the “Enable” checkbox, as I see it, is that certain packages define a lot of snippets (e.g., language-html
), and that can cause pollution of one’s autocomplete suggestions, and possibly have surprising effects when you hit Tab and trigger a snippet when you didn’t mean to.
By contrast, a snippet that has a command name isn’t really getting in anything’s way, and would be quite hard to trigger by accident. The explanation of “Enable” would have to be a bit more nuanced, but I think this would be the best option.
Scope?
The snippets.cson
file uses top-level keys to restrict certain snippets so that they’re triggerable only within certain scopes. When a snippet defines a command
property, it makes it possible for the user to attempt to trigger that snippet from the command palette, regardless of the scope they’re in. I think that we should still guard against this possibility by writing the command handler so that it verifies we’re in a valid scope for that snippet, and throws up an error notification otherwise.
If the user wants that snippet to apply globally, they can define it under the *
scope.
You’ll notice that keymap.cson
has its own (clumsy) way of allowing certain keystrokes to have meaning only in certain kinds of files. Thus this also solves the hypothetical (probably rare) scenario where a user mistakenly maps a keystroke to a context that is mutually exclusive with the one under which the snippet itself is defined. In that scenario, we’d show the user a helpful error message to explain where they’d gone wrong.
What benefits does this feature provide?
Currently, the only good way I can think of to make a snippet triggerable by keystroke is to definie it in init.js
/init.coffee
:
atom.commands.add('atom-text-editor', {
'custom:wrap-in-html-tag': () => {
let editor = atom.workspace.getActiveTextEditor();
let selection = editor.getLastSelection();
let snippet = util.getSnippetsModule();
// Closing tag transforms the first tab stop's text so that it ignores
// anything that comes after the tag name.
snippet.insert('<${1:div}>' + selection.getText() + '</${1/[ ]+.*$//}>');
}
});
I use this approach in a number of places in my own init.js
, but it cries out for a simpler solution.
Any alternatives?
There are hypothetical alternatives, but this is the one that (to me) works best within Pulsar’s existing architectural choices.
Other examples:
See also the documentation for snippets in TextMate (the originator) and in VSCode.