Skip to content

Commit

Permalink
Merge pull request #11 from savetheclocktower/add-variables
Browse files Browse the repository at this point in the history
Add variables
  • Loading branch information
savetheclocktower committed Jun 12, 2023
2 parents bb00f90 + 44c0760 commit 78cb1c9
Show file tree
Hide file tree
Showing 12 changed files with 1,816 additions and 236 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
.tool-versions
96 changes: 78 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Snippets files are stored in a package's `snippets/` folder and also loaded from

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

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

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

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

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

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

### The ‘prefix’ key

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.
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.

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

### The ‘command’ key

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`.

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`.
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**.

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.
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**.

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

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.

### Optional parameters

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

### Determining the correct scope for a snippet

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*:
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`:

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

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)
(<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`.
If it's difficult to determine the package handling the file type in question (for example, for `.md`-documents), you can use another approach:

1. Put your cursor in a file in which you want the snippet to be available.
2. Open the [Command Palette](https://github.com/pulsar-edit/command-palette)
(<kbd>cmd-shift-p</kbd> or <kbd>ctrl-shift-p</kbd>).
3. 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`.

## Snippet syntax

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].

The following features from TextMate snippets are not yet supported:

* 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.

The following features from VSCode snippets are not yet supported:

* “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.

### Variables

Pulsar snippets support all of the variables mentioned in the [LSP specification][lsp], plus many of the variables [supported by VSCode][vscode].

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}`.

### Snippet syntax
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.)

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

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

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

### Multi-line Snippet Body
#### Variable transformation flags

Pulsar supports the three flags defined in the [LSP snippets specification][lsp] and two other flags that are [implemented in VSCode][vscode]:

* `/upcase` (`foo``FOO`)
* `/downcase` (`BAR``bar`)
* `/capitalize` (`lorem ipsum dolor``Lorem ipsum dolor`)
* `/camelcase` (`foo bar``fooBar`, `lorem-ipsum.dolor``loremIpsumDolor`)
* `/pascalcase` (`foo bar``FooBar`, `lorem-ipsum.dolor``LoremIpsumDolor`)

#### Variable caveats

* `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.
* `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.

#### Variables that are not yet supported

Of the variables supported by VSCode, Pulsar does not yet support:

* `UUID`
* `BLOCK_COMMENT_START`
* `BLOCK_COMMENT_END`
* `LINE_COMMENT`

## Multi-line Snippet Body

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

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

### Escaping Characters
## Escaping Characters

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:

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

### Multiple snippets for the same scope
Likewise, if your snippet includes literal references to `$` or `{`, you may have to escape those with two backslashes as well, depending on the context.

## Multiple snippets for the same scope

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.


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.
[lsp]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#variables
[vscode]: https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables
82 changes: 9 additions & 73 deletions lib/insertion.js
Original file line number Diff line number Diff line change
@@ -1,94 +1,30 @@
const ESCAPES = {
u: (flags) => {
flags.lowercaseNext = false
flags.uppercaseNext = true
},
l: (flags) => {
flags.uppercaseNext = false
flags.lowercaseNext = true
},
U: (flags) => {
flags.lowercaseAll = false
flags.uppercaseAll = true
},
L: (flags) => {
flags.uppercaseAll = false
flags.lowercaseAll = true
},
E: (flags) => {
flags.uppercaseAll = false
flags.lowercaseAll = false
},
r: (flags, result) => {
result.push('\\r')
},
n: (flags, result) => {
result.push('\\n')
},
$: (flags, result) => {
result.push('$')
}
}

function transformText (str, flags) {
if (flags.uppercaseAll) {
return str.toUpperCase()
} else if (flags.lowercaseAll) {
return str.toLowerCase()
} else if (flags.uppercaseNext) {
flags.uppercaseNext = false
return str.replace(/^./, s => s.toUpperCase())
} else if (flags.lowercaseNext) {
return str.replace(/^./, s => s.toLowerCase())
}
return str
}
const Replacer = require('./replacer')

class Insertion {
constructor ({ range, substitution, references }) {
constructor ({range, substitution, references}) {
this.range = range
this.substitution = substitution
this.references = references
if (substitution) {
if (substitution.replace === undefined) {
substitution.replace = ''
}
this.replacer = this.makeReplacer(substitution.replace)
this.replacer = new Replacer(substitution.replace)
}
}

isTransformation () {
return !!this.substitution
}

makeReplacer (replace) {
return function replacer (...match) {
let flags = {
uppercaseAll: false,
lowercaseAll: false,
uppercaseNext: false,
lowercaseNext: false
}
replace = [...replace]
let result = []
replace.forEach(token => {
if (typeof token === 'string') {
result.push(transformText(token, flags))
} else if (token.escape) {
ESCAPES[token.escape](flags, result)
} else if (token.backreference) {
let transformed = transformText(match[token.backreference], flags)
result.push(transformed)
}
})
return result.join('')
}
}

transform (input) {
let { substitution } = this
let {substitution} = this
if (!substitution) { return input }
return input.replace(substitution.find, this.replacer)
this.replacer.resetFlags()
return input.replace(substitution.find, (...args) => {
let result = this.replacer.replace(...args)
return result
})
}
}

Expand Down
100 changes: 100 additions & 0 deletions lib/replacer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const ESCAPES = {
u: (flags) => {
flags.lowercaseNext = false
flags.uppercaseNext = true
},
l: (flags) => {
flags.uppercaseNext = false
flags.lowercaseNext = true
},
U: (flags) => {
flags.lowercaseAll = false
flags.uppercaseAll = true
},
L: (flags) => {
flags.uppercaseAll = false
flags.lowercaseAll = true
},
E: (flags) => {
flags.uppercaseAll = false
flags.lowercaseAll = false
},
r: (flags, result) => {
result.push('\\r')
},
n: (flags, result) => {
result.push('\\n')
},
$: (flags, result) => {
result.push('$')
}
}

function transformTextWithFlags (str, flags) {
if (flags.uppercaseAll) {
return str.toUpperCase()
} else if (flags.lowercaseAll) {
return str.toLowerCase()
} else if (flags.uppercaseNext) {
flags.uppercaseNext = false
return str.replace(/^./, s => s.toUpperCase())
} else if (flags.lowercaseNext) {
return str.replace(/^./, s => s.toLowerCase())
}
return str
}


// `Replacer` handles shared substitution semantics for tabstop and variable
// transformations.
class Replacer {
constructor (tokens) {
this.tokens = [...tokens]
this.resetFlags()
}

resetFlags () {
this.flags = {
uppercaseAll: false,
lowercaseAll: false,
uppercaseNext: false,
lowercaseNext: false
}
}

replace (...match) {
let result = []

function handleToken (token) {
if (typeof token === 'string') {
result.push(transformTextWithFlags(token, this.flags))
} else if (token.escape) {
ESCAPES[token.escape](this.flags, result)
} else if (token.backreference) {
let {iftext, elsetext} = token
if (iftext != null && elsetext != null) {
// If-else syntax makes choices based on the presence or absence of a
// capture group backreference.
let m = match[token.backreference]
let tokenToHandle = m ? iftext : elsetext
if (Array.isArray(tokenToHandle)) {
result.push(...tokenToHandle.map(handleToken.bind(this)))
} else {
result.push(handleToken.call(this, tokenToHandle))
}
} else {
let transformed = transformTextWithFlags(
match[token.backreference],
this.flags
)
result.push(transformed)
}
}
}

this.tokens.forEach(handleToken.bind(this))
return result.join('')
}
}

module.exports = Replacer

0 comments on commit 78cb1c9

Please sign in to comment.