Skip to content
This repository was archived by the owner on Jan 8, 2025. It is now read-only.

Commit 78cb1c9

Browse files
Merge pull request #11 from savetheclocktower/add-variables
Add variables
2 parents bb00f90 + 44c0760 commit 78cb1c9

12 files changed

+1816
-236
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
node_modules
2+
.tool-versions

README.md

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Snippets files are stored in a package's `snippets/` folder and also loaded from
1818

1919
The outermost keys are the selectors where these snippets should be active, prefixed with a period (`.`) (details below).
2020

21-
The next level of keys are the snippet names.
21+
The next level of keys are the snippet names. Because this is object notation, each snippet must have a different name.
2222

2323
Under each snippet name is a `body` to insert when the snippet is triggered.
2424

@@ -32,24 +32,26 @@ console.log("crash");
3232

3333
The string `"crash"` would be initially selected and pressing tab again would place the cursor after the `;`
3434

35-
A snippet must define **at least one** of the following keys:
35+
A snippet specifies how it can be triggered. Thus it must provide **at least one** of the following keys:
3636

3737
### The ‘prefix’ key
3838

39-
If a `prefix` is defined, it specifies a string that can trigger the snippet: type the string in the editor and press <kbd>Tab</kbd>. In this example, typing `log` (as its own word) and then pressing <kbd>Tab</kbd> would replace `log` with the string `console.log("crash")` as described above.
39+
If a `prefix` is defined, it specifies a string that can trigger the snippet. In the above example, typing `log` (as its own word) and then pressing <kbd>Tab</kbd> would replace `log` with the string `console.log("crash")` as described above.
4040

4141
Prefix completions can be suggested if partially typed thanks to the `autocomplete-snippets` package.
4242

4343
### The ‘command’ key
4444

4545
If a `command` is defined, it specifies a command name that can trigger the snippet. That command can be invoked from the command palette or mapped to a keyboard shortcut via your `keymap.cson`.
4646

47-
If you defined the `console.log` snippet described above in your own `snippets.cson`, it would be available in the command palette as “Snippets: Insert Console Log”, or could be referenced in a keymap file as `snippets:insert-console-log`.
47+
If a package called `some-package` had defined that snippet, it would be available in the keymap as `some-package:insert-console-log`, or in the command palette as **Some Package: Insert Console Log**.
4848

49-
If a package called `some-package` had defined that snippet, it would be available in the keymap as `some-package:insert-console-log`, or in the command palette as “Some Package: Insert Console Log.
49+
If you defined the `console.log` snippet described above in your own `snippets.cson`, it could be referenced in a keymap file as `snippets:insert-console-log`, or in the command palette as **Snippets: Insert Console Log**.
5050

5151
Invoking the command would insert the snippet at the cursor, replacing any text that may be selected.
5252

53+
Snippet command names must be unique. They can’t conflict with each other, nor can they conflict with any other commands that have been defined. If there is such a conflict, you’ll see an error notification describing the problem.
54+
5355
### Optional parameters
5456

5557
These parameters are meant to provide extra information about your snippet to [autocomplete-plus](https://github.com/atom/autocomplete-plus/wiki/Provider-API).
@@ -74,24 +76,76 @@ Example:
7476

7577
### Determining the correct scope for a snippet
7678

77-
The outmost key of a snippet is the "scope" that you want the descendent snippets to be available in. The key should be prefixed with a period (`text.html.basic` => `.text.html.basic`). You can find out the correct scope by opening the Settings (<kbd>cmd-,</kbd> on macOS) and selecting the corresponding *Language [xxx]* package, e.g. for *Language Html*:
79+
The outmost key of a snippet is the scope that you want the descendent snippets to be available in. The key should be prefixed with a period (`text.html.basic` `.text.html.basic`). You can find out the correct scope by opening the Settings (<kbd>cmd-,</kbd> on macOS) and selecting the corresponding *Language [xxx]* package. For example, here’s the settings page for `language-html`:
7880

7981
![Screenshot of Language Html settings](https://cloud.githubusercontent.com/assets/1038121/5137632/126beb66-70f2-11e4-839b-bc7e84103f67.png)
8082

81-
If it's difficult to determine the package handling the file type in question (for example, for `.md`-documents), you can also proceed as following. Put your cursor in a file in which you want the snippet to be available, open the [Command Palette](https://github.com/pulsar-edit/command-palette)
82-
(<kbd>cmd-shift-p</kbd>), and run the `Editor: Log Cursor Scope` command. This will trigger a notification which will contain a list of scopes. The first scope that's listed is the scope for that language. Here are some examples: `source.coffee`, `text.plain`, `text.html.basic`.
83+
If it's difficult to determine the package handling the file type in question (for example, for `.md`-documents), you can use another approach:
84+
85+
1. Put your cursor in a file in which you want the snippet to be available.
86+
2. Open the [Command Palette](https://github.com/pulsar-edit/command-palette)
87+
(<kbd>cmd-shift-p</kbd> or <kbd>ctrl-shift-p</kbd>).
88+
3. Run the `Editor: Log Cursor Scope` command.
89+
90+
This will trigger a notification which will contain a list of scopes. The first scope that's listed is the scope for that language. Here are some examples: `source.coffee`, `text.plain`, `text.html.basic`.
91+
92+
## Snippet syntax
93+
94+
This package supports a subset of the features of TextMate snippets, [documented here](http://manual.macromates.com/en/snippets), as well as most features described in the [LSP specification][lsp] and [supported by VSCode][vscode].
95+
96+
The following features from TextMate snippets are not yet supported:
97+
98+
* Interpolated shell code can’t reliably be supported cross-platform, and is probably a bad idea anyway. No other editors that support snippets have adopted this feature, and Pulsar won’t either.
99+
100+
The following features from VSCode snippets are not yet supported:
101+
102+
* “Choice” syntax like `${1|one,two,three|}` requires that the autocomplete engine pop up a menu to offer the user a choice between the available placeholder options. This may be supported in the future, but right now Pulsar effectively converts this to `${1:one}`, treating the first choice as a conventional placeholder.
103+
104+
### Variables
105+
106+
Pulsar snippets support all of the variables mentioned in the [LSP specification][lsp], plus many of the variables [supported by VSCode][vscode].
107+
108+
Variables can be referenced with `$`, either without braces (`$CLIPBOARD`) or with braces (`${CLIPBOARD}`). Variables can also have fallback values (`${CLIPBOARD:http://example.com}`), simple flag-based transformations (`${CLIPBOARD:/upcase}`), or `sed`-style transformations `${CLIPBOARD/ /_/g}`.
83109

84-
### Snippet syntax
110+
One of the most useful is `TM_SELECTED_TEXT`, which represents whatever text was selected when the snippet was invoked. (Naturally, this can only happen when a snippet is invoked via command or key shortcut, rather than by typing in a <kbd>Tab</kbd> trigger.)
85111

86-
This package supports a subset of the features of TextMate snippets, [documented here](http://manual.macromates.com/en/snippets#transformations).
112+
Others that can be useful:
87113

88-
The following features are not yet supported:
114+
* `TM_FILENAME`: The name of the current file (`foo.rb`).
115+
* `TM_FILENAME_BASE`: The name of the current file, but without its extension (`foo`).
116+
* `TM_FILEPATH`: The entire path on disk to the current file.
117+
* `TM_CURRENT_LINE`: The entire current line that the cursor is sitting on.
118+
* `TM_CURRENT_WORD`: The entire word that the cursor is within or adjacent to, as interpreted by `cursor.getCurrentWordBufferRange`.
119+
* `CLIPBOARD`: The current contents of the clipboard.
120+
* `CURRENT_YEAR`, `CURRENT_MONTH`, et cetera: referneces to the current date and time in various formats.
89121

90-
* Variables
91-
* Interpolated shell code
92-
* Conditional insertions in transformations
122+
Any variable that has no value — for instance, `TM_FILENAME` on an untitled document — will resolve to an empty string.
93123

94-
### Multi-line Snippet Body
124+
#### Variable transformation flags
125+
126+
Pulsar supports the three flags defined in the [LSP snippets specification][lsp] and two other flags that are [implemented in VSCode][vscode]:
127+
128+
* `/upcase` (`foo``FOO`)
129+
* `/downcase` (`BAR``bar`)
130+
* `/capitalize` (`lorem ipsum dolor``Lorem ipsum dolor`)
131+
* `/camelcase` (`foo bar``fooBar`, `lorem-ipsum.dolor``loremIpsumDolor`)
132+
* `/pascalcase` (`foo bar``FooBar`, `lorem-ipsum.dolor``LoremIpsumDolor`)
133+
134+
#### Variable caveats
135+
136+
* `WORKSPACE_NAME`, `WORKSPACE_FOLDER`, and `RELATIVE_PATH` all rely on the presence of a root project folder, but a Pulsar project can technically have multiple root folders. While this is rare, it is handled by `snippets` as follows: whichever project path is an ancestor of the currently active file is treated as the project root — or the first one found if multiple roots are ancestors.
137+
* `WORKSPACE_NAME` in VSCode refers to “the name of the opened workspace or folder.” In the former case, this appears to mean bundled projects with a `.code-workspace` file extension — which have no Pulsar equivalent. Instead, `WORKSPACE_NAME` will always refer to the last path component of your project’s root directory as defined above.
138+
139+
#### Variables that are not yet supported
140+
141+
Of the variables supported by VSCode, Pulsar does not yet support:
142+
143+
* `UUID`
144+
* `BLOCK_COMMENT_START`
145+
* `BLOCK_COMMENT_END`
146+
* `LINE_COMMENT`
147+
148+
## Multi-line Snippet Body
95149

96150
You can also use multi-line syntax using `"""` for larger templates:
97151

@@ -110,7 +164,7 @@ You can also use multi-line syntax using `"""` for larger templates:
110164
"""
111165
```
112166

113-
### Escaping Characters
167+
## Escaping Characters
114168

115169
Including a literal closing brace inside the text provided by a snippet's tab stop will close that tab stop early. To prevent that, escape the brace with two backslashes, like so:
116170

@@ -127,6 +181,12 @@ Including a literal closing brace inside the text provided by a snippet's tab st
127181
"""
128182
```
129183

130-
### Multiple snippets for the same scope
184+
Likewise, if your snippet includes literal references to `$` or `{`, you may have to escape those with two backslashes as well, depending on the context.
185+
186+
## Multiple snippets for the same scope
187+
188+
Snippets for the same scope must be placed within the same key. See [this section of the Pulsar Flight Manual](https://pulsar-edit.dev/docs/launch-manual/sections/using-pulsar/#configuring-with-cson) for more information.
189+
131190

132-
Snippets for the same scope must be placed within the same key. See [this section of the Atom Flight Manual](https://pulsar-edit.dev/docs/atom-archive/using-atom/#configuring-with-cson) for more information.
191+
[lsp]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#variables
192+
[vscode]: https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables

lib/insertion.js

Lines changed: 9 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,30 @@
1-
const ESCAPES = {
2-
u: (flags) => {
3-
flags.lowercaseNext = false
4-
flags.uppercaseNext = true
5-
},
6-
l: (flags) => {
7-
flags.uppercaseNext = false
8-
flags.lowercaseNext = true
9-
},
10-
U: (flags) => {
11-
flags.lowercaseAll = false
12-
flags.uppercaseAll = true
13-
},
14-
L: (flags) => {
15-
flags.uppercaseAll = false
16-
flags.lowercaseAll = true
17-
},
18-
E: (flags) => {
19-
flags.uppercaseAll = false
20-
flags.lowercaseAll = false
21-
},
22-
r: (flags, result) => {
23-
result.push('\\r')
24-
},
25-
n: (flags, result) => {
26-
result.push('\\n')
27-
},
28-
$: (flags, result) => {
29-
result.push('$')
30-
}
31-
}
32-
33-
function transformText (str, flags) {
34-
if (flags.uppercaseAll) {
35-
return str.toUpperCase()
36-
} else if (flags.lowercaseAll) {
37-
return str.toLowerCase()
38-
} else if (flags.uppercaseNext) {
39-
flags.uppercaseNext = false
40-
return str.replace(/^./, s => s.toUpperCase())
41-
} else if (flags.lowercaseNext) {
42-
return str.replace(/^./, s => s.toLowerCase())
43-
}
44-
return str
45-
}
1+
const Replacer = require('./replacer')
462

473
class Insertion {
48-
constructor ({ range, substitution, references }) {
4+
constructor ({range, substitution, references}) {
495
this.range = range
506
this.substitution = substitution
517
this.references = references
528
if (substitution) {
539
if (substitution.replace === undefined) {
5410
substitution.replace = ''
5511
}
56-
this.replacer = this.makeReplacer(substitution.replace)
12+
this.replacer = new Replacer(substitution.replace)
5713
}
5814
}
5915

6016
isTransformation () {
6117
return !!this.substitution
6218
}
6319

64-
makeReplacer (replace) {
65-
return function replacer (...match) {
66-
let flags = {
67-
uppercaseAll: false,
68-
lowercaseAll: false,
69-
uppercaseNext: false,
70-
lowercaseNext: false
71-
}
72-
replace = [...replace]
73-
let result = []
74-
replace.forEach(token => {
75-
if (typeof token === 'string') {
76-
result.push(transformText(token, flags))
77-
} else if (token.escape) {
78-
ESCAPES[token.escape](flags, result)
79-
} else if (token.backreference) {
80-
let transformed = transformText(match[token.backreference], flags)
81-
result.push(transformed)
82-
}
83-
})
84-
return result.join('')
85-
}
86-
}
87-
8820
transform (input) {
89-
let { substitution } = this
21+
let {substitution} = this
9022
if (!substitution) { return input }
91-
return input.replace(substitution.find, this.replacer)
23+
this.replacer.resetFlags()
24+
return input.replace(substitution.find, (...args) => {
25+
let result = this.replacer.replace(...args)
26+
return result
27+
})
9228
}
9329
}
9430

lib/replacer.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const ESCAPES = {
2+
u: (flags) => {
3+
flags.lowercaseNext = false
4+
flags.uppercaseNext = true
5+
},
6+
l: (flags) => {
7+
flags.uppercaseNext = false
8+
flags.lowercaseNext = true
9+
},
10+
U: (flags) => {
11+
flags.lowercaseAll = false
12+
flags.uppercaseAll = true
13+
},
14+
L: (flags) => {
15+
flags.uppercaseAll = false
16+
flags.lowercaseAll = true
17+
},
18+
E: (flags) => {
19+
flags.uppercaseAll = false
20+
flags.lowercaseAll = false
21+
},
22+
r: (flags, result) => {
23+
result.push('\\r')
24+
},
25+
n: (flags, result) => {
26+
result.push('\\n')
27+
},
28+
$: (flags, result) => {
29+
result.push('$')
30+
}
31+
}
32+
33+
function transformTextWithFlags (str, flags) {
34+
if (flags.uppercaseAll) {
35+
return str.toUpperCase()
36+
} else if (flags.lowercaseAll) {
37+
return str.toLowerCase()
38+
} else if (flags.uppercaseNext) {
39+
flags.uppercaseNext = false
40+
return str.replace(/^./, s => s.toUpperCase())
41+
} else if (flags.lowercaseNext) {
42+
return str.replace(/^./, s => s.toLowerCase())
43+
}
44+
return str
45+
}
46+
47+
48+
// `Replacer` handles shared substitution semantics for tabstop and variable
49+
// transformations.
50+
class Replacer {
51+
constructor (tokens) {
52+
this.tokens = [...tokens]
53+
this.resetFlags()
54+
}
55+
56+
resetFlags () {
57+
this.flags = {
58+
uppercaseAll: false,
59+
lowercaseAll: false,
60+
uppercaseNext: false,
61+
lowercaseNext: false
62+
}
63+
}
64+
65+
replace (...match) {
66+
let result = []
67+
68+
function handleToken (token) {
69+
if (typeof token === 'string') {
70+
result.push(transformTextWithFlags(token, this.flags))
71+
} else if (token.escape) {
72+
ESCAPES[token.escape](this.flags, result)
73+
} else if (token.backreference) {
74+
let {iftext, elsetext} = token
75+
if (iftext != null && elsetext != null) {
76+
// If-else syntax makes choices based on the presence or absence of a
77+
// capture group backreference.
78+
let m = match[token.backreference]
79+
let tokenToHandle = m ? iftext : elsetext
80+
if (Array.isArray(tokenToHandle)) {
81+
result.push(...tokenToHandle.map(handleToken.bind(this)))
82+
} else {
83+
result.push(handleToken.call(this, tokenToHandle))
84+
}
85+
} else {
86+
let transformed = transformTextWithFlags(
87+
match[token.backreference],
88+
this.flags
89+
)
90+
result.push(transformed)
91+
}
92+
}
93+
}
94+
95+
this.tokens.forEach(handleToken.bind(this))
96+
return result.join('')
97+
}
98+
}
99+
100+
module.exports = Replacer

0 commit comments

Comments
 (0)