From 4f02ca3b71c3fa2cc0b279ca5ca64647fccc8664 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 28 Apr 2024 13:16:43 -0700 Subject: [PATCH 1/4] Bundle snippets --- packages/snippets/.eslintignore | 1 + packages/snippets/.eslintrc | 13 + packages/snippets/.gitignore | 2 + packages/snippets/.pairs | 16 + packages/snippets/CONTRIBUTING.md | 1 + packages/snippets/README.md | 208 ++ packages/snippets/keymaps/snippets-1.cson | 2 + packages/snippets/keymaps/snippets-2.cson | 6 + packages/snippets/lib/editor-store.js | 76 + packages/snippets/lib/helpers.js | 13 + packages/snippets/lib/insertion.js | 31 + packages/snippets/lib/replacer.js | 107 + .../snippets/lib/simple-transformations.js | 47 + packages/snippets/lib/snippet-body-parser.js | 18 + packages/snippets/lib/snippet-body.js | 2948 +++++++++++++++++ packages/snippets/lib/snippet-body.pegjs | 231 ++ packages/snippets/lib/snippet-expansion.js | 496 +++ .../snippets/lib/snippet-history-provider.js | 27 + packages/snippets/lib/snippet.js | 109 + packages/snippets/lib/snippets-available.js | 84 + packages/snippets/lib/snippets.cson | 57 + packages/snippets/lib/snippets.js | 936 ++++++ packages/snippets/lib/tab-stop-list.js | 48 + packages/snippets/lib/tab-stop.js | 61 + packages/snippets/lib/variable.js | 235 ++ packages/snippets/menus/snippets.cson | 12 + packages/snippets/package-lock.json | 2574 ++++++++++++++ packages/snippets/package.json | 31 + packages/snippets/spec/.eslintrc | 5 + packages/snippets/spec/body-parser-spec.js | 704 ++++ .../snippets/.hidden-file | 1 + .../snippets/invalid.json | 1 + .../snippets/.hidden-file | 1 + .../package-with-snippets/snippets/junk-file | 1 + .../package-with-snippets/snippets/test.cson | 31 + packages/snippets/spec/fixtures/sample.js | 13 + packages/snippets/spec/insertion-spec.js | 134 + .../snippets/spec/snippet-loading-spec.js | 345 ++ packages/snippets/spec/snippets-spec.js | 2017 +++++++++++ packages/snippets/spec/variable-spec.js | 67 + 40 files changed, 11710 insertions(+) create mode 100644 packages/snippets/.eslintignore create mode 100644 packages/snippets/.eslintrc create mode 100644 packages/snippets/.gitignore create mode 100644 packages/snippets/.pairs create mode 100644 packages/snippets/CONTRIBUTING.md create mode 100644 packages/snippets/README.md create mode 100644 packages/snippets/keymaps/snippets-1.cson create mode 100644 packages/snippets/keymaps/snippets-2.cson create mode 100644 packages/snippets/lib/editor-store.js create mode 100644 packages/snippets/lib/helpers.js create mode 100644 packages/snippets/lib/insertion.js create mode 100644 packages/snippets/lib/replacer.js create mode 100644 packages/snippets/lib/simple-transformations.js create mode 100644 packages/snippets/lib/snippet-body-parser.js create mode 100644 packages/snippets/lib/snippet-body.js create mode 100644 packages/snippets/lib/snippet-body.pegjs create mode 100644 packages/snippets/lib/snippet-expansion.js create mode 100644 packages/snippets/lib/snippet-history-provider.js create mode 100644 packages/snippets/lib/snippet.js create mode 100644 packages/snippets/lib/snippets-available.js create mode 100644 packages/snippets/lib/snippets.cson create mode 100644 packages/snippets/lib/snippets.js create mode 100644 packages/snippets/lib/tab-stop-list.js create mode 100644 packages/snippets/lib/tab-stop.js create mode 100644 packages/snippets/lib/variable.js create mode 100644 packages/snippets/menus/snippets.cson create mode 100644 packages/snippets/package-lock.json create mode 100644 packages/snippets/package.json create mode 100644 packages/snippets/spec/.eslintrc create mode 100644 packages/snippets/spec/body-parser-spec.js create mode 100644 packages/snippets/spec/fixtures/package-with-broken-snippets/snippets/.hidden-file create mode 100644 packages/snippets/spec/fixtures/package-with-broken-snippets/snippets/invalid.json create mode 100644 packages/snippets/spec/fixtures/package-with-snippets/snippets/.hidden-file create mode 100644 packages/snippets/spec/fixtures/package-with-snippets/snippets/junk-file create mode 100644 packages/snippets/spec/fixtures/package-with-snippets/snippets/test.cson create mode 100644 packages/snippets/spec/fixtures/sample.js create mode 100644 packages/snippets/spec/insertion-spec.js create mode 100644 packages/snippets/spec/snippet-loading-spec.js create mode 100644 packages/snippets/spec/snippets-spec.js create mode 100644 packages/snippets/spec/variable-spec.js diff --git a/packages/snippets/.eslintignore b/packages/snippets/.eslintignore new file mode 100644 index 000000000..e868bcf3f --- /dev/null +++ b/packages/snippets/.eslintignore @@ -0,0 +1 @@ +*.pegjs diff --git a/packages/snippets/.eslintrc b/packages/snippets/.eslintrc new file mode 100644 index 000000000..13b430eff --- /dev/null +++ b/packages/snippets/.eslintrc @@ -0,0 +1,13 @@ +{ + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2022 + }, + "rules": { + "indent": ["error", 2], + "linebreak-style": ["error", "unix"], + "object-curly-spacing": ["error", "never"], + "space-before-function-paren": ["error", "always"], + "semi": ["error", "never"] + } +} diff --git a/packages/snippets/.gitignore b/packages/snippets/.gitignore new file mode 100644 index 000000000..173600315 --- /dev/null +++ b/packages/snippets/.gitignore @@ -0,0 +1,2 @@ +node_modules +.tool-versions diff --git a/packages/snippets/.pairs b/packages/snippets/.pairs new file mode 100644 index 000000000..91845b111 --- /dev/null +++ b/packages/snippets/.pairs @@ -0,0 +1,16 @@ +pairs: + ns: Nathan Sobo; nathan + cj: Corey Johnson; cj + dg: David Graham; dgraham + ks: Kevin Sawicki; kevin + jc: Jerry Cheung; jerry + bl: Brian Lopez; brian + jp: Justin Palmer; justin + gt: Garen Torikian; garen + mc: Matt Colyer; mcolyer + bo: Ben Ogle; benogle + jr: Jason Rudolph; jasonrudolph + jl: Jessica Lord; jlord +email: + domain: github.com +#global: true diff --git a/packages/snippets/CONTRIBUTING.md b/packages/snippets/CONTRIBUTING.md new file mode 100644 index 000000000..9c8ac3e5b --- /dev/null +++ b/packages/snippets/CONTRIBUTING.md @@ -0,0 +1 @@ +[See how you can contribute](https://github.com/pulsar-edit/.github/blob/main/CONTRIBUTING.md) diff --git a/packages/snippets/README.md b/packages/snippets/README.md new file mode 100644 index 000000000..5f15fe4e9 --- /dev/null +++ b/packages/snippets/README.md @@ -0,0 +1,208 @@ +# Snippets package + +Expand snippets matching the current prefix with tab in Pulsar. + +To add your own snippets, select the _Pulsar > Snippets..._ menu option if you're using macOS, or the _File > Snippets..._ menu option if you're using Windows, or the _Edit > Snippets..._ menu option if you are using Linux. + +## Snippet Format + +Snippets files are stored in a package's `snippets/` folder and also loaded from `~/.pulsar/snippets.cson`. They can be either `.json` or `.cson` file types. + +```coffee +'.source.js': + 'console.log': + 'prefix': 'log' + 'command': 'insert-console-log' + 'body': 'console.log(${1:"crash"});$2' +``` + +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. 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. + +`$` followed by a number are the tabs stops which can be cycled between by pressing Tab once a snippet has been triggered. + +The above example adds a `console.log` snippet to JavaScript files that would expand to: + +```js +console.log("crash"); +``` + +The string `"crash"` would be initially selected and pressing tab again would place the cursor after the `;` + +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. In the above example, typing `log` (as its own word) and then pressing Tab 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 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). + +* `leftLabel` will add text to the left part of the autocomplete results box. +* `leftLabelHTML` will overwrite what's in `leftLabel` and allow you to use a bit of CSS such as `color`. +* `rightLabelHTML`. By default, in the right part of the results box you will see the name of the snippet. When using `rightLabelHTML` the name of the snippet will no longer be displayed, and you will be able to use a bit of CSS. +* `description` will add text to a description box under the autocomplete results list. +* `descriptionMoreURL` URL to the documentation of the snippet. + +![autocomplete-description](http://i.imgur.com/cvI2lOq.png) + +Example: +```coffee +'.source.js': + 'console.log': + 'prefix': 'log' + 'body': 'console.log(${1:"crash"});$2' + 'description': 'Output data to the console' + 'rightLabelHTML': 'JS' +``` + +### 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 (cmd-, 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 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) +(cmd-shift-p or ctrl-shift-p). +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}`). + +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 Tab trigger.) + +Others that can be useful: + +* `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. +* `LINE_COMMENT`, `BLOCK_COMMENT_START`, `BLOCK_COMMENT_END`: uses the correct comment delimiters for whatever language you’re in. + +Any variable that has no value — for instance, `TM_FILENAME` on an untitled document, or `LINE_COMMENT` in a CSS file — will resolve to an empty string. + +#### 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`) *(first letter uppercased; rest of input left intact)* +* `/camelcase` (`foo bar` → `fooBar`, `lorem-ipsum.dolor` → `loremIpsumDolor`) +* `/pascalcase` (`foo bar` → `FooBar`, `lorem-ipsum.dolor` → `LoremIpsumDolor`) + +It also supports two other common transformations: + +* `/snakecase` (`foo bar` → `foo_bar`, `lorem-ipsum.dolor` → `lorem_ipsum_dolor`) +* `/kebabcase` (`foo bar` → `foo-bar`, `lorem-ipsum.dolor` → `lorem-ipsum-dolor`) + +These transformation flags can also be applied on backreferences in `sed`-style replacements for transformed tab stops. Given the following example snippet body… + +``` +[$1] becomes [${1/(.*)/${1:/upcase}/}] +``` + +…invoking the snippet and typing `Lorem ipsum dolor` will produce: + +``` +[Lorem ipsum dolor] becomes [LOREM IPSUM DOLOR] +``` + + +#### 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` (Will automatically be supported when Pulsar uses a version of Electron that has native `crypto.randomUUID`.) + +## Multi-line Snippet Body + +You can also use multi-line syntax using `"""` for larger templates: + +```coffee +'.source.js': + 'if, else if, else': + 'prefix': 'ieie' + 'body': """ + if (${1:true}) { + $2 + } else if (${3:false}) { + $4 + } else { + $5 + } + """ +``` + +## 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: + +```coffee +'.source.js': + 'function': + 'prefix': 'funct' + 'body': """ + ${1:function () { + statements; + \\} + this line is also included in the snippet tab; + } + """ +``` + +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. + + +[lsp]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#variables +[vscode]: https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables diff --git a/packages/snippets/keymaps/snippets-1.cson b/packages/snippets/keymaps/snippets-1.cson new file mode 100644 index 000000000..ac786f45f --- /dev/null +++ b/packages/snippets/keymaps/snippets-1.cson @@ -0,0 +1,2 @@ +'atom-text-editor:not([mini])': + 'tab': 'snippets:expand' diff --git a/packages/snippets/keymaps/snippets-2.cson b/packages/snippets/keymaps/snippets-2.cson new file mode 100644 index 000000000..1ce10c9be --- /dev/null +++ b/packages/snippets/keymaps/snippets-2.cson @@ -0,0 +1,6 @@ +# it's critical that these bindings be loaded after those snippets-1 so they +# are later in the cascade, hence breaking the keymap into 2 files + +'atom-text-editor:not([mini])': + 'tab': 'snippets:next-tab-stop' + 'shift-tab': 'snippets:previous-tab-stop' diff --git a/packages/snippets/lib/editor-store.js b/packages/snippets/lib/editor-store.js new file mode 100644 index 000000000..44678b782 --- /dev/null +++ b/packages/snippets/lib/editor-store.js @@ -0,0 +1,76 @@ +const SnippetHistoryProvider = require('./snippet-history-provider') + +class EditorStore { + constructor (editor) { + this.editor = editor + this.buffer = this.editor.getBuffer() + this.observer = null + this.checkpoint = null + this.expansions = [] + this.existingHistoryProvider = null + } + + getExpansions () { + return this.expansions + } + + setExpansions (list) { + this.expansions = list + } + + clearExpansions () { + this.expansions = [] + } + + addExpansion (snippetExpansion) { + this.expansions.push(snippetExpansion) + } + + observeHistory (delegates) { + let isObservingHistory = this.existingHistoryProvider != null + if (isObservingHistory) { + return + } else { + this.existingHistoryProvider = this.buffer.historyProvider + } + + const newProvider = SnippetHistoryProvider(this.existingHistoryProvider, delegates) + this.buffer.setHistoryProvider(newProvider) + } + + stopObservingHistory (editor) { + if (this.existingHistoryProvider == null) { return } + this.buffer.setHistoryProvider(this.existingHistoryProvider) + this.existingHistoryProvider = null + } + + observe (callback) { + if (this.observer != null) { this.observer.dispose() } + this.observer = this.buffer.onDidChangeText(callback) + } + + stopObserving () { + if (this.observer == null) { return false } + this.observer.dispose() + this.observer = null + return true + } + + makeCheckpoint () { + const existing = this.checkpoint + if (existing) { + this.buffer.groupChangesSinceCheckpoint(existing) + } + this.checkpoint = this.buffer.createCheckpoint() + } +} + +EditorStore.store = new WeakMap() +EditorStore.findOrCreate = function (editor) { + if (!this.store.has(editor)) { + this.store.set(editor, new EditorStore(editor)) + } + return this.store.get(editor) +} + +module.exports = EditorStore diff --git a/packages/snippets/lib/helpers.js b/packages/snippets/lib/helpers.js new file mode 100644 index 000000000..0814a3dfb --- /dev/null +++ b/packages/snippets/lib/helpers.js @@ -0,0 +1,13 @@ +/** @babel */ + +import path from 'path' + +export function getPackageRoot() { + const {resourcePath} = atom.getLoadSettings() + const currentFileWasRequiredFromSnapshot = !path.isAbsolute(__dirname) + if (currentFileWasRequiredFromSnapshot) { + return path.join(resourcePath, 'node_modules', 'snippets') + } else { + return path.resolve(__dirname, '..') + } +} diff --git a/packages/snippets/lib/insertion.js b/packages/snippets/lib/insertion.js new file mode 100644 index 000000000..74fc09f12 --- /dev/null +++ b/packages/snippets/lib/insertion.js @@ -0,0 +1,31 @@ +const Replacer = require('./replacer') + +class Insertion { + constructor ({range, substitution, references}) { + this.range = range + this.substitution = substitution + this.references = references + if (substitution) { + if (substitution.replace === undefined) { + substitution.replace = '' + } + this.replacer = new Replacer(substitution.replace) + } + } + + isTransformation () { + return !!this.substitution + } + + transform (input) { + let {substitution} = this + if (!substitution) { return input } + this.replacer.resetFlags() + return input.replace(substitution.find, (...args) => { + let result = this.replacer.replace(...args) + return result + }) + } +} + +module.exports = Insertion diff --git a/packages/snippets/lib/replacer.js b/packages/snippets/lib/replacer.js new file mode 100644 index 000000000..75d2229d6 --- /dev/null +++ b/packages/snippets/lib/replacer.js @@ -0,0 +1,107 @@ +const FLAGS = require('./simple-transformations') + +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) { + if (token.transform && (token.transform in FLAGS)) { + let transformed = FLAGS[token.transform](match[token.backreference]) + result.push(transformed) + } else { + 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 diff --git a/packages/snippets/lib/simple-transformations.js b/packages/snippets/lib/simple-transformations.js new file mode 100644 index 000000000..fde568a8c --- /dev/null +++ b/packages/snippets/lib/simple-transformations.js @@ -0,0 +1,47 @@ +// Simple transformation flags that can convert a string in various ways. They +// are specified for variables and for transforming substitution +// backreferences, so we need to use them in two places. +const FLAGS = { + // These are included in the LSP spec. + upcase: value => (value || '').toLocaleUpperCase(), + downcase: value => (value || '').toLocaleLowerCase(), + capitalize: (value) => { + return !value ? '' : (value[0].toLocaleUpperCase() + value.substr(1)) + }, + + // These are supported by VSCode. + pascalcase (value) { + const match = value.match(/[a-z0-9]+/gi) + if (!match) { + return value + } + return match.map(word => { + return word.charAt(0).toUpperCase() + word.substr(1) + }).join('') + }, + camelcase (value) { + const match = value.match(/[a-z0-9]+/gi) + if (!match) { + return value + } + return match.map((word, index) => { + if (index === 0) { + return word.charAt(0).toLowerCase() + word.substr(1) + } + return word.charAt(0).toUpperCase() + word.substr(1) + }).join('') + }, + + // No reason not to implement these also. + snakecase (value) { + let camel = this.camelcase(value) + return camel.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`) + }, + + kebabcase (value) { + let camel = this.camelcase(value) + return camel.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) + } +} + +module.exports = FLAGS diff --git a/packages/snippets/lib/snippet-body-parser.js b/packages/snippets/lib/snippet-body-parser.js new file mode 100644 index 000000000..91ef024d6 --- /dev/null +++ b/packages/snippets/lib/snippet-body-parser.js @@ -0,0 +1,18 @@ +let parser +try { + // When the .pegjs file is stable and you're ready for release, run `npx + // pegjs lib/snippet-body.pegjs` to compile the parser. That way end users + // won't have to pay the cost of runtime evaluation. + parser = require('./snippet-body') +} catch (error) { + // When you're iterating on the parser, rename or delete `snippet-body.js` so + // you can make changes to the .pegjs file and have them reflected after a + // window reload. + const fs = require('fs') + const PEG = require('pegjs') + + const grammarSrc = fs.readFileSync(require.resolve('./snippet-body.pegjs'), 'utf8') + parser = PEG.generate(grammarSrc) +} + +module.exports = parser diff --git a/packages/snippets/lib/snippet-body.js b/packages/snippets/lib/snippet-body.js new file mode 100644 index 000000000..374e45b80 --- /dev/null +++ b/packages/snippets/lib/snippet-body.js @@ -0,0 +1,2948 @@ +/* + * Generated by PEG.js 0.10.0. + * + * http://pegjs.org/ + */ + +"use strict" + +function peg$subclass (child, parent) { + function ctor () { this.constructor = child } + ctor.prototype = parent.prototype + child.prototype = new ctor() +} + +function peg$SyntaxError (message, expected, found, location) { + this.message = message + this.expected = expected + this.found = found + this.location = location + this.name = "SyntaxError" + + if (typeof Error.captureStackTrace === "function") { + Error.captureStackTrace(this, peg$SyntaxError) + } +} + +peg$subclass(peg$SyntaxError, Error) + +peg$SyntaxError.buildMessage = function (expected, found) { + var DESCRIBE_EXPECTATION_FNS = { + literal: function (expectation) { + return "\"" + literalEscape(expectation.text) + "\"" + }, + + "class": function (expectation) { + var escapedParts = "", + i + + for (i = 0; i < expectation.parts.length; i++) { + escapedParts += expectation.parts[i] instanceof Array + ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1]) + : classEscape(expectation.parts[i]) + } + + return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]" + }, + + any: function (expectation) { + return "any character" + }, + + end: function (expectation) { + return "end of input" + }, + + other: function (expectation) { + return expectation.description + } + } + + function hex (ch) { + return ch.charCodeAt(0).toString(16).toUpperCase() + } + + function literalEscape (s) { + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function (ch) { return '\\x0' + hex(ch) }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) { return '\\x' + hex(ch) }) + } + + function classEscape (s) { + return s + .replace(/\\/g, '\\\\') + .replace(/\]/g, '\\]') + .replace(/\^/g, '\\^') + .replace(/-/g, '\\-') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function (ch) { return '\\x0' + hex(ch) }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) { return '\\x' + hex(ch) }) + } + + function describeExpectation (expectation) { + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation) + } + + function describeExpected (expected) { + var descriptions = new Array(expected.length), + i, j + + for (i = 0; i < expected.length; i++) { + descriptions[i] = describeExpectation(expected[i]) + } + + descriptions.sort() + + if (descriptions.length > 0) { + for (i = 1, j = 1; i < descriptions.length; i++) { + if (descriptions[i - 1] !== descriptions[i]) { + descriptions[j] = descriptions[i] + j++ + } + } + descriptions.length = j + } + + switch (descriptions.length) { + case 1: + return descriptions[0] + + case 2: + return descriptions[0] + " or " + descriptions[1] + + default: + return descriptions.slice(0, -1).join(", ") + + ", or " + + descriptions[descriptions.length - 1] + } + } + + function describeFound (found) { + return found ? "\"" + literalEscape(found) + "\"" : "end of input" + } + + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found." +} + +function peg$parse (input, options) { + options = options !== void 0 ? options : {} + + var peg$FAILED = {}, + + peg$startRuleFunctions = {bodyContent: peg$parsebodyContent}, + peg$startRuleFunction = peg$parsebodyContent, + + peg$c0 = function (content) { return content }, + peg$c1 = "$", + peg$c2 = peg$literalExpectation("$", false), + peg$c3 = function (index) { + return {index: makeInteger(index), content: []} + }, + peg$c4 = "${", + peg$c5 = peg$literalExpectation("${", false), + peg$c6 = "}", + peg$c7 = peg$literalExpectation("}", false), + peg$c8 = ":", + peg$c9 = peg$literalExpectation(":", false), + peg$c10 = function (index, content) { + return {index: makeInteger(index), content: content} + }, + peg$c11 = function (index, substitution) { + return { + index: makeInteger(index), + content: [], + substitution: substitution + } + }, + peg$c12 = "|", + peg$c13 = peg$literalExpectation("|", false), + peg$c14 = "|}", + peg$c15 = peg$literalExpectation("|}", false), + peg$c16 = function (index, choice) { + // Choice syntax requires an autocompleter to offer the user the options. As + // a fallback, we can take the first option and treat it as a placeholder. + const content = choice.length > 0 ? [choice[0]] : [] + return {index: makeInteger(index), choice: choice, content: content} + }, + peg$c17 = ",", + peg$c18 = peg$literalExpectation(",", false), + peg$c19 = function (elem, val) { return val }, + peg$c20 = function (elem, rest) { + return [elem, ...rest] + }, + peg$c21 = /^[^|,]/, + peg$c22 = peg$classExpectation(["|", ","], true, false), + peg$c23 = /^[^}]/, + peg$c24 = peg$classExpectation(["}"], true, false), + peg$c25 = function (barred) { return barred.join('') }, + peg$c26 = function (choicetext) { + return choicetext.join('') + }, + peg$c27 = "/", + peg$c28 = peg$literalExpectation("/", false), + peg$c29 = function (regex, replace, flags) { + return {find: new RegExp(regex, flags), replace: replace} + }, + peg$c30 = /^[^\/]/, + peg$c31 = peg$classExpectation(["/"], true, false), + peg$c32 = function (regex) { + return regex.join('') + }, + peg$c33 = function (index) { + return {backreference: makeInteger(index)} + }, + peg$c34 = function (index, caseTransform) { + return {backreference: makeInteger(index), transform: caseTransform} + }, + peg$c35 = ":+", + peg$c36 = peg$literalExpectation(":+", false), + peg$c37 = "", + peg$c38 = function (index, iftext) { + return {backreference: makeInteger(index), iftext: unwrap(iftext), elsetext: ''} + }, + peg$c39 = "(?", + peg$c40 = peg$literalExpectation("(?", false), + peg$c41 = ")", + peg$c42 = peg$literalExpectation(")", false), + peg$c43 = function (index, iftext) { + return {backreference: makeInteger(index), iftext: unwrap(iftext), elseText: ''} + }, + peg$c44 = ":-", + peg$c45 = peg$literalExpectation(":-", false), + peg$c46 = function (index, elsetext) { + return {backreference: makeInteger(index), iftext: '', elsetext: unwrap(elsetext)} + }, + peg$c47 = ":?", + peg$c48 = peg$literalExpectation(":?", false), + peg$c49 = function (index, iftext, elsetext) { + return {backreference: makeInteger(index), iftext: iftext, elsetext: elsetext} + }, + peg$c50 = "\\:", + peg$c51 = peg$literalExpectation("\\:", false), + peg$c52 = function () { return ':' }, + peg$c53 = /^[^:]/, + peg$c54 = peg$classExpectation([":"], true, false), + peg$c55 = function (text) { + return text.join('') + }, + peg$c56 = "\\", + peg$c57 = peg$literalExpectation("\\", false), + peg$c58 = /^[ULulErn]/, + peg$c59 = peg$classExpectation(["U", "L", "u", "l", "E", "r", "n"], false, false), + peg$c60 = function (flag) { + return {escape: flag} + }, + peg$c61 = /^[a-zA-Z]/, + peg$c62 = peg$classExpectation([["a", "z"], ["A", "Z"]], false, false), + peg$c63 = function (type) { + return type.join('') + }, + peg$c64 = function (char) { return char }, + peg$c65 = function (replacetext) { + return replacetext.join('') + }, + peg$c66 = function (name) { + return {variable: name} + }, + peg$c67 = function (name, content) { + return {variable: name, content: content} + }, + peg$c68 = function (name, substitution) { + return {variable: name, substitution: substitution} + }, + peg$c69 = ":/", + peg$c70 = peg$literalExpectation(":/", false), + peg$c71 = function (name, substitutionFlag) { + return {variable: name, substitution: {flag: substitutionFlag}} + }, + peg$c72 = /^[a-zA-Z_]/, + peg$c73 = peg$classExpectation([["a", "z"], ["A", "Z"], "_"], false, false), + peg$c74 = /^[a-zA-Z_0-9]/, + peg$c75 = peg$classExpectation([["a", "z"], ["A", "Z"], "_", ["0", "9"]], false, false), + peg$c76 = function (first, rest) { + return first + rest.join('') + }, + peg$c77 = /^[a-z]/, + peg$c78 = peg$classExpectation([["a", "z"]], false, false), + peg$c79 = function (chars) { + return chars.join('') + }, + peg$c80 = /^[0-9]/, + peg$c81 = peg$classExpectation([["0", "9"]], false, false), + peg$c82 = peg$anyExpectation(), + peg$c83 = function (char) { + switch (char) { + case '$': + case '\\': + case ':': + case '\x7D': // back brace; PEGjs would treat it as the JS scope end though + return char + default: + return '\\' + char + } + }, + peg$c84 = function (char) { + switch (char) { + case '$': + case '\\': + case '\x7D': + case '|': + case ',': + return char + default: + return '\\' + char + } + }, + peg$c85 = function (flags) { + return flags.join('') + }, + peg$c86 = function (text) { + return coalesce(text) + }, + peg$c87 = /^[^)]/, + peg$c88 = peg$classExpectation([")"], true, false), + + peg$currPos = 0, + peg$savedPos = 0, + peg$posDetailsCache = [{line: 1, column: 1}], + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + + peg$result + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\".") + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule] + } + + function text () { + return input.substring(peg$savedPos, peg$currPos) + } + + function location () { + return peg$computeLocation(peg$savedPos, peg$currPos) + } + + function expected (description, location) { + location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) + + throw peg$buildStructuredError( + [peg$otherExpectation(description)], + input.substring(peg$savedPos, peg$currPos), + location + ) + } + + function error (message, location) { + location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) + + throw peg$buildSimpleError(message, location) + } + + function peg$literalExpectation (text, ignoreCase) { + return {type: "literal", text: text, ignoreCase: ignoreCase} + } + + function peg$classExpectation (parts, inverted, ignoreCase) { + return {type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase} + } + + function peg$anyExpectation () { + return {type: "any"} + } + + function peg$endExpectation () { + return {type: "end"} + } + + function peg$otherExpectation (description) { + return {type: "other", description: description} + } + + function peg$computePosDetails (pos) { + var details = peg$posDetailsCache[pos], p + + if (details) { + return details + } else { + p = pos - 1 + while (!peg$posDetailsCache[p]) { + p-- + } + + details = peg$posDetailsCache[p] + details = { + line: details.line, + column: details.column + } + + while (p < pos) { + if (input.charCodeAt(p) === 10) { + details.line++ + details.column = 1 + } else { + details.column++ + } + + p++ + } + + peg$posDetailsCache[pos] = details + return details + } + } + + function peg$computeLocation (startPos, endPos) { + var startPosDetails = peg$computePosDetails(startPos), + endPosDetails = peg$computePosDetails(endPos) + + return { + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column + } + } + } + + function peg$fail (expected) { + if (peg$currPos < peg$maxFailPos) { return } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos + peg$maxFailExpected = [] + } + + peg$maxFailExpected.push(expected) + } + + function peg$buildSimpleError (message, location) { + return new peg$SyntaxError(message, null, null, location) + } + + function peg$buildStructuredError (expected, found, location) { + return new peg$SyntaxError( + peg$SyntaxError.buildMessage(expected, found), + expected, + found, + location + ) + } + + function peg$parsebodyContent () { + var s0, s1, s2 + + s0 = peg$currPos + s1 = [] + s2 = peg$parsetabstop() + if (s2 === peg$FAILED) { + s2 = peg$parsechoice() + if (s2 === peg$FAILED) { + s2 = peg$parsevariable() + if (s2 === peg$FAILED) { + s2 = peg$parsetext() + } + } + } + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parsetabstop() + if (s2 === peg$FAILED) { + s2 = peg$parsechoice() + if (s2 === peg$FAILED) { + s2 = peg$parsevariable() + if (s2 === peg$FAILED) { + s2 = peg$parsetext() + } + } + } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c0(s1) + } + s0 = s1 + + return s0 + } + + function peg$parseinnerBodyContent () { + var s0, s1, s2 + + s0 = peg$currPos + s1 = [] + s2 = peg$parsetabstop() + if (s2 === peg$FAILED) { + s2 = peg$parsechoice() + if (s2 === peg$FAILED) { + s2 = peg$parsevariable() + if (s2 === peg$FAILED) { + s2 = peg$parsenonCloseBraceText() + } + } + } + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parsetabstop() + if (s2 === peg$FAILED) { + s2 = peg$parsechoice() + if (s2 === peg$FAILED) { + s2 = peg$parsevariable() + if (s2 === peg$FAILED) { + s2 = peg$parsenonCloseBraceText() + } + } + } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c0(s1) + } + s0 = s1 + + return s0 + } + + function peg$parsetabstop () { + var s0 + + s0 = peg$parsesimpleTabstop() + if (s0 === peg$FAILED) { + s0 = peg$parsetabstopWithoutPlaceholder() + if (s0 === peg$FAILED) { + s0 = peg$parsetabstopWithPlaceholder() + if (s0 === peg$FAILED) { + s0 = peg$parsetabstopWithTransform() + } + } + } + + return s0 + } + + function peg$parsesimpleTabstop () { + var s0, s1, s2 + + s0 = peg$currPos + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c1 + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c2) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c3(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsetabstopWithoutPlaceholder () { + var s0, s1, s2, s3 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s3 = peg$c6 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c3(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsetabstopWithPlaceholder () { + var s0, s1, s2, s3, s4, s5 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c8 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c9) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseinnerBodyContent() + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s5 = peg$c6 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c10(s2, s4) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsetabstopWithTransform () { + var s0, s1, s2, s3, s4 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + s3 = peg$parsetransform() + if (s3 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s4 = peg$c6 + peg$currPos++ + } else { + s4 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c11(s2, s3) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsechoice () { + var s0, s1, s2, s3, s4, s5 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 124) { + s3 = peg$c12 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c13) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parsechoicecontents() + if (s4 !== peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c14) { + s5 = peg$c14 + peg$currPos += 2 + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c15) } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c16(s2, s4) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsechoicecontents () { + var s0, s1, s2, s3, s4, s5 + + s0 = peg$currPos + s1 = peg$parsechoicetext() + if (s1 !== peg$FAILED) { + s2 = [] + s3 = peg$currPos + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c17 + peg$currPos++ + } else { + s4 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c18) } + } + if (s4 !== peg$FAILED) { + s5 = peg$parsechoicetext() + if (s5 !== peg$FAILED) { + peg$savedPos = s3 + s4 = peg$c19(s1, s5) + s3 = s4 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + while (s3 !== peg$FAILED) { + s2.push(s3) + s3 = peg$currPos + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c17 + peg$currPos++ + } else { + s4 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c18) } + } + if (s4 !== peg$FAILED) { + s5 = peg$parsechoicetext() + if (s5 !== peg$FAILED) { + peg$savedPos = s3 + s4 = peg$c19(s1, s5) + s3 = s4 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c20(s1, s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsechoicetext () { + var s0, s1, s2, s3, s4, s5, s6 + + s0 = peg$currPos + s1 = [] + s2 = peg$parsechoiceEscaped() + if (s2 === peg$FAILED) { + if (peg$c21.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c22) } + } + if (s2 === peg$FAILED) { + s2 = peg$currPos + s3 = peg$currPos + if (input.charCodeAt(peg$currPos) === 124) { + s4 = peg$c12 + peg$currPos++ + } else { + s4 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c13) } + } + if (s4 !== peg$FAILED) { + s5 = peg$currPos + peg$silentFails++ + if (peg$c23.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s6 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c24) } + } + peg$silentFails-- + if (s6 !== peg$FAILED) { + peg$currPos = s5 + s5 = void 0 + } else { + s5 = peg$FAILED + } + if (s5 !== peg$FAILED) { + s4 = [s4, s5] + s3 = s4 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c25(s3) + } + s2 = s3 + } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parsechoiceEscaped() + if (s2 === peg$FAILED) { + if (peg$c21.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c22) } + } + if (s2 === peg$FAILED) { + s2 = peg$currPos + s3 = peg$currPos + if (input.charCodeAt(peg$currPos) === 124) { + s4 = peg$c12 + peg$currPos++ + } else { + s4 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c13) } + } + if (s4 !== peg$FAILED) { + s5 = peg$currPos + peg$silentFails++ + if (peg$c23.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s6 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c24) } + } + peg$silentFails-- + if (s6 !== peg$FAILED) { + peg$currPos = s5 + s5 = void 0 + } else { + s5 = peg$FAILED + } + if (s5 !== peg$FAILED) { + s4 = [s4, s5] + s3 = s4 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c25(s3) + } + s2 = s3 + } + } + } + } else { + s1 = peg$FAILED + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c26(s1) + } + s0 = s1 + + return s0 + } + + function peg$parsetransform () { + var s0, s1, s2, s3, s4, s5, s6 + + s0 = peg$currPos + if (input.charCodeAt(peg$currPos) === 47) { + s1 = peg$c27 + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c28) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseregexString() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 47) { + s3 = peg$c27 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c28) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parsereplace() + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 47) { + s5 = peg$c27 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c28) } + } + if (s5 !== peg$FAILED) { + s6 = peg$parseflags() + if (s6 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c29(s2, s4, s6) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseregexString () { + var s0, s1, s2 + + s0 = peg$currPos + s1 = [] + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + if (peg$c30.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c31) } + } + } + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + if (peg$c30.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c31) } + } + } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c32(s1) + } + s0 = s1 + + return s0 + } + + function peg$parsereplace () { + var s0, s1 + + s0 = [] + s1 = peg$parseformat() + if (s1 === peg$FAILED) { + s1 = peg$parsereplacetext() + } + while (s1 !== peg$FAILED) { + s0.push(s1) + s1 = peg$parseformat() + if (s1 === peg$FAILED) { + s1 = peg$parsereplacetext() + } + } + + return s0 + } + + function peg$parseformat () { + var s0 + + s0 = peg$parsesimpleFormat() + if (s0 === peg$FAILED) { + s0 = peg$parseformatWithoutPlaceholder() + if (s0 === peg$FAILED) { + s0 = peg$parseformatWithCaseTransform() + if (s0 === peg$FAILED) { + s0 = peg$parseformatWithIf() + if (s0 === peg$FAILED) { + s0 = peg$parseformatWithIfElse() + if (s0 === peg$FAILED) { + s0 = peg$parseformatWithElse() + if (s0 === peg$FAILED) { + s0 = peg$parseformatEscape() + if (s0 === peg$FAILED) { + s0 = peg$parseformatWithIfElseAlt() + if (s0 === peg$FAILED) { + s0 = peg$parseformatWithIfAlt() + } + } + } + } + } + } + } + } + + return s0 + } + + function peg$parsesimpleFormat () { + var s0, s1, s2 + + s0 = peg$currPos + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c1 + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c2) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c33(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseformatWithoutPlaceholder () { + var s0, s1, s2, s3 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s3 = peg$c6 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c33(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseformatWithCaseTransform () { + var s0, s1, s2, s3, s4, s5 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c8 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c9) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parsecaseTransform() + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s5 = peg$c6 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c34(s2, s4) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseformatWithIf () { + var s0, s1, s2, s3, s4, s5 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c35) { + s3 = peg$c35 + peg$currPos += 2 + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c36) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseifElseText() + if (s4 === peg$FAILED) { + s4 = peg$c37 + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s5 = peg$c6 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c38(s2, s4) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseformatWithIfAlt () { + var s0, s1, s2, s3, s4, s5 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c39) { + s1 = peg$c39 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c40) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c8 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c9) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseifTextAlt() + if (s4 === peg$FAILED) { + s4 = peg$c37 + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s5 = peg$c41 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c42) } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c43(s2, s4) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseformatWithElse () { + var s0, s1, s2, s3, s4, s5 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c44) { + s3 = peg$c44 + peg$currPos += 2 + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c45) } + } + if (s3 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c8 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c9) } + } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseifElseText() + if (s4 === peg$FAILED) { + s4 = peg$c37 + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s5 = peg$c6 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c46(s2, s4) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseformatWithIfElse () { + var s0, s1, s2, s3, s4, s5, s6, s7 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c47) { + s3 = peg$c47 + peg$currPos += 2 + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c48) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseifText() + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s5 = peg$c8 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c9) } + } + if (s5 !== peg$FAILED) { + s6 = peg$parseifElseText() + if (s6 === peg$FAILED) { + s6 = peg$c37 + } + if (s6 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s7 = peg$c6 + peg$currPos++ + } else { + s7 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s7 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c49(s2, s4, s6) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseformatWithIfElseAlt () { + var s0, s1, s2, s3, s4, s5, s6, s7 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c39) { + s1 = peg$c39 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c40) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseint() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c8 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c9) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseifTextAlt() + if (s4 === peg$FAILED) { + s4 = peg$c37 + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s5 = peg$c8 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c9) } + } + if (s5 !== peg$FAILED) { + s6 = peg$parseelseTextAlt() + if (s6 === peg$FAILED) { + s6 = peg$c37 + } + if (s6 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s7 = peg$c41 + peg$currPos++ + } else { + s7 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c42) } + } + if (s7 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c49(s2, s4, s6) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsenonColonText () { + var s0, s1, s2, s3 + + s0 = peg$currPos + s1 = [] + s2 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c50) { + s3 = peg$c50 + peg$currPos += 2 + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c51) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c52() + } + s2 = s3 + if (s2 === peg$FAILED) { + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + if (peg$c53.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c54) } + } + } + } + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c50) { + s3 = peg$c50 + peg$currPos += 2 + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c51) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c52() + } + s2 = s3 + if (s2 === peg$FAILED) { + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + if (peg$c53.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c54) } + } + } + } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c55(s1) + } + s0 = s1 + + return s0 + } + + function peg$parseformatEscape () { + var s0, s1, s2 + + s0 = peg$currPos + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c56 + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c57) } + } + if (s1 !== peg$FAILED) { + if (peg$c58.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c59) } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c60(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsecaseTransform () { + var s0, s1, s2, s3 + + s0 = peg$currPos + if (input.charCodeAt(peg$currPos) === 47) { + s1 = peg$c27 + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c28) } + } + if (s1 !== peg$FAILED) { + s2 = [] + if (peg$c61.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c62) } + } + while (s3 !== peg$FAILED) { + s2.push(s3) + if (peg$c61.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c62) } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c63(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsereplacetext () { + var s0, s1, s2, s3, s4 + + s0 = peg$currPos + s1 = [] + s2 = peg$currPos + s3 = peg$currPos + peg$silentFails++ + s4 = peg$parseformatEscape() + peg$silentFails-- + if (s4 === peg$FAILED) { + s3 = void 0 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + s4 = peg$parseescaped() + if (s4 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s4) + s2 = s3 + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + if (s2 === peg$FAILED) { + s2 = peg$currPos + s3 = peg$currPos + peg$silentFails++ + s4 = peg$parseformat() + peg$silentFails-- + if (s4 === peg$FAILED) { + s3 = void 0 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + if (peg$c30.test(input.charAt(peg$currPos))) { + s4 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s4 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c31) } + } + if (s4 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s4) + s2 = s3 + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$currPos + s3 = peg$currPos + peg$silentFails++ + s4 = peg$parseformatEscape() + peg$silentFails-- + if (s4 === peg$FAILED) { + s3 = void 0 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + s4 = peg$parseescaped() + if (s4 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s4) + s2 = s3 + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + if (s2 === peg$FAILED) { + s2 = peg$currPos + s3 = peg$currPos + peg$silentFails++ + s4 = peg$parseformat() + peg$silentFails-- + if (s4 === peg$FAILED) { + s3 = void 0 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + if (peg$c30.test(input.charAt(peg$currPos))) { + s4 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s4 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c31) } + } + if (s4 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s4) + s2 = s3 + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } + } + } else { + s1 = peg$FAILED + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c65(s1) + } + s0 = s1 + + return s0 + } + + function peg$parsevariable () { + var s0 + + s0 = peg$parsesimpleVariable() + if (s0 === peg$FAILED) { + s0 = peg$parsevariableWithSimpleTransform() + if (s0 === peg$FAILED) { + s0 = peg$parsevariableWithoutPlaceholder() + if (s0 === peg$FAILED) { + s0 = peg$parsevariableWithPlaceholder() + if (s0 === peg$FAILED) { + s0 = peg$parsevariableWithTransform() + } + } + } + } + + return s0 + } + + function peg$parsesimpleVariable () { + var s0, s1, s2 + + s0 = peg$currPos + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c1 + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c2) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsevariableName() + if (s2 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c66(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsevariableWithoutPlaceholder () { + var s0, s1, s2, s3 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsevariableName() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s3 = peg$c6 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c66(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsevariableWithPlaceholder () { + var s0, s1, s2, s3, s4, s5 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsevariableName() + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c8 + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c9) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseinnerBodyContent() + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s5 = peg$c6 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c67(s2, s4) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsevariableWithTransform () { + var s0, s1, s2, s3, s4 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsevariableName() + if (s2 !== peg$FAILED) { + s3 = peg$parsetransform() + if (s3 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s4 = peg$c6 + peg$currPos++ + } else { + s4 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c68(s2, s3) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsevariableWithSimpleTransform () { + var s0, s1, s2, s3, s4, s5 + + s0 = peg$currPos + if (input.substr(peg$currPos, 2) === peg$c4) { + s1 = peg$c4 + peg$currPos += 2 + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c5) } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsevariableName() + if (s2 !== peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c69) { + s3 = peg$c69 + peg$currPos += 2 + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c70) } + } + if (s3 !== peg$FAILED) { + s4 = peg$parsesubstitutionFlag() + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s5 = peg$c6 + peg$currPos++ + } else { + s5 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c7) } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c71(s2, s4) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsevariableName () { + var s0, s1, s2, s3 + + s0 = peg$currPos + if (peg$c72.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c73) } + } + if (s1 !== peg$FAILED) { + s2 = [] + if (peg$c74.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c75) } + } + while (s3 !== peg$FAILED) { + s2.push(s3) + if (peg$c74.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c75) } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c76(s1, s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsesubstitutionFlag () { + var s0, s1, s2 + + s0 = peg$currPos + s1 = [] + if (peg$c77.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c78) } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2) + if (peg$c77.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c78) } + } + } + } else { + s1 = peg$FAILED + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c79(s1) + } + s0 = s1 + + return s0 + } + + function peg$parseint () { + var s0, s1 + + s0 = [] + if (peg$c80.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c81) } + } + if (s1 !== peg$FAILED) { + while (s1 !== peg$FAILED) { + s0.push(s1) + if (peg$c80.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c81) } + } + } + } else { + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseescaped () { + var s0, s1, s2 + + s0 = peg$currPos + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c56 + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c57) } + } + if (s1 !== peg$FAILED) { + if (input.length > peg$currPos) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c82) } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c83(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parsechoiceEscaped () { + var s0, s1, s2 + + s0 = peg$currPos + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c56 + peg$currPos++ + } else { + s1 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c57) } + } + if (s1 !== peg$FAILED) { + if (input.length > peg$currPos) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c82) } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c84(s2) + s0 = s1 + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + } else { + peg$currPos = s0 + s0 = peg$FAILED + } + + return s0 + } + + function peg$parseflags () { + var s0, s1, s2 + + s0 = peg$currPos + s1 = [] + if (peg$c77.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c78) } + } + while (s2 !== peg$FAILED) { + s1.push(s2) + if (peg$c77.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s2 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c78) } + } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c85(s1) + } + s0 = s1 + + return s0 + } + + function peg$parsetext () { + var s0, s1, s2, s3, s4, s5, s6 + + s0 = peg$currPos + s1 = [] + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + s3 = peg$currPos + peg$silentFails++ + s4 = peg$parsetabstop() + peg$silentFails-- + if (s4 === peg$FAILED) { + s3 = void 0 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + s4 = peg$currPos + peg$silentFails++ + s5 = peg$parsevariable() + peg$silentFails-- + if (s5 === peg$FAILED) { + s4 = void 0 + } else { + peg$currPos = s4 + s4 = peg$FAILED + } + if (s4 !== peg$FAILED) { + s5 = peg$currPos + peg$silentFails++ + s6 = peg$parsechoice() + peg$silentFails-- + if (s6 === peg$FAILED) { + s5 = void 0 + } else { + peg$currPos = s5 + s5 = peg$FAILED + } + if (s5 !== peg$FAILED) { + if (input.length > peg$currPos) { + s6 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s6 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c82) } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s6) + s2 = s3 + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + s3 = peg$currPos + peg$silentFails++ + s4 = peg$parsetabstop() + peg$silentFails-- + if (s4 === peg$FAILED) { + s3 = void 0 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + s4 = peg$currPos + peg$silentFails++ + s5 = peg$parsevariable() + peg$silentFails-- + if (s5 === peg$FAILED) { + s4 = void 0 + } else { + peg$currPos = s4 + s4 = peg$FAILED + } + if (s4 !== peg$FAILED) { + s5 = peg$currPos + peg$silentFails++ + s6 = peg$parsechoice() + peg$silentFails-- + if (s6 === peg$FAILED) { + s5 = void 0 + } else { + peg$currPos = s5 + s5 = peg$FAILED + } + if (s5 !== peg$FAILED) { + if (input.length > peg$currPos) { + s6 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s6 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c82) } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s6) + s2 = s3 + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } + } + } else { + s1 = peg$FAILED + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c55(s1) + } + s0 = s1 + + return s0 + } + + function peg$parsenonCloseBraceText () { + var s0, s1, s2, s3, s4, s5, s6 + + s0 = peg$currPos + s1 = [] + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + s3 = peg$currPos + peg$silentFails++ + s4 = peg$parsetabstop() + peg$silentFails-- + if (s4 === peg$FAILED) { + s3 = void 0 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + s4 = peg$currPos + peg$silentFails++ + s5 = peg$parsevariable() + peg$silentFails-- + if (s5 === peg$FAILED) { + s4 = void 0 + } else { + peg$currPos = s4 + s4 = peg$FAILED + } + if (s4 !== peg$FAILED) { + s5 = peg$currPos + peg$silentFails++ + s6 = peg$parsechoice() + peg$silentFails-- + if (s6 === peg$FAILED) { + s5 = void 0 + } else { + peg$currPos = s5 + s5 = peg$FAILED + } + if (s5 !== peg$FAILED) { + if (peg$c23.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s6 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c24) } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s6) + s2 = s3 + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + s3 = peg$currPos + peg$silentFails++ + s4 = peg$parsetabstop() + peg$silentFails-- + if (s4 === peg$FAILED) { + s3 = void 0 + } else { + peg$currPos = s3 + s3 = peg$FAILED + } + if (s3 !== peg$FAILED) { + s4 = peg$currPos + peg$silentFails++ + s5 = peg$parsevariable() + peg$silentFails-- + if (s5 === peg$FAILED) { + s4 = void 0 + } else { + peg$currPos = s4 + s4 = peg$FAILED + } + if (s4 !== peg$FAILED) { + s5 = peg$currPos + peg$silentFails++ + s6 = peg$parsechoice() + peg$silentFails-- + if (s6 === peg$FAILED) { + s5 = void 0 + } else { + peg$currPos = s5 + s5 = peg$FAILED + } + if (s5 !== peg$FAILED) { + if (peg$c23.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s6 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c24) } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s6) + s2 = s3 + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } else { + peg$currPos = s2 + s2 = peg$FAILED + } + } + } + } else { + s1 = peg$FAILED + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c55(s1) + } + s0 = s1 + + return s0 + } + + function peg$parseifText () { + var s0, s1, s2, s3 + + s0 = peg$currPos + s1 = [] + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + if (peg$c53.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c54) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s3) + } + s2 = s3 + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + if (peg$c53.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c54) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s3) + } + s2 = s3 + } + } + } else { + s1 = peg$FAILED + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c55(s1) + } + s0 = s1 + + return s0 + } + + function peg$parseifElseText () { + var s0, s1, s2, s3 + + s0 = peg$currPos + s1 = [] + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + if (peg$c23.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c24) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s3) + } + s2 = s3 + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + if (peg$c23.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c24) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s3) + } + s2 = s3 + } + } + } else { + s1 = peg$FAILED + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c55(s1) + } + s0 = s1 + + return s0 + } + + function peg$parseifTextAlt () { + var s0, s1, s2, s3 + + s0 = peg$currPos + s1 = [] + s2 = peg$parseformatEscape() + if (s2 === peg$FAILED) { + s2 = peg$parseformat() + if (s2 === peg$FAILED) { + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + if (peg$c53.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c54) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s3) + } + s2 = s3 + } + } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parseformatEscape() + if (s2 === peg$FAILED) { + s2 = peg$parseformat() + if (s2 === peg$FAILED) { + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + if (peg$c53.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c54) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s3) + } + s2 = s3 + } + } + } + } + } else { + s1 = peg$FAILED + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c86(s1) + } + s0 = s1 + + return s0 + } + + function peg$parseelseTextAlt () { + var s0, s1, s2, s3 + + s0 = peg$currPos + s1 = [] + s2 = peg$parseformatEscape() + if (s2 === peg$FAILED) { + s2 = peg$parseformat() + if (s2 === peg$FAILED) { + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + if (peg$c87.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c88) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s3) + } + s2 = s3 + } + } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2) + s2 = peg$parseformatEscape() + if (s2 === peg$FAILED) { + s2 = peg$parseformat() + if (s2 === peg$FAILED) { + s2 = peg$parseescaped() + if (s2 === peg$FAILED) { + s2 = peg$currPos + if (peg$c87.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos) + peg$currPos++ + } else { + s3 = peg$FAILED + if (peg$silentFails === 0) { peg$fail(peg$c88) } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2 + s3 = peg$c64(s3) + } + s2 = s3 + } + } + } + } + } else { + s1 = peg$FAILED + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0 + s1 = peg$c86(s1) + } + s0 = s1 + + return s0 + } + + + function makeInteger (i) { + return parseInt(i.join(''), 10) + } + + function coalesce (parts) { + const result = [] + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + const ri = result.length - 1 + if (typeof part === 'string' && typeof result[ri] === 'string') { + result[ri] = result[ri] + part + } else { + result.push(part) + } + } + return result + } + + function unwrap (val) { + let shouldUnwrap = Array.isArray(val) && val.length === 1 && typeof val[0] === 'string' + return shouldUnwrap ? val[0] : val + } + + + + peg$result = peg$startRuleFunction() + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result + } else { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail(peg$endExpectation()) + } + + throw peg$buildStructuredError( + peg$maxFailExpected, + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ) + } +} + +module.exports = { + SyntaxError: peg$SyntaxError, + parse: peg$parse +} diff --git a/packages/snippets/lib/snippet-body.pegjs b/packages/snippets/lib/snippet-body.pegjs new file mode 100644 index 000000000..1e83e1202 --- /dev/null +++ b/packages/snippets/lib/snippet-body.pegjs @@ -0,0 +1,231 @@ + +{ + // If you're making changes to this file, be sure to re-compile afterward + // using the instructions in `snippet-body-parser.js`. + + function makeInteger(i) { + return parseInt(i.join(''), 10); + } + + function coalesce (parts) { + const result = []; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const ri = result.length - 1; + if (typeof part === 'string' && typeof result[ri] === 'string') { + result[ri] = result[ri] + part; + } else { + result.push(part); + } + } + return result; + } + + function unwrap (val) { + let shouldUnwrap = Array.isArray(val) && val.length === 1 && typeof val[0] === 'string'; + return shouldUnwrap ? val[0] : val; + } + +} + +bodyContent = content:(tabstop / choice / variable / text)* { return content; } + +innerBodyContent = content:(tabstop / choice / variable / nonCloseBraceText)* { return content; } + +tabstop = simpleTabstop / tabstopWithoutPlaceholder / tabstopWithPlaceholder / tabstopWithTransform + +simpleTabstop = '$' index:int { + return {index: makeInteger(index), content: []} +} + +tabstopWithoutPlaceholder = '${' index:int '}' { + return {index: makeInteger(index), content: []} +} + +tabstopWithPlaceholder = '${' index:int ':' content:innerBodyContent '}' { + return {index: makeInteger(index), content: content} +} + +tabstopWithTransform = '${' index:int substitution:transform '}' { + return { + index: makeInteger(index), + content: [], + substitution: substitution + } +} + +choice = '${' index:int '|' choice:choicecontents '|}' { + // Choice syntax requires an autocompleter to offer the user the options. As + // a fallback, we can take the first option and treat it as a placeholder. + const content = choice.length > 0 ? [choice[0]] : [] + return {index: makeInteger(index), choice: choice, content: content} +} + +choicecontents = elem:choicetext rest:(',' val:choicetext { return val } )* { + return [elem, ...rest] +} + +choicetext = choicetext:(choiceEscaped / [^|,] / barred:('|' &[^}]) { return barred.join('') } )+ { + return choicetext.join('') +} + +transform = '/' regex:regexString '/' replace:replace '/' flags:flags { + return {find: new RegExp(regex, flags), replace: replace} +} + +regexString = regex:(escaped / [^/])* { + return regex.join('') +} + +replace = (format / replacetext)* + +format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatWithIf / formatWithIfElse / formatWithElse / formatEscape / formatWithIfElseAlt / formatWithIfAlt + +simpleFormat = '$' index:int { + return {backreference: makeInteger(index)} +} + +formatWithoutPlaceholder = '${' index:int '}' { + return {backreference: makeInteger(index)} +} + +formatWithCaseTransform = '${' index:int ':' caseTransform:caseTransform '}' { + return {backreference: makeInteger(index), transform: caseTransform} +} + +formatWithIf = '${' index:int ':+' iftext:(ifElseText / '') '}' { + return {backreference: makeInteger(index), iftext: unwrap(iftext), elsetext: ''} +} + +formatWithIfAlt = '(?' index:int ':' iftext:(ifTextAlt / '') ')' { + return {backreference: makeInteger(index), iftext: unwrap(iftext), elseText: '' } +} + +formatWithElse = '${' index:int (':-' / ':') elsetext:(ifElseText / '') '}' { + return {backreference: makeInteger(index), iftext: '', elsetext: unwrap(elsetext)} +} + +// Variable interpolation if-else; conditional clause queries the presence of a +// specific tabstop value. +formatWithIfElse = '${' index:int ':?' iftext:ifText ':' elsetext:(ifElseText / '') '}' { + return {backreference: makeInteger(index), iftext: iftext, elsetext: elsetext} +} + +// Substitution if-else; conditional clause tests whether a given regex capture +// group matched anything. +formatWithIfElseAlt = '(?' index:int ':' iftext:(ifTextAlt / '') ':' elsetext:(elseTextAlt / '') ')' { + return {backreference: makeInteger(index), iftext: iftext, elsetext: elsetext} +} + +nonColonText = text:('\\:' { return ':' } / escaped / [^:])* { + return text.join('') +} + +formatEscape = '\\' flag:[ULulErn] { + return {escape: flag} +} + +caseTransform = '/' type:[a-zA-Z]* { + return type.join('') +} + +replacetext = replacetext:(!formatEscape char:escaped { return char } / !format char:[^/] { return char })+ { + return replacetext.join('') +} + +variable = simpleVariable / variableWithSimpleTransform / variableWithoutPlaceholder / variableWithPlaceholder / variableWithTransform + +simpleVariable = '$' name:variableName { + return {variable: name} +} + +variableWithoutPlaceholder = '${' name:variableName '}' { + return {variable: name} +} + +variableWithPlaceholder = '${' name:variableName ':' content:innerBodyContent '}' { + return {variable: name, content: content} +} + +variableWithTransform = '${' name:variableName substitution:transform '}' { + return {variable: name, substitution: substitution} +} + +variableWithSimpleTransform = '${' name:variableName ':/' substitutionFlag:substitutionFlag '}' { + return {variable: name, substitution: {flag: substitutionFlag}} +} + +variableName = first:[a-zA-Z_] rest:[a-zA-Z_0-9]* { + return first + rest.join('') +} + +substitutionFlag = chars:[a-z]+ { + return chars.join('') +} + +int = [0-9]+ + +escaped = '\\' char:. { + switch (char) { + case '$': + case '\\': + case ':': + case '\x7D': // back brace; PEGjs would treat it as the JS scope end though + return char + default: + return '\\' + char + } +} + +choiceEscaped = '\\' char:. { + switch (char) { + case '$': + case '\\': + case '\x7D': + case '|': + case ',': + return char + default: + return '\\' + char + } +} + +flags = flags:[a-z]* { + return flags.join('') +} + +text = text:(escaped / !tabstop !variable !choice char:. { return char })+ { + return text.join('') +} + +nonCloseBraceText = text:(escaped / !tabstop !variable !choice char:[^}] { return char })+ { + return text.join('') +} + +// Two kinds of format string conditional syntax: the `${` flavor and the `(?` +// flavor. +// +// VSCode supports only the `${` flavor. It's easier to parse because the +// if-result and else-result can only be plain text, as per the specification. +// +// TextMate supports both. `(?` is more powerful, but also harder to parse, +// because it can contain special flags and regex backreferences. + +// For the first part of a two-part if-else. Runs until the `:` delimiter. +ifText = text:(escaped / char:[^:] { return char })+ { + return text.join('') +} + +// For either the second part of a two-part if-else OR the sole part of a +// one-part if/else. Runs until the `}` that ends the expression. +ifElseText = text:(escaped / char:[^}] { return char })+ { + return text.join('') +} + +ifTextAlt = text:(formatEscape / format / escaped / char:[^:] { return char })+ { + return coalesce(text); +} + +elseTextAlt = text:(formatEscape / format / escaped / char:[^)] { return char })+ { + return coalesce(text); +} diff --git a/packages/snippets/lib/snippet-expansion.js b/packages/snippets/lib/snippet-expansion.js new file mode 100644 index 000000000..859754f53 --- /dev/null +++ b/packages/snippets/lib/snippet-expansion.js @@ -0,0 +1,496 @@ +const {CompositeDisposable, Range, Point} = require('atom') + +module.exports = class SnippetExpansion { + constructor (snippet, editor, cursor, snippets, {method} = {}) { + this.settingTabStop = false + this.isIgnoringBufferChanges = false + this.onUndoOrRedo = this.onUndoOrRedo.bind(this) + this.snippet = snippet + this.editor = editor + this.cursor = cursor + this.snippets = snippets + this.subscriptions = new CompositeDisposable + this.selections = [this.cursor.selection] + + // Method refers to how the snippet was invoked; known values are `prefix` + // or `command`. If neither is present, then snippet was inserted + // programmatically. + this.method = method + + // Holds the `Insertion` instance corresponding to each tab stop marker. We + // don't use the tab stop's own numbering here; we renumber them + // consecutively starting at 0 in the order in which they should be + // visited. So `$1` (if present) will always be at index `0`, and `$0` (if + // present) will always be the last index. + this.insertionsByIndex = [] + + // Each insertion has a corresponding marker. We keep them in a map so we + // can easily reassociate an insertion with its new marker when we destroy + // its old one. + this.markersForInsertions = new Map() + + this.resolutionsForVariables = new Map() + this.markersForVariables = new Map() + + // The index of the active tab stop. + this.tabStopIndex = null + + // If, say, tab stop 4's placeholder references tab stop 2, then tab stop + // 4's insertion goes into this map as a "related" insertion to tab stop 2. + // We need to keep track of this because tab stop 4's marker will need to + // be replaced while 2 is the active index. + this.relatedInsertionsByIndex = new Map() + + const startPosition = this.cursor.selection.getBufferRange().start + let {body, tabStopList} = this.snippet + let tabStops = tabStopList.toArray() + + let indent = this.editor.lineTextForBufferRow(startPosition.row).match(/^\s*/)[0] + if (this.snippet.lineCount > 1 && indent) { + // Add proper leading indentation to the snippet + body = body.replace(/\n/g, `\n${indent}`) + + tabStops = tabStops.map(tabStop => tabStop.copyWithIndent(indent)) + } + + this.ignoringBufferChanges(() => { + this.editor.transact(() => { + // Determine what each variable reference will be replaced by + // _before_ we make any changes to the state of the editor. This + // affects $TM_SELECTED_TEXT, $TM_CURRENT_WORD, and others. + this.resolveVariables(startPosition) + // Insert the snippet body at the cursor. + const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) + // Mark the range we just inserted. Once we interpolate variables and + // apply transformations, the range may grow, and we need to keep + // track of that so we can normalize tabs later on. + const newRangeMarker = this.getMarkerLayer(this.editor).markBufferRange(newRange, {exclusive: false}) + + if (this.snippet.tabStopList.length > 0) { + // Listen for cursor changes so we can decide whether to keep the + // snippet active or terminate it. + this.subscriptions.add( + this.cursor.onDidChangePosition(event => this.cursorMoved(event)), + this.cursor.onDidDestroy(() => this.cursorDestroyed()) + ) + // First we'll add display markers for tab stops and variables. + // Both need these areas to be marked before any expansion happens + // so that they don't lose track of where their slots are. + this.placeTabStopMarkers(startPosition, tabStops) + this.markVariables(startPosition) + + // Now we'll expand variables. All markers in the previous step + // were defined with `exclusive: false`, so any that are affected + // by variable expansion will grow if necessary. + this.expandVariables(startPosition) + + // Now we'll make the first tab stop active and apply snippet + // transformations for the first time. As part of this process, + // most markers will be converted to `exclusive: true` and adjusted + // as necessary as the user tabs through the snippet. + this.setTabStopIndex(0) + this.applyAllTransformations() + + this.snippets.addExpansion(this.editor, this) + } else { + // No tab stops, so we're free to mark and expand variables without + // worrying about the delicate order of operations. + this.markVariables(startPosition) + this.expandVariables(startPosition) + } + + // Snippet bodies are written generically and don't know anything + // about the user's indentation settings. So we adjust them after + // expansion. + this.editor.normalizeTabsInBufferRange(newRangeMarker.getBufferRange()) + }) + }) + } + + // Set a flag on undo or redo so that we know not to re-apply transforms. + // They're already accounted for in the history. + onUndoOrRedo (isUndo) { + this.isUndoingOrRedoing = true + } + + cursorMoved ({oldBufferPosition, newBufferPosition, textChanged}) { + if (this.settingTabStop || textChanged) { return } + const insertionAtCursor = this.insertionsByIndex[this.tabStopIndex].find(insertion => { + let marker = this.markersForInsertions.get(insertion) + return marker.getBufferRange().containsPoint(newBufferPosition) + }) + + if (insertionAtCursor && !insertionAtCursor.isTransformation()) { return } + + this.destroy() + } + + cursorDestroyed () { + // The only time a cursor can be destroyed without it ending the snippet is + // if we move from a mirrored tab stop (i.e., multiple cursors) to a + // single-cursor tab stop. + if (!this.settingTabStop) { this.destroy() } + } + + textChanged (event) { + if (this.isIgnoringBufferChanges) { return } + + // Don't try to alter the buffer if all we're doing is restoring a snapshot + // from history. + if (this.isUndoingOrRedoing) { + this.isUndoingOrRedoing = false + return + } + + this.applyTransformations(this.tabStopIndex) + } + + ignoringBufferChanges (callback) { + const wasIgnoringBufferChanges = this.isIgnoringBufferChanges + this.isIgnoringBufferChanges = true + callback() + this.isIgnoringBufferChanges = wasIgnoringBufferChanges + } + + applyAllTransformations () { + this.editor.transact(() => { + this.insertionsByIndex.forEach((insertion, index) => + this.applyTransformations(index)) + }) + } + + applyTransformations (tabStopIndex) { + const insertions = [...this.insertionsByIndex[tabStopIndex]] + if (insertions.length === 0) { return } + + const primaryInsertion = insertions.shift() + const primaryRange = this.markersForInsertions.get(primaryInsertion).getBufferRange() + const inputText = this.editor.getTextInBufferRange(primaryRange) + + this.ignoringBufferChanges(() => { + for (const [index, insertion] of insertions.entries()) { + // Don't transform mirrored tab stops. They have their own cursors, so + // mirroring happens automatically. + if (!insertion.isTransformation()) { continue } + + var marker = this.markersForInsertions.get(insertion) + var range = marker.getBufferRange() + + var outputText = insertion.transform(inputText) + this.editor.transact(() => this.editor.setTextInBufferRange(range, outputText)) + + // Manually adjust the marker's range rather than rely on its internal + // heuristics. (We don't have to worry about whether it's been + // invalidated because setting its buffer range implicitly marks it as + // valid again.) + const newRange = new Range( + range.start, + range.start.traverse(new Point(0, outputText.length)) + ) + marker.setBufferRange(newRange) + } + }) + } + + resolveVariables (startPosition) { + let params = { + editor: this.editor, + cursor: this.cursor, + selectionRange: this.cursor.selection.getBufferRange(), + method: this.method + } + + for (const variable of this.snippet.variables) { + let resolution = variable.resolve(params) + this.resolutionsForVariables.set(variable, resolution) + } + } + + markVariables (startPosition) { + // We make two passes here. On the first pass, we create markers for each + // point where a variable will be inserted. On the second pass, we use each + // marker to insert the resolved variable value. + // + // Those points will move around as we insert text into them, so the + // markers are crucial for ensuring we adapt to those changes. + for (const variable of this.snippet.variables) { + const {point} = variable + const marker = this.getMarkerLayer(this.editor).markBufferRange([ + startPosition.traverse(point), + startPosition.traverse(point) + ], {exclusive: false}) + this.markersForVariables.set(variable, marker) + } + } + + expandVariables (startPosition) { + this.editor.transact(() => { + for (const variable of this.snippet.variables) { + let marker = this.markersForVariables.get(variable) + let resolution = this.resolutionsForVariables.get(variable) + let range = marker.getBufferRange() + this.editor.setTextInBufferRange(range, resolution) + } + }) + } + + placeTabStopMarkers (startPosition, tabStops) { + // Tab stops within a snippet refer to one another by their external index + // (1 for $1, 3 for $3, etc.). We respect the order of these tab stops, but + // we renumber them starting at 0 and using consecutive numbers. + // + // Luckily, we don't need to convert between the two numbering systems very + // often. But we do have to build a map from external index to our internal + // index. We do this in a separate loop so that the table is complete + // before we need to consult it in the following loop. + const indexTable = {} + for (let [index, tabStop] of tabStops.entries()) { + indexTable[tabStop.index] = index + } + + for (let [index, tabStop] of tabStops.entries()) { + const {insertions} = tabStop + + if (!tabStop.isValid()) { continue } + + for (const insertion of insertions) { + const {range} = insertion + const {start, end} = range + let references = null + if (insertion.references) { + references = insertion.references.map(external => indexTable[external]) + } + // This is our initial pass at marking tab stop regions. In a minute, + // once the first tab stop is made active, we will make some of these + // markers exclusive and some inclusive. But right now we need them all + // to be inclusive, because we want them all to react when we resolve + // snippet variables, and grow if they need to. + const marker = this.getMarkerLayer(this.editor).markBufferRange([ + startPosition.traverse(start), + startPosition.traverse(end) + ], {exclusive: false}) + // Now that we've created these markers, we need to store them in a + // data structure because they'll need to be deleted and re-created + // when their exclusivity changes. + this.markersForInsertions.set(insertion, marker) + + if (references) { + // The insertion at tab stop `index` (internal numbering) is related + // to, and affected by, all the tab stops mentioned in `references` + // (internal numbering). We need to make sure we're included in these + // other tab stops' exclusivity changes. + for (let ref of references) { + let relatedInsertions = this.relatedInsertionsByIndex.get(ref) || [] + relatedInsertions.push(insertion) + this.relatedInsertionsByIndex.set(ref, relatedInsertions) + } + } + } + this.insertionsByIndex[index] = insertions + } + } + + // When two insertion markers are directly adjacent to one another, and the + // cursor is placed right at the border between them, the marker that should + // "claim" the newly typed content will vary based on context. + // + // All else being equal, that content should get added to the marker (if any) + // whose tab stop is active, or else the marker whose tab stop's placeholder + // references an active tab stop. To use the terminology of Atom's + // `DisplayMarker`, all markers related to the active tab stop should be + // "inclusive," and all others should be "exclusive." + // + // Exclusivity cannot be changed after a marker is created. So we need to + // revisit the markers whenever the active tab stop changes, figure out which + // ones need to be touched, and replace them with markers that have the + // settings we need. + // + adjustTabStopMarkers (oldIndex, newIndex) { + // All the insertions belonging to the newly active tab stop (and all + // insertions whose placeholders reference the newly active tab stop) + // should become inclusive. + const insertionsToMakeInclusive = [ + ...this.insertionsByIndex[newIndex], + ...(this.relatedInsertionsByIndex.get(newIndex) || []) + ] + + // All insertions that are _not_ related to the newly active tab stop + // should become exclusive if they aren't already. + let insertionsToMakeExclusive + if (oldIndex === null) { + // This is the first index to be made active. Since all insertion markers + // were initially created to be inclusive, we need to adjust _all_ + // insertion markers that are not related to the new tab stop. + let allInsertions = this.insertionsByIndex.reduce((set, ins) => { + set.push(...ins) + return set + }, []) + insertionsToMakeExclusive = allInsertions.filter(ins => { + return !insertionsToMakeInclusive.includes(ins) + }) + } else { + // We are moving from one tab stop to another, so we only need to touch + // the markers related to the tab stop we're departing. + insertionsToMakeExclusive = [ + ...this.insertionsByIndex[oldIndex], + ...(this.relatedInsertionsByIndex.get(oldIndex) || []) + ] + } + + for (let insertion of insertionsToMakeExclusive) { + this.replaceMarkerForInsertion(insertion, {exclusive: true}) + } + + for (let insertion of insertionsToMakeInclusive) { + this.replaceMarkerForInsertion(insertion, {exclusive: false}) + } + } + + replaceMarkerForInsertion (insertion, settings) { + const marker = this.markersForInsertions.get(insertion) + + // If the marker is invalid or destroyed, return it as-is. Other methods + // need to know if a marker has been invalidated or destroyed, and we have + // no need to change the settings on such markers anyway. + if (!marker.isValid() || marker.isDestroyed()) { + return marker + } + + // Otherwise, create a new marker with an identical range and the specified + // settings. + const range = marker.getBufferRange() + const replacement = this.getMarkerLayer(this.editor).markBufferRange(range, settings) + + marker.destroy() + this.markersForInsertions.set(insertion, replacement) + return replacement + } + + goToNextTabStop () { + const nextIndex = this.tabStopIndex + 1 + if (nextIndex < this.insertionsByIndex.length) { + if (this.setTabStopIndex(nextIndex)) { + return true + } else { + return this.goToNextTabStop() + } + } else { + // The user has tabbed past the last tab stop. If the last tab stop is a + // $0, we shouldn't move the cursor any further. + if (this.snippet.tabStopList.hasEndStop) { + this.destroy() + return false + } else { + const succeeded = this.goToEndOfLastTabStop() + this.destroy() + return succeeded + } + } + } + + goToPreviousTabStop () { + if (this.tabStopIndex > 0) { this.setTabStopIndex(this.tabStopIndex - 1) } + } + + setTabStopIndex (newIndex) { + const oldIndex = this.tabStopIndex + this.tabStopIndex = newIndex + // Set a flag before moving any selections so that our change handlers know + // that the movements were initiated by us. + this.settingTabStop = true + // Keep track of whether we placed any selections or cursors. + let markerSelected = false + + const insertions = this.insertionsByIndex[this.tabStopIndex] + if (insertions.length === 0) { return false } + + const ranges = [] + this.hasTransforms = false + + // Go through the active tab stop's markers to figure out where to place + // cursors and/or selections. + for (const insertion of insertions) { + const marker = this.markersForInsertions.get(insertion) + if (marker.isDestroyed()) { continue } + if (!marker.isValid()) { continue } + if (insertion.isTransformation()) { + // Set a flag for later, but skip transformation insertions because + // they don't get their own cursors. + this.hasTransforms = true + continue + } + ranges.push(marker.getBufferRange()) + } + + if (ranges.length > 0) { + // We have new selections to apply. Reuse existing selections if + // possible, destroying the unused ones if we already have too many. + for (const selection of this.selections.slice(ranges.length)) { selection.destroy() } + this.selections = this.selections.slice(0, ranges.length) + for (let i = 0; i < ranges.length; i++) { + const range = ranges[i] + if (this.selections[i]) { + this.selections[i].setBufferRange(range) + } else { + const newSelection = this.editor.addSelectionForBufferRange(range) + this.subscriptions.add(newSelection.cursor.onDidChangePosition(event => this.cursorMoved(event))) + this.subscriptions.add(newSelection.cursor.onDidDestroy(() => this.cursorDestroyed())) + this.selections.push(newSelection) + } + } + // We placed at least one selection, so this tab stop was successfully + // set. + markerSelected = true + } + + this.settingTabStop = false + // If this snippet has at least one transform, we need to observe changes + // made to the editor so that we can update the transformed tab stops. + if (this.hasTransforms) { + this.snippets.observeEditor(this.editor) + } else { + this.snippets.stopObservingEditor(this.editor) + } + + this.adjustTabStopMarkers(oldIndex, newIndex) + + return markerSelected + } + + goToEndOfLastTabStop () { + const size = this.insertionsByIndex.length + if (size === 0) { return } + const insertions = this.insertionsByIndex[size - 1] + if (insertions.length === 0) { return } + const lastMarker = this.markersForInsertions.get(insertions[insertions.length - 1]) + + if (lastMarker.isDestroyed()) { + return false + } else { + this.editor.setCursorBufferPosition(lastMarker.getEndBufferPosition()) + return true + } + } + + destroy () { + this.subscriptions.dispose() + this.getMarkerLayer(this.editor).clear() + this.insertionsByIndex = [] + this.relatedInsertionsByIndex.clear() + this.markersForInsertions.clear() + this.resolutionsForVariables.clear() + this.markersForVariables.clear() + + this.snippets.stopObservingEditor(this.editor) + this.snippets.clearExpansions(this.editor) + } + + getMarkerLayer () { + return this.snippets.findOrCreateMarkerLayer(this.editor) + } + + restore (editor) { + this.editor = editor + this.snippets.addExpansion(this.editor, this) + } +} diff --git a/packages/snippets/lib/snippet-history-provider.js b/packages/snippets/lib/snippet-history-provider.js new file mode 100644 index 000000000..b1b3e57cb --- /dev/null +++ b/packages/snippets/lib/snippet-history-provider.js @@ -0,0 +1,27 @@ +function wrap (manager, callbacks) { + let klass = new SnippetHistoryProvider(manager) + return new Proxy(manager, { + get (target, name) { + if (name in callbacks) { + callbacks[name]() + } + return name in klass ? klass[name] : target[name] + } + }) +} + +class SnippetHistoryProvider { + constructor (manager) { + this.manager = manager + } + + undo (...args) { + return this.manager.undo(...args) + } + + redo (...args) { + return this.manager.redo(...args) + } +} + +module.exports = wrap diff --git a/packages/snippets/lib/snippet.js b/packages/snippets/lib/snippet.js new file mode 100644 index 000000000..86af19d88 --- /dev/null +++ b/packages/snippets/lib/snippet.js @@ -0,0 +1,109 @@ +const {Point, Range} = require('atom') +const TabStopList = require('./tab-stop-list') +const Variable = require('./variable') + +function tabStopsReferencedWithinTabStopContent (segment) { + const results = [] + for (const item of segment) { + if (item.index) { + results.push(item.index, ...tabStopsReferencedWithinTabStopContent(item.content)) + } + } + return new Set(results) +} + +module.exports = class Snippet { + constructor (attrs) { + let { + id, + bodyText, + bodyTree, + command, + description, + descriptionMoreURL, + leftLabel, + leftLabelHTML, + name, + prefix, + packageName, + rightLabelHTML, + selector + } = attrs + + this.id = id + this.name = name + this.prefix = prefix + this.command = command + this.packageName = packageName + this.bodyText = bodyText + this.description = description + this.descriptionMoreURL = descriptionMoreURL + this.rightLabelHTML = rightLabelHTML + this.leftLabel = leftLabel + this.leftLabelHTML = leftLabelHTML + this.selector = selector + + this.variables = [] + this.tabStopList = new TabStopList(this) + this.body = this.extractTokens(bodyTree) + + if (packageName && command) { + this.commandName = `${packageName}:${command}` + } + } + + extractTokens (bodyTree) { + const bodyText = [] + let row = 0, column = 0 + + let extract = bodyTree => { + for (let segment of bodyTree) { + if (segment.index != null) { + // Tabstop. + let {index, content, substitution} = segment + // Ensure tabstop `$0` is always last. + if (index === 0) { index = Infinity } + + const start = [row, column] + extract(content) + + const referencedTabStops = tabStopsReferencedWithinTabStopContent(content) + + const range = new Range(start, [row, column]) + + const tabStop = this.tabStopList.findOrCreate({ + index, snippet: this + }) + + tabStop.addInsertion({ + range, + substitution, + references: [...referencedTabStops] + }) + } else if (segment.variable != null) { + // Variable. + let point = new Point(row, column) + this.variables.push( + new Variable({...segment, point, snippet: this}) + ) + } else if (typeof segment === 'string') { + bodyText.push(segment) + let segmentLines = segment.split('\n') + column += segmentLines.shift().length + let nextLine + while ((nextLine = segmentLines.shift()) != null) { + row += 1 + column = nextLine.length + } + } + } + } + + extract(bodyTree) + this.lineCount = row + 1 + this.insertions = this.tabStopList.getInsertions() + + return bodyText.join('') + } + +} diff --git a/packages/snippets/lib/snippets-available.js b/packages/snippets/lib/snippets-available.js new file mode 100644 index 000000000..d244cb16d --- /dev/null +++ b/packages/snippets/lib/snippets-available.js @@ -0,0 +1,84 @@ +/** @babel */ + +import _ from 'underscore-plus' +import SelectListView from 'atom-select-list' + +export default class SnippetsAvailable { + constructor (snippets) { + this.panel = null + this.snippets = snippets + this.selectListView = new SelectListView({ + items: [], + filterKeyForItem: (snippet) => snippet.searchText, + elementForItem: (snippet) => { + const li = document.createElement('li') + li.classList.add('two-lines') + + const primaryLine = document.createElement('div') + primaryLine.classList.add('primary-line') + primaryLine.textContent = snippet.prefix + li.appendChild(primaryLine) + + const secondaryLine = document.createElement('div') + secondaryLine.classList.add('secondary-line') + secondaryLine.textContent = snippet.name + li.appendChild(secondaryLine) + + return li + }, + didConfirmSelection: (snippet) => { + for (const cursor of this.editor.getCursors()) { + this.snippets.insert(snippet.bodyText, this.editor, cursor) + } + this.cancel() + }, + didConfirmEmptySelection: () => { + this.cancel() + }, + didCancelSelection: () => { + this.cancel() + } + }) + this.selectListView.element.classList.add('available-snippets') + this.element = this.selectListView.element + } + + async toggle (editor) { + this.editor = editor + if (this.panel != null) { + this.cancel() + } else { + this.selectListView.reset() + await this.populate() + this.attach() + } + } + + cancel () { + this.editor = null + + if (this.panel != null) { + this.panel.destroy() + this.panel = null + } + + if (this.previouslyFocusedElement) { + this.previouslyFocusedElement.focus() + this.previouslyFocusedElement = null + } + } + + populate () { + const snippets = Object.values(this.snippets.getSnippets(this.editor)) + for (let snippet of snippets) { + snippet.searchText = _.compact([snippet.prefix, snippet.name]).join(' ') + } + return this.selectListView.update({items: snippets}) + } + + attach () { + this.previouslyFocusedElement = document.activeElement + this.panel = atom.workspace.addModalPanel({item: this}) + this.selectListView.focus() + } +} diff --git a/packages/snippets/lib/snippets.cson b/packages/snippets/lib/snippets.cson new file mode 100644 index 000000000..585a896aa --- /dev/null +++ b/packages/snippets/lib/snippets.cson @@ -0,0 +1,57 @@ +'.source.json': + 'Atom Snippet': + prefix: 'snip' + body: """ + { + "${1:.source.js}": { + "${2:Snippet Name}": { + "prefix": "${3:Snippet Trigger}", + "body": "${4:Hello World!}" + } + } + }$5 + """ + + 'Atom Snippet With No Selector': + prefix: 'snipns' + body: """ + "${1:Snippet Name}": { + "prefix": "${2:Snippet Trigger}", + "body": "${3:Hello World!}" + }$4 + """ + + 'Atom Keymap': + prefix: 'key' + body: """ + { + "${1:body}": { + "${2:cmd}-${3:i}": "${4:namespace}:${5:event}" + } + }$6 + """ + +'.source.coffee': + 'Atom Snippet': + prefix: 'snip' + body: """ + '${1:.source.js}': + '${2:Snippet Name}': + 'prefix': '${3:Snippet Trigger}' + 'body': '${4:Hello World!}'$5 + """ + + 'Atom Snippet With No Selector': + prefix: 'snipns' + body: """ + '${1:Snippet Name}': + 'prefix': '${2:Snippet Trigger}' + 'body': '${3:Hello World!}'$4 + """ + + 'Atom Keymap': + prefix: 'key' + body: """ + '${1:body}': + '${2:cmd}-${3:i}': '${4:namespace}:${5:event}'$6 + """ diff --git a/packages/snippets/lib/snippets.js b/packages/snippets/lib/snippets.js new file mode 100644 index 000000000..ab361bcac --- /dev/null +++ b/packages/snippets/lib/snippets.js @@ -0,0 +1,936 @@ +const path = require('path') +const {Emitter, Disposable, CompositeDisposable, File} = require('atom') +const _ = require('underscore-plus') +const async = require('async') +const CSON = require('season') +const fs = require('fs') +const ScopedPropertyStore = require('scoped-property-store') + +const Snippet = require('./snippet') +const SnippetExpansion = require('./snippet-expansion') +const EditorStore = require('./editor-store') +const {getPackageRoot} = require('./helpers') + +// TODO: Not sure about validity of numbers in here, but might as well be +// permissive. +const COMMAND_NAME_PATTERN = /^[a-z\d][a-z\d\-]*[a-z\d]$/ +function isValidCommandName (commandName) { + return COMMAND_NAME_PATTERN.test(commandName) +} + +function showCommandNameConflictNotification (name, commandName, packageName, snippetsPath) { + let remedy + if (packageName === 'builtin') { + // If somehow this happens with a builtin snippet, something crazy is + // happening. But we shouldn't show a notification because there's no + // action for the user to take. Just fail silently. + return + } + if (packageName === 'snippets') { + let extension = snippetsPath.substring(snippetsPath.length - 4) + remedy = `Edit your \`snippets.${extension}\` file to resolve this conflict.` + } else { + remedy = `Contact the maintainer of \`${packageName}\` so they can resolve this conflict.` + } + const message = `Cannot register command \`${commandName}\` for snippet “${name}” because that command name already exists.\n\n${remedy}` + atom.notifications.addError( + `Snippets conflict`, + { + description: message, + dismissable: true + } + ) +} + +function showInvalidCommandNameNotification (name, commandName) { + const message = `Cannot register \`${commandName}\` for snippet “${name}” because the command name isn’t valid. Command names must be all lowercase and use hyphens between words instead of spaces.` + atom.notifications.addError( + `Snippets error`, + { + description: message, + dismissable: true + } + ) +} + +// When we first run, checking `atom.commands.registeredCommands` is a good way +// of checking whether a command of a certain name already exists. But if we +// register a command and then unregister it (e.g., upon later disabling of a +// package's snippets), the relevant key won't get deleted from +// `registeredCommands`. So if the user re-enables the snippets, we'll +// incorrectly think that the command already exists. +// +// Hence, after the first check, we have to keep track ourselves. At least this +// gives us a place to keep track of individual command disposables. +// +const CommandMonitor = { + map: new Map, + disposables: new Map, + compositeDisposable: new CompositeDisposable, + exists (commandName) { + let {map} = this + if (!map.has(commandName)) { + // If it's missing altogether from the registry, we haven't asked yet. + let value = atom.commands.registeredCommands[commandName] + map.set(commandName, value) + return value + } else { + return map.get(commandName) + } + }, + + add (commandName, disposable) { + this.map.set(commandName, true) + this.disposables.set(commandName, disposable) + this.compositeDisposable.add(disposable) + }, + + remove (commandName) { + this.map.set(commandName, false) + let disposable = this.disposables.get(commandName) + if (disposable) { disposable.dispose() } + }, + + reset () { + this.map.clear() + this.disposables.clear() + this.compositeDisposable.dispose() + } +} + +// When we load snippets from packages, we're given a bunch of package paths +// instead of package names. This lets us match the former to the latter. +const PackageNameResolver = { + pathsToNames: new Map, + setup () { + this.pathsToNames.clear() + let meta = atom.packages.getLoadedPackages() || [] + for (let {name, path} of meta) { + this.pathsToNames.set(path, name) + } + if (!this._observing) { + atom.packages.onDidLoadPackage(() => this.setup()) + atom.packages.onDidUnloadPackage(() => this.setup()) + } + this._observing = true + }, + find (filePath) { + for (let [packagePath, name] of this.pathsToNames.entries()) { + if (filePath.startsWith(`${packagePath}${path.sep}`)) return name + } + return null + } +} + +module.exports = { + activate () { + this.loaded = false + this.userSnippetsPath = null + this.snippetIdCounter = 0 + this.snippetsByPackage = new Map + this.parsedSnippetsById = new Map + this.editorMarkerLayers = new WeakMap + + this.scopedPropertyStore = new ScopedPropertyStore + // The above ScopedPropertyStore will store the main registry of snippets. + // But we need a separate ScopedPropertyStore for the snippets that come + // from disabled packages. They're isolated so that they're not considered + // as candidates when the user expands a prefix, but we still need the data + // around so that the snippets provided by those packages can be shown in + // the settings view. + this.disabledSnippetsScopedPropertyStore = new ScopedPropertyStore + + this.subscriptions = new CompositeDisposable + this.subscriptions.add(atom.workspace.addOpener(uri => { + if (uri === 'atom://.pulsar/snippets') { + return atom.workspace.openTextFile(this.getUserSnippetsPath()) + } + })) + + PackageNameResolver.setup() + + this.loadAll() + this.watchUserSnippets(watchDisposable => { + this.subscriptions.add(watchDisposable) + }) + + this.subscriptions.add( + atom.config.onDidChange( + 'core.packagesWithSnippetsDisabled', + ({newValue, oldValue}) => { + this.handleDisabledPackagesDidChange(newValue, oldValue) + } + ) + ) + + const snippets = this + + this.subscriptions.add(atom.commands.add('atom-text-editor', { + 'snippets:expand' (event) { + const editor = this.getModel() + if (snippets.snippetToExpandUnderCursor(editor)) { + snippets.clearExpansions(editor) + snippets.expandSnippetsUnderCursors(editor) + } else { + event.abortKeyBinding() + } + }, + + 'snippets:next-tab-stop' (event) { + const editor = this.getModel() + if (!snippets.goToNextTabStop(editor)) { event.abortKeyBinding() } + }, + + 'snippets:previous-tab-stop' (event) { + const editor = this.getModel() + if (!snippets.goToPreviousTabStop(editor)) { event.abortKeyBinding() } + }, + + 'snippets:available' (event) { + const editor = this.getModel() + const SnippetsAvailable = require('./snippets-available') + if (snippets.availableSnippetsView == null) { + snippets.availableSnippetsView = new SnippetsAvailable(snippets) + } + snippets.availableSnippetsView.toggle(editor) + } + })) + }, + + deactivate () { + if (this.emitter != null) { + this.emitter.dispose() + } + this.emitter = null + this.editorSnippetExpansions = null + atom.config.transact(() => this.subscriptions.dispose()) + CommandMonitor.reset() + }, + + getUserSnippetsPath () { + if (this.userSnippetsPath != null) { return this.userSnippetsPath } + + this.userSnippetsPath = CSON.resolve(path.join(atom.getConfigDirPath(), 'snippets')) + if (this.userSnippetsPath == null) { this.userSnippetsPath = path.join(atom.getConfigDirPath(), 'snippets.cson') } + return this.userSnippetsPath + }, + + loadAll () { + this.loadBundledSnippets(bundledSnippets => { + this.loadPackageSnippets(packageSnippets => { + this.loadUserSnippets(userSnippets => { + atom.config.transact(() => { + for (const [filepath, snippetsBySelector] of Object.entries(bundledSnippets)) { + this.add(filepath, snippetsBySelector, 'builtin') + } + for (const [filepath, snippetsBySelector] of Object.entries(packageSnippets)) { + let packageName = PackageNameResolver.find(filepath) || 'snippets' + this.add(filepath, snippetsBySelector, packageName) + } + for (const [filepath, snippetsBySelector] of Object.entries(userSnippets)) { + this.add(filepath, snippetsBySelector, 'snippets') + } + }) + this.doneLoading() + }) + }) + }) + }, + + loadBundledSnippets (callback) { + const bundledSnippetsPath = CSON.resolve(path.join(getPackageRoot(), 'lib', 'snippets')) + this.loadSnippetsFile(bundledSnippetsPath, snippets => { + const snippetsByPath = {} + snippetsByPath[bundledSnippetsPath] = snippets + callback(snippetsByPath) + }) + }, + + loadUserSnippets (callback) { + const userSnippetsPath = this.getUserSnippetsPath() + fs.stat(userSnippetsPath, (error, stat) => { + if (stat != null && stat.isFile()) { + this.loadSnippetsFile(userSnippetsPath, snippets => { + const result = {} + result[userSnippetsPath] = snippets + callback(result) + }) + } else { + callback({}) + } + }) + }, + + watchUserSnippets (callback) { + const userSnippetsPath = this.getUserSnippetsPath() + fs.stat(userSnippetsPath, (error, stat) => { + if (stat != null && stat.isFile()) { + const userSnippetsFileDisposable = new CompositeDisposable() + const userSnippetsFile = new File(userSnippetsPath) + try { + userSnippetsFileDisposable.add(userSnippetsFile.onDidChange(() => this.handleUserSnippetsDidChange())) + userSnippetsFileDisposable.add(userSnippetsFile.onDidDelete(() => this.handleUserSnippetsDidChange())) + userSnippetsFileDisposable.add(userSnippetsFile.onDidRename(() => this.handleUserSnippetsDidChange())) + } catch (e) { + const message = `\ + Unable to watch path: \`snippets.cson\`. Make sure you have permissions + to the \`~/.pulsar\` directory and \`${userSnippetsPath}\`. + + On linux there are currently problems with watch sizes. See + [this document][watches] for more info. + [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\ + ` + atom.notifications.addError(message, {dismissable: true}) + } + + callback(userSnippetsFileDisposable) + } else { + callback(new Disposable()) + } + }) + }, + + // Called when a user's snippets file is changed, deleted, or moved so that we + // can immediately re-process the snippets it contains. + handleUserSnippetsDidChange () { + // TODO: There appear to be scenarios where this method gets invoked more + // than once with each change to the user's `snippets.cson`. To prevent + // more than one concurrent rescan of the snippets file, we block any + // additional calls to this method while the first call is still operating. + const userSnippetsPath = this.getUserSnippetsPath() + + if (this.isHandlingUserSnippetsChange) { + return + } + + this.isHandlingUserSnippetsChange = true + atom.config.transact(() => { + this.clearSnippetsForPath(userSnippetsPath) + this.loadSnippetsFile(userSnippetsPath, result => { + this.add(userSnippetsPath, result, 'snippets') + this.isHandlingUserSnippetsChange = false + }) + }) + }, + + // Called when the "Enable" checkbox is checked/unchecked in the Snippets + // section of a package's settings view. + handleDisabledPackagesDidChange (newDisabledPackages = [], oldDisabledPackages = []) { + const packagesToAdd = [] + const packagesToRemove = [] + for (const p of oldDisabledPackages) { + if (!newDisabledPackages.includes(p)) { packagesToAdd.push(p) } + } + + for (const p of newDisabledPackages) { + if (!oldDisabledPackages.includes(p)) { packagesToRemove.push(p) } + } + + atom.config.transact(() => { + for (const p of packagesToRemove) { this.removeSnippetsForPackage(p) } + for (const p of packagesToAdd) { this.addSnippetsForPackage(p) } + }) + }, + + addSnippetsForPackage (packageName) { + const snippetSet = this.snippetsByPackage.get(packageName) + for (const filePath in snippetSet) { + const snippetsBySelector = snippetSet[filePath] + this.add(filePath, snippetsBySelector, packageName) + } + }, + + removeSnippetsForPackage (packageName) { + const snippetSet = this.snippetsByPackage.get(packageName) + // Copy these snippets to the "quarantined" ScopedPropertyStore so that they + // remain present in the list of unparsed snippets reported to the settings + // view. + this.addSnippetsInDisabledPackage(snippetSet) + for (const filePath in snippetSet) { + this.clearSnippetsForPath(filePath) + } + }, + + loadPackageSnippets (callback) { + const disabledPackageNames = atom.config.get('core.packagesWithSnippetsDisabled') || [] + const packages = atom.packages.getLoadedPackages().sort((pack, _) => { + return pack.path.includes(`${path.sep}node_modules${path.sep}`) ? -1 : 1 + }) + + const snippetsDirPaths = [] + for (const pack of packages) { + snippetsDirPaths.push(path.join(pack.path, 'snippets')) + } + + async.map(snippetsDirPaths, this.loadSnippetsDirectory.bind(this), (error, results) => { + const zipped = [] + for (const key in results) { + zipped.push({result: results[key], pack: packages[key]}) + } + + const enabledPackages = [] + for (const o of zipped) { + // Skip packages that contain no snippets. + if (Object.keys(o.result).length === 0) { continue } + // Keep track of which snippets come from which packages so we can + // unload them selectively later. All packages get put into this map, + // even disabled packages, because we need to know which snippets to add + // if those packages are enabled again. + this.snippetsByPackage.set(o.pack.name, o.result) + if (disabledPackageNames.includes(o.pack.name)) { + // Since disabled packages' snippets won't get added to the main + // ScopedPropertyStore, we'll keep track of them in a separate + // ScopedPropertyStore so that they can still be represented in the + // settings view. + this.addSnippetsInDisabledPackage(o.result) + } else { + enabledPackages.push(o.result) + } + } + + callback(_.extend({}, ...enabledPackages)) + }) + }, + + doneLoading () { + this.loaded = true + this.getEmitter().emit('did-load-snippets') + }, + + onDidLoadSnippets (callback) { + this.getEmitter().on('did-load-snippets', callback) + }, + + getEmitter () { + if (this.emitter == null) { + this.emitter = new Emitter + } + return this.emitter + }, + + loadSnippetsDirectory (snippetsDirPath, callback) { + fs.stat(snippetsDirPath, (error, stat) => { + if (error || !stat.isDirectory()) return callback(null, {}) + + fs.readdir(snippetsDirPath, (error, entries) => { + if (error) { + console.warn(`Error reading snippets directory ${snippetsDirPath}`, error) + return callback(null, {}) + } + + async.map( + entries, + (entry, done) => { + const filePath = path.join(snippetsDirPath, entry) + this.loadSnippetsFile(filePath, snippets => done(null, {filePath, snippets})) + }, + (error, results) => { + const snippetsByPath = {} + for (const {filePath, snippets} of results) { + snippetsByPath[filePath] = snippets + } + callback(null, snippetsByPath) + } + ) + }) + }) + }, + + loadSnippetsFile (filePath, callback) { + if (!CSON.isObjectPath(filePath)) { return callback({}) } + CSON.readFile(filePath, {allowDuplicateKeys: false}, (error, object = {}) => { + if (error != null) { + console.warn(`Error reading snippets file '${filePath}': ${error.stack != null ? error.stack : error}`) + atom.notifications.addError(`Failed to load snippets from '${filePath}'`, {detail: error.message, dismissable: true}) + } + callback(object) + }) + }, + + add (filePath, snippetsBySelector, packageName = null, isDisabled = false) { + packageName ??= 'snippets' + for (const selector in snippetsBySelector) { + const snippetsByName = snippetsBySelector[selector] + const unparsedSnippetsByPrefix = {} + for (const name in snippetsByName) { + const attributes = snippetsByName[name] + const {prefix, command, body} = attributes + if (!prefix && !command) { + // A snippet must define either `prefix` or `command`, or both. + // TODO: Worth showing notification? + console.error(`Skipping snippet ${name}: no "prefix" or "command" property present`) + continue + } + attributes.selector = selector + attributes.name = name + attributes.id = this.snippetIdCounter++ + attributes.packageName = packageName + // Snippets with "prefix"es will get indexed according to that prefix. + // Snippets without "prefix"es will be indexed by their ID below _if_ + // they have a "command" property. Snippets without "prefix" or + // "command" have already been filtered out. + if (prefix) { + if (typeof body === 'string') { + unparsedSnippetsByPrefix[prefix] = attributes + } else if (body == null) { + unparsedSnippetsByPrefix[prefix] = null + } + } + if (command) { + if (!isValidCommandName(command)) { + showInvalidCommandNameNotification(name, command) + continue + } + if (!prefix) { + // We need a key for these snippets that will not clash with any + // prefix key. Since prefixes aren't allowed to have spaces, we'll + // put a space in this key. + // + // We'll use the snippet ID as part of the key. If a snippet's + // `command` property clashes with another command, we'll catch + // that later. + let unparsedSnippetsKey = `command ${attributes.id}` + if (typeof body === 'string') { + unparsedSnippetsByPrefix[unparsedSnippetsKey] = attributes + } else { + unparsedSnippetsByPrefix[unparsedSnippetsKey] = null + } + } + if (!isDisabled) { + this.addCommandForSnippet(attributes, packageName, selector) + } + } + } + + this.storeUnparsedSnippets(unparsedSnippetsByPrefix, filePath, selector, packageName, isDisabled) + } + }, + + addCommandForSnippet (attributes, packageName, selector) { + packageName = packageName || 'snippets' + let {name, command} = attributes + let commandName = `${packageName}:${command}` + if (CommandMonitor.exists(commandName)) { + console.error(`Skipping ${commandName} because it's already been registered!`) + showCommandNameConflictNotification( + name, + commandName, + packageName, + this.getUserSnippetsPath() + ) + // We won't remove the snippet because it might still be triggerable by + // prefix. But we will null out the `command` property to prevent any + // possible confusion. + attributes.command = null + return + } + + let commandHandler = (event) => { + let editor = event.target.closest('atom-text-editor').getModel() + + // We match the multi-cursor behavior that prefix-triggered snippets + // exhibit: only the last cursor determines which scoped set of snippets + // we pull, but we'll insert this snippet for each cursor, whether it + // happens to be valid for that cursor's scope or not. This could + // possibly be refined in the future. + let snippets = this.getSnippets(editor) + + let targetSnippet = null + for (let snippet of Object.values(snippets)) { + if (snippet.id === attributes.id) { + targetSnippet = snippet + break + } + } + + if (!targetSnippet) { + // We don't show an error notification here because it isn't + // necessarily a mistake. But we put a warning in the console just in + // case the user is confused. + console.warn(`Snippet “${name}” not invoked because its scope was not matched.`) + + // Because its scope was not matched, we abort the key binding; this + // signals to the key binding resolver that it can pick the next + // candidate for a key shortcut, if one exists. + return event.abortKeyBinding() + } + + this.expandSnippet(editor, targetSnippet) + } + + let disposable = atom.commands.add( + 'atom-text-editor', + commandName, + commandHandler + ) + + this.subscriptions.add(disposable) + CommandMonitor.add(commandName, disposable) + }, + + addSnippetsInDisabledPackage (bundle) { + for (const filePath in bundle) { + const snippetsBySelector = bundle[filePath] + const packageName = PackageNameResolver.find(filePath) + this.add(filePath, snippetsBySelector, packageName, true) + } + }, + + getScopeChain (object) { + let scopesArray = object + if (object && object.getScopesArray) { + scopesArray = object.getScopesArray() + } + + return scopesArray + .map(scope => scope[0] === '.' ? scope : `.${scope}`) + .join(' ') + }, + + storeUnparsedSnippets (value, path, selector, packageName, isDisabled = false) { + // The `isDisabled` flag determines which scoped property store we'll use. + // Active snippets get put into one and inactive snippets get put into + // another. Only the first one gets consulted when we look up a snippet + // prefix for expansion, but both stores have their contents exported when + // the settings view asks for all available snippets. + const unparsedSnippets = {} + unparsedSnippets[selector] = {"snippets": value} + const store = isDisabled ? this.disabledSnippetsScopedPropertyStore : this.scopedPropertyStore + store.addProperties(path, unparsedSnippets, {priority: this.priorityForSource(path)}) + }, + + clearSnippetsForPath (path) { + for (const scopeSelector in this.scopedPropertyStore.propertiesForSource(path)) { + let object = this.scopedPropertyStore.propertiesForSourceAndSelector(path, scopeSelector) + if (object.snippets) { object = object.snippets } + for (const prefix in object) { + const attributes = object[prefix] + if (!attributes) { continue } + let {command, packageName} = attributes + if (packageName && command) { + CommandMonitor.remove(`${packageName}:${command}`) + } + this.parsedSnippetsById.delete(attributes.id) + } + + this.scopedPropertyStore.removePropertiesForSourceAndSelector(path, scopeSelector) + } + }, + + parsedSnippetsForScopes (scopeDescriptor) { + let unparsedLegacySnippetsByPrefix + + const unparsedSnippetsByPrefix = this.scopedPropertyStore.getPropertyValue( + this.getScopeChain(scopeDescriptor), + "snippets" + ) + + const legacyScopeDescriptor = atom.config.getLegacyScopeDescriptorForNewScopeDescriptor + ? atom.config.getLegacyScopeDescriptorForNewScopeDescriptor(scopeDescriptor) + : undefined + + if (legacyScopeDescriptor) { + unparsedLegacySnippetsByPrefix = this.scopedPropertyStore.getPropertyValue( + this.getScopeChain(legacyScopeDescriptor), + "snippets" + ) + } + + const snippets = {} + + if (unparsedSnippetsByPrefix) { + for (const prefix in unparsedSnippetsByPrefix) { + const attributes = unparsedSnippetsByPrefix[prefix] + if (typeof (attributes != null ? attributes.body : undefined) !== 'string') { continue } + snippets[prefix] = this.getParsedSnippet(attributes) + } + } + + if (unparsedLegacySnippetsByPrefix) { + for (const prefix in unparsedLegacySnippetsByPrefix) { + const attributes = unparsedLegacySnippetsByPrefix[prefix] + if (snippets[prefix]) { continue } + if (typeof (attributes != null ? attributes.body : undefined) !== 'string') { continue } + snippets[prefix] = this.getParsedSnippet(attributes) + } + } + + return snippets + }, + + getParsedSnippet (attributes) { + let snippet = this.parsedSnippetsById.get(attributes.id) + if (snippet == null) { + let {id, prefix, command, name, body, bodyTree, description, packageName, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, selector} = attributes + if (bodyTree == null) { bodyTree = this.getBodyParser().parse(body) } + snippet = new Snippet({id, name, prefix, command, bodyTree, description, packageName, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, selector, bodyText: body}) + this.parsedSnippetsById.set(attributes.id, snippet) + } + return snippet + }, + + priorityForSource (source) { + if (source === this.getUserSnippetsPath()) { + return 1000 + } else { + return 0 + } + }, + + getBodyParser () { + if (this.bodyParser == null) { + this.bodyParser = require('./snippet-body-parser') + } + return this.bodyParser + }, + + // Get an {Object} with these keys: + // * `snippetPrefix`: the possible snippet prefix text preceding the cursor + // * `wordPrefix`: the word preceding the cursor + // + // Returns `null` if the values aren't the same for all cursors + getPrefixText (snippets, editor) { + const wordRegex = this.wordRegexForSnippets(snippets) + + let snippetPrefix = null + let wordPrefix = null + + for (const cursor of editor.getCursors()) { + const position = cursor.getBufferPosition() + + const prefixStart = cursor.getBeginningOfCurrentWordBufferPosition({wordRegex}) + const cursorSnippetPrefix = editor.getTextInRange([prefixStart, position]) + if ((snippetPrefix != null) && (cursorSnippetPrefix !== snippetPrefix)) { return null } + snippetPrefix = cursorSnippetPrefix + + const wordStart = cursor.getBeginningOfCurrentWordBufferPosition() + const cursorWordPrefix = editor.getTextInRange([wordStart, position]) + if ((wordPrefix != null) && (cursorWordPrefix !== wordPrefix)) { return null } + wordPrefix = cursorWordPrefix + } + + return {snippetPrefix, wordPrefix} + }, + + // Get a RegExp of all the characters used in the snippet prefixes + wordRegexForSnippets (snippets) { + const prefixes = {} + + for (const prefix in snippets) { + for (const character of prefix) { prefixes[character] = true } + } + + const prefixCharacters = Object.keys(prefixes).join('') + return new RegExp(`[${_.escapeRegExp(prefixCharacters)}]+`) + }, + + // Get the best match snippet for the given prefix text. This will return + // the longest match where there is no exact match to the prefix text. + snippetForPrefix (snippets, prefix, wordPrefix) { + let longestPrefixMatch = null + + for (const snippetPrefix in snippets) { + // Any snippet without a prefix was keyed on its snippet ID, but with a + // space introduced to ensure it would never be a prefix match. But let's + // play it safe here anyway. + if (snippetPrefix.includes(' ')) { continue } + const snippet = snippets[snippetPrefix] + if (prefix.endsWith(snippetPrefix) && (wordPrefix.length <= snippetPrefix.length)) { + if ((longestPrefixMatch == null) || (snippetPrefix.length > longestPrefixMatch.prefix.length)) { + longestPrefixMatch = snippet + } + } + } + + return longestPrefixMatch + }, + + getSnippets (editor) { + return this.parsedSnippetsForScopes(editor.getLastCursor().getScopeDescriptor()) + }, + + snippetToExpandUnderCursor (editor) { + if (!editor.getLastSelection().isEmpty()) { return false } + const snippets = this.getSnippets(editor) + if (_.isEmpty(snippets)) { return false } + + const prefixData = this.getPrefixText(snippets, editor) + if (prefixData) { + return this.snippetForPrefix(snippets, prefixData.snippetPrefix, prefixData.wordPrefix) + } + }, + + // Expands a snippet invoked via command. + expandSnippet (editor, snippet) { + this.getStore(editor).observeHistory({ + undo: event => { this.onUndoOrRedo(editor, event, true) }, + redo: event => { this.onUndoOrRedo(editor, event, false) } + }) + + this.findOrCreateMarkerLayer(editor) + + editor.transact(() => { + const cursors = editor.getCursors() + for (const cursor of cursors) { + this.insert(snippet, editor, cursor, {method: 'command'}) + } + }) + }, + + // Expands a snippet defined via tab trigger _if_ such a snippet can be found + // for the current prefix and scope. + expandSnippetsUnderCursors (editor) { + const snippet = this.snippetToExpandUnderCursor(editor) + if (!snippet) { return false } + + this.getStore(editor).observeHistory({ + undo: event => { this.onUndoOrRedo(editor, event, true) }, + redo: event => { this.onUndoOrRedo(editor, event, false) } + }) + + this.findOrCreateMarkerLayer(editor) + editor.transact(() => { + const cursors = editor.getCursors() + for (const cursor of cursors) { + // Select the prefix text so that it gets consumed when the snippet + // expands. + const cursorPosition = cursor.getBufferPosition() + const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0]) + cursor.selection.setBufferRange([startPoint, cursorPosition]) + this.insert(snippet, editor, cursor, {method: 'prefix'}) + } + }) + return true + }, + + goToNextTabStop (editor) { + let nextTabStopVisited = false + for (const expansion of this.getExpansions(editor)) { + if (expansion && expansion.goToNextTabStop()) { + nextTabStopVisited = true + } + } + return nextTabStopVisited + }, + + goToPreviousTabStop (editor) { + let previousTabStopVisited = false + for (const expansion of this.getExpansions(editor)) { + if (expansion && expansion.goToPreviousTabStop()) { + previousTabStopVisited = true + } + } + return previousTabStopVisited + }, + + getStore (editor) { + return EditorStore.findOrCreate(editor) + }, + + findOrCreateMarkerLayer (editor) { + let layer = this.editorMarkerLayers.get(editor) + if (layer === undefined) { + layer = editor.addMarkerLayer({maintainHistory: true}) + this.editorMarkerLayers.set(editor, layer) + } + return layer + }, + + getExpansions (editor) { + return this.getStore(editor).getExpansions() + }, + + clearExpansions (editor) { + const store = this.getStore(editor) + store.clearExpansions() + // There are no more active instances of this expansion, so we should undo + // the spying we set up on this editor. + store.stopObserving() + store.stopObservingHistory() + }, + + addExpansion (editor, snippetExpansion) { + this.getStore(editor).addExpansion(snippetExpansion) + }, + + textChanged (editor, event) { + const store = this.getStore(editor) + const activeExpansions = store.getExpansions() + + if ((activeExpansions.length === 0) || activeExpansions[0].isIgnoringBufferChanges) { return } + + this.ignoringTextChangesForEditor(editor, () => + editor.transact(() => + activeExpansions.map(expansion => expansion.textChanged(event))) + ) + + // Create a checkpoint here to consolidate all the changes we just made into + // the transaction that prompted them. + this.makeCheckpoint(editor) + }, + + // Perform an action inside the editor without triggering our `textChanged` + // callback. + ignoringTextChangesForEditor (editor, callback) { + this.stopObservingEditor(editor) + callback() + this.observeEditor(editor) + }, + + observeEditor (editor) { + this.getStore(editor).observe(event => this.textChanged(editor, event)) + }, + + stopObservingEditor (editor) { + this.getStore(editor).stopObserving() + }, + + makeCheckpoint (editor) { + this.getStore(editor).makeCheckpoint() + }, + + insert (snippet, editor, cursor, {method = null} = {}) { + if (editor == null) { editor = atom.workspace.getActiveTextEditor() } + if (cursor == null) { cursor = editor.getLastCursor() } + if (typeof snippet === 'string') { + const bodyTree = this.getBodyParser().parse(snippet) + snippet = new Snippet({id: this.snippetIdCounter++, name: '__anonymous', prefix: '', bodyTree, bodyText: snippet}) + } + return new SnippetExpansion(snippet, editor, cursor, this, {method}) + }, + + getUnparsedSnippets () { + const results = [] + const iterate = sets => { + for (const item of sets) { + const newItem = _.deepClone(item) + // The atom-slick library has already parsed the `selector` property, + // so it's an AST here instead of a string. The object has a `toString` + // method that turns it back into a string. That custom behavior won't + // be preserved in the deep clone of the object, so we have to handle + // it separately. + newItem.selectorString = item.selector.toString() + results.push(newItem) + } + } + + iterate(this.scopedPropertyStore.propertySets) + iterate(this.disabledSnippetsScopedPropertyStore.propertySets) + return results + }, + + provideSnippets () { + return { + bundledSnippetsLoaded: () => this.loaded, + insertSnippet: this.insert.bind(this), + snippetsForScopes: this.parsedSnippetsForScopes.bind(this), + getUnparsedSnippets: this.getUnparsedSnippets.bind(this), + getUserSnippetsPath: this.getUserSnippetsPath.bind(this) + } + }, + + onUndoOrRedo (editor, event, isUndo) { + const activeExpansions = this.getExpansions(editor) + activeExpansions.forEach(expansion => expansion.onUndoOrRedo(isUndo)) + } +} diff --git a/packages/snippets/lib/tab-stop-list.js b/packages/snippets/lib/tab-stop-list.js new file mode 100644 index 000000000..0d3bd0101 --- /dev/null +++ b/packages/snippets/lib/tab-stop-list.js @@ -0,0 +1,48 @@ +const TabStop = require('./tab-stop') + +class TabStopList { + constructor (snippet) { + this.snippet = snippet + this.list = {} + } + + get length () { + return Object.keys(this.list).length + } + + get hasEndStop () { + return !!this.list[Infinity] + } + + findOrCreate ({ index, snippet }) { + if (!this.list[index]) { + this.list[index] = new TabStop({ index, snippet }) + } + return this.list[index] + } + + forEachIndex (iterator) { + let indices = Object.keys(this.list).sort((a1, a2) => a1 - a2) + indices.forEach(iterator) + } + + getInsertions () { + let results = [] + this.forEachIndex(index => { + results.push(...this.list[index].insertions) + }) + return results + } + + toArray () { + let results = [] + this.forEachIndex(index => { + let tabStop = this.list[index] + if (!tabStop.isValid()) return + results.push(tabStop) + }) + return results + } +} + +module.exports = TabStopList diff --git a/packages/snippets/lib/tab-stop.js b/packages/snippets/lib/tab-stop.js new file mode 100644 index 000000000..322f1ccf7 --- /dev/null +++ b/packages/snippets/lib/tab-stop.js @@ -0,0 +1,61 @@ +const {Range} = require('atom') +const Insertion = require('./insertion') + +// A tab stop: +// * belongs to a snippet +// * has an index (one tab stop per index) +// * has multiple Insertions +class TabStop { + constructor ({ snippet, index, insertions }) { + this.insertions = insertions || [] + Object.assign(this, { snippet, index }) + } + + isValid () { + let any = this.insertions.some(insertion => insertion.isTransformation()) + if (!any) return true + let all = this.insertions.every(insertion => insertion.isTransformation()) + // If there are any transforming insertions, there must be at least one + // non-transforming insertion to act as the primary. + return !all + } + + addInsertion ({ range, substitution, references }) { + let insertion = new Insertion({ range, substitution, references }) + let insertions = this.insertions + insertions.push(insertion) + insertions = insertions.sort((i1, i2) => { + return i1.range.start.compare(i2.range.start) + }) + let initial = insertions.find(insertion => !insertion.isTransformation()) + if (initial) { + insertions.splice(insertions.indexOf(initial), 1) + insertions.unshift(initial) + } + this.insertions = insertions + } + + copyWithIndent (indent) { + let { snippet, index, insertions } = this + let newInsertions = insertions.map(insertion => { + let { range, substitution } = insertion + let newRange = Range.fromObject(range, true) + if (newRange.start.row) { + newRange.start.column += indent.length + newRange.end.column += indent.length + } + return new Insertion({ + range: newRange, + substitution + }) + }) + + return new TabStop({ + snippet, + index, + insertions: newInsertions + }) + } +} + +module.exports = TabStop diff --git a/packages/snippets/lib/variable.js b/packages/snippets/lib/variable.js new file mode 100644 index 000000000..dc0ac5067 --- /dev/null +++ b/packages/snippets/lib/variable.js @@ -0,0 +1,235 @@ +const path = require('path') +const crypto = require('crypto') +const Replacer = require('./replacer') +const FLAGS = require('./simple-transformations') +const {remote} = require('electron') + +function resolveClipboard () { + return atom.clipboard.read() +} + +function makeDateResolver (dateParams) { + // TODO: I do not know if this method ever returns anything other than + // 'en-us'; I suspect it does not. But this is likely the forward-compatible + // way of doing things. + // + // On the other hand, if the output of CURRENT_* variables _did_ vary based + // on locale, we'd probably need to implement a setting to force an arbitrary + // locale. I imagine lots of people use their native language for their OS's + // locale but write code in English. + // + let locale = remote.app.getLocale() + return () => new Date().toLocaleString(locale, dateParams) +} + +const RESOLVERS = { + // All the TM_-prefixed variables are part of the LSP specification for + // snippets. + 'TM_SELECTED_TEXT' ({editor, selectionRange, method}) { + // When a snippet is inserted via tab trigger, the trigger is + // programmatically selected prior to snippet expansion so that it is + // consumed when the snippet body is inserted. The trigger _should not_ be + // treated as selected text. There is no way for $TM_SELECTED_TEXT to + // contain anything when a snippet is invoked via tab trigger. + if (method === 'prefix') return '' + + if (!selectionRange || selectionRange.isEmpty()) return '' + return editor.getTextInBufferRange(selectionRange) + }, + 'TM_CURRENT_LINE' ({editor, cursor}) { + return editor.lineTextForBufferRow(cursor.getBufferRow()) + }, + 'TM_CURRENT_WORD' ({editor, cursor}) { + return editor.getTextInBufferRange(cursor.getCurrentWordBufferRange()) + }, + 'TM_LINE_INDEX' ({cursor}) { + return `${cursor.getBufferRow()}` + }, + 'TM_LINE_NUMBER' ({cursor}) { + return `${cursor.getBufferRow() + 1}` + }, + 'TM_FILENAME' ({editor}) { + return editor.getTitle() + }, + 'TM_FILENAME_BASE' ({editor}) { + let fileName = editor.getTitle() + if (!fileName) { return undefined } + + const index = fileName.lastIndexOf('.') + if (index >= 0) { + return fileName.slice(0, index) + } + return fileName + }, + 'TM_FILEPATH' ({editor}) { + return editor.getPath() + }, + 'TM_DIRECTORY' ({editor}) { + const filePath = editor.getPath() + if (filePath === undefined) return undefined + return path.dirname(filePath) + }, + + // VSCode supports these. + 'CLIPBOARD': resolveClipboard, + + 'CURRENT_YEAR': makeDateResolver({year: 'numeric'}), + 'CURRENT_YEAR_SHORT': makeDateResolver({year: '2-digit'}), + 'CURRENT_MONTH': makeDateResolver({month: '2-digit'}), + 'CURRENT_MONTH_NAME': makeDateResolver({month: 'long'}), + 'CURRENT_MONTH_NAME_SHORT': makeDateResolver({month: 'short'}), + 'CURRENT_DATE': makeDateResolver({day: '2-digit'}), + 'CURRENT_DAY_NAME': makeDateResolver({weekday: 'long'}), + 'CURRENT_DAY_NAME_SHORT': makeDateResolver({weekday: 'short'}), + 'CURRENT_HOUR': makeDateResolver({hour12: false, hour: '2-digit'}), + 'CURRENT_MINUTE': makeDateResolver({minute: '2-digit'}), + 'CURRENT_SECOND': makeDateResolver({second: '2-digit'}), + 'CURRENT_SECONDS_UNIX': () => { + return Math.floor( Date.now() / 1000 ) + }, + + // NOTE: "Ancestor project path" is determined as follows: + // + // * Get all project paths via `atom.project.getPaths()`. + // * Return the first path (in the order we received) that is an ancestor of + // the current file in the editor. + + // The current file's path relative to the ancestor project path. + 'RELATIVE_FILEPATH' ({editor}) { + let filePath = editor.getPath() + let projectPaths = atom.project.getPaths() + if (projectPaths.length === 0) { return filePath } + // A project can have multiple path roots. Return whichever is the first + // that is an ancestor of the file path. + let ancestor = projectPaths.find(pp => { + return filePath.startsWith(`${pp}${path.sep}`) + }) + if (!ancestor) return {filePath} + + return filePath.substring(ancestor.length) + }, + + // Last path component of the ancestor project path. + 'WORKSPACE_NAME' ({editor}) { + let projectPaths = atom.project.getPaths() + if (projectPaths.length === 0) { return '' } + let filePath = editor.getPath() + let ancestor = projectPaths.find(pp => { + return filePath.startsWith(`${pp}${path.sep}`) + }) + + return path.basename(ancestor) + }, + + // The full path to the ancestor project path. + 'WORKSPACE_FOLDER' ({editor}) { + let projectPaths = atom.project.getPaths() + if (projectPaths.length === 0) { return '' } + let filePath = editor.getPath() + let ancestor = projectPaths.find(pp => { + return filePath.startsWith(`${pp}${path.sep}`) + }) + + return ancestor + }, + + 'CURSOR_INDEX' ({editor, cursor}) { + let cursors = editor.getCursors() + let index = cursors.indexOf(cursor) + return index >= 0 ? String(index) : '' + }, + + 'CURSOR_NUMBER' ({editor, cursor}) { + let cursors = editor.getCursors() + let index = cursors.indexOf(cursor) + return index >= 0 ? String(index + 1) : '' + }, + + 'RANDOM' () { + return Math.random().toString().slice(-6) + }, + + 'RANDOM_HEX' () { + return Math.random().toString(16).slice(-6) + }, + + 'BLOCK_COMMENT_START' ({editor, cursor}) { + let delimiters = editor.getCommentDelimitersForBufferPosition( + cursor.getBufferPosition() + ) + return (delimiters?.block?.[0] ?? '').trim() + }, + + 'BLOCK_COMMENT_END' ({editor, cursor}) { + let delimiters = editor.getCommentDelimitersForBufferPosition( + cursor.getBufferPosition() + ) + return (delimiters?.block?.[1] ?? '').trim() + }, + + 'LINE_COMMENT' ({editor, cursor}) { + let delimiters = editor.getCommentDelimitersForBufferPosition( + cursor.getBufferPosition() + ) + return (delimiters?.line ?? '').trim() + } + + // TODO: VSCode also supports: + // + // UUID + // + // (can be done without dependencies once we use Node >= 14.17.0 or >= + // 15.6.0; see below) + // +} + +// $UUID will be easy to implement once Pulsar runs a newer version of Node, so +// there's no reason not to be proactive and sniff for the function we need. +if (('randomUUID' in crypto) && (typeof crypto.randomUUID === 'function')) { + RESOLVERS['UUID'] = () => { + return crypto.randomUUID({disableEntropyCache: true}) + } +} + + +function replaceByFlag (text, flag) { + let replacer = FLAGS[flag] + if (!replacer) { return text } + return replacer(text) +} + +class Variable { + constructor ({point, snippet, variable: name, substitution}) { + Object.assign(this, {point, snippet, name, substitution}) + } + + resolve (params) { + let base = '' + if (this.name in RESOLVERS) { + base = RESOLVERS[this.name](params) + } + + if (!this.substitution) { + return base + } + + let {flag, find, replace} = this.substitution + + // Two kinds of substitution. + if (flag) { + // This is the kind with the trailing `:/upcase`, `:/downcase`, etc. + return replaceByFlag(base, flag) + } else if (find && replace) { + // This is the more complex sed-style substitution. + let {find, replace} = this.substitution + this.replacer ??= new Replacer(replace) + return base.replace(find, (...args) => { + return this.replacer.replace(...args) + }) + } else { + return base + } + } +} + +module.exports = Variable diff --git a/packages/snippets/menus/snippets.cson b/packages/snippets/menus/snippets.cson new file mode 100644 index 000000000..6557d2a5b --- /dev/null +++ b/packages/snippets/menus/snippets.cson @@ -0,0 +1,12 @@ +'menu': [ + 'label': 'Packages' + 'submenu': [ + 'label': 'Snippets' + 'submenu': [ + { 'label': 'Expand', 'command': 'snippets:show' } + { 'label': 'Next Stop', 'command': 'snippets:next-tab-stop' } + { 'label': 'Previous Stop', 'command': 'snippets:previous-tab-stop' } + { 'label': 'Available', 'command': 'snippets:available' } + ] + ] +] diff --git a/packages/snippets/package-lock.json b/packages/snippets/package-lock.json new file mode 100644 index 000000000..5e74c65e3 --- /dev/null +++ b/packages/snippets/package-lock.json @@ -0,0 +1,2574 @@ +{ + "name": "snippets", + "version": "1.8.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "snippets", + "version": "1.8.0", + "license": "MIT", + "dependencies": { + "async": "~0.2.6", + "atom-select-list": "^0.7.0", + "pegjs": "^0.10.0", + "scoped-property-store": "^0.17.0", + "season": "^6.0.2", + "temp": "~0.8.0", + "underscore-plus": "^1.0.0" + }, + "devDependencies": { + "eslint": "^8.35.0" + }, + "engines": { + "atom": "*" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz", + "integrity": "sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.35.0.tgz", + "integrity": "sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/async": { + "version": "0.2.10" + }, + "node_modules/atom-select-list": { + "version": "0.7.2", + "license": "MIT", + "dependencies": { + "etch": "^0.12.6", + "fuzzaldrin": "^2.1.0" + } + }, + "node_modules/atom-slick": { + "version": "2.0.0", + "license": "MIT (http://mootools.net/license.txt)", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.8", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "3.2.0", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/coffee-script": { + "version": "1.12.7", + "license": "MIT", + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cson-parser": { + "version": "1.3.5", + "license": "BSD-3-Clause", + "dependencies": { + "coffee-script": "^1.10.0" + } + }, + "node_modules/d": { + "version": "0.1.1", + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emissary": { + "version": "1.3.3", + "dependencies": { + "es6-weak-map": "^0.1.2", + "mixto": "1.x", + "property-accessors": "^1.1", + "underscore-plus": "1.x" + } + }, + "node_modules/es5-ext": { + "version": "0.10.30", + "license": "MIT", + "dependencies": { + "es6-iterator": "2", + "es6-symbol": "~3.1" + } + }, + "node_modules/es5-ext/node_modules/d": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "es5-ext": "^0.10.9" + } + }, + "node_modules/es5-ext/node_modules/es6-iterator": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-symbol": "^3.1" + } + }, + "node_modules/es5-ext/node_modules/es6-symbol": { + "version": "3.1.1", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/es6-iterator": { + "version": "0.1.3", + "license": "MIT", + "dependencies": { + "d": "~0.1.1", + "es5-ext": "~0.10.5", + "es6-symbol": "~2.0.1" + } + }, + "node_modules/es6-symbol": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "d": "~0.1.1", + "es5-ext": "~0.10.5" + } + }, + "node_modules/es6-weak-map": { + "version": "0.1.4", + "license": "MIT", + "dependencies": { + "d": "~0.1.1", + "es5-ext": "~0.10.6", + "es6-iterator": "~0.1.3", + "es6-symbol": "~2.0.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz", + "integrity": "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^2.0.0", + "@eslint/js": "8.35.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", + "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etch": { + "version": "0.12.8", + "license": "MIT" + }, + "node_modules/event-kit": { + "version": "1.5.0", + "dependencies": { + "grim": "^1.2.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/fs-plus": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "async": "^1.5.2", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2", + "underscore-plus": "1.x" + } + }, + "node_modules/fs-plus/node_modules/async": { + "version": "1.5.2", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/fuzzaldrin": { + "version": "2.1.0" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/grim": { + "version": "1.5.0", + "dependencies": { + "emissary": "^1.2.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "license": "ISC" + }, + "node_modules/invert-kv": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/key-path-helpers": { + "version": "0.1.0" + }, + "node_modules/lcid": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "license": "MIT" + }, + "node_modules/mixto": { + "version": "1.0.0" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-locale": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "lcid": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pegjs": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", + "integrity": "sha512-qI5+oFNEGi3L5HAxDwN2LA4Gg7irF70Zs25edhjld9QemOgp0CbvMtbFcMvFtEo1OityPrcCzkQFB8JP/hxgow==", + "bin": { + "pegjs": "bin/pegjs" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/property-accessors": { + "version": "1.1.3", + "dependencies": { + "es6-weak-map": "^0.1.2", + "mixto": "1.x" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.6.2", + "license": "ISC", + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scoped-property-store": { + "version": "0.17.0", + "dependencies": { + "atom-slick": "^2", + "event-kit": "^1.0.0", + "grim": "^1.2.1", + "key-path-helpers": "^0.1.0", + "underscore-plus": "^1.6.3" + } + }, + "node_modules/season": { + "version": "6.0.2", + "dependencies": { + "cson-parser": "^1.3.0", + "fs-plus": "^3.0.0", + "yargs": "^3.23.0" + }, + "bin": { + "csonc": "bin/csonc" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/temp": { + "version": "0.8.3", + "engines": [ + "node >=0.8.0" + ], + "license": "MIT", + "dependencies": { + "os-tmpdir": "^1.0.0", + "rimraf": "~2.2.6" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.2.8", + "license": "MIT", + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/underscore": { + "version": "1.6.0" + }, + "node_modules/underscore-plus": { + "version": "1.6.6", + "dependencies": { + "underscore": "~1.6.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/window-size": { + "version": "0.1.4", + "license": "MIT", + "bin": { + "window-size": "cli.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "3.2.1", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "3.32.0", + "license": "MIT", + "dependencies": { + "camelcase": "^2.0.1", + "cliui": "^3.0.3", + "decamelize": "^1.1.1", + "os-locale": "^1.4.0", + "string-width": "^1.0.1", + "window-size": "^0.1.4", + "y18n": "^3.2.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@eslint/eslintrc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz", + "integrity": "sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@eslint/js": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.35.0.tgz", + "integrity": "sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "2.1.1" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "async": { + "version": "0.2.10" + }, + "atom-select-list": { + "version": "0.7.2", + "requires": { + "etch": "^0.12.6", + "fuzzaldrin": "^2.1.0" + } + }, + "atom-slick": { + "version": "2.0.0" + }, + "balanced-match": { + "version": "1.0.0" + }, + "brace-expansion": { + "version": "1.1.8", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "2.1.1" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cliui": { + "version": "3.2.0", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "code-point-at": { + "version": "1.1.0" + }, + "coffee-script": { + "version": "1.12.7" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "cson-parser": { + "version": "1.3.5", + "requires": { + "coffee-script": "^1.10.0" + } + }, + "d": { + "version": "0.1.1", + "requires": { + "es5-ext": "~0.10.2" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0" + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emissary": { + "version": "1.3.3", + "requires": { + "es6-weak-map": "^0.1.2", + "mixto": "1.x", + "property-accessors": "^1.1", + "underscore-plus": "1.x" + } + }, + "es5-ext": { + "version": "0.10.30", + "requires": { + "es6-iterator": "2", + "es6-symbol": "~3.1" + }, + "dependencies": { + "d": { + "version": "1.0.0", + "requires": { + "es5-ext": "^0.10.9" + } + }, + "es6-iterator": { + "version": "2.0.1", + "requires": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-symbol": "^3.1" + } + }, + "es6-symbol": { + "version": "3.1.1", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + } + } + }, + "es6-iterator": { + "version": "0.1.3", + "requires": { + "d": "~0.1.1", + "es5-ext": "~0.10.5", + "es6-symbol": "~2.0.1" + } + }, + "es6-symbol": { + "version": "2.0.1", + "requires": { + "d": "~0.1.1", + "es5-ext": "~0.10.5" + } + }, + "es6-weak-map": { + "version": "0.1.4", + "requires": { + "d": "~0.1.1", + "es5-ext": "~0.10.6", + "es6-iterator": "~0.1.3", + "es6-symbol": "~2.0.1" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz", + "integrity": "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^2.0.0", + "@eslint/js": "8.35.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", + "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etch": { + "version": "0.12.8" + }, + "event-kit": { + "version": "1.5.0", + "requires": { + "grim": "^1.2.1" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "fs-plus": { + "version": "3.0.1", + "requires": { + "async": "^1.5.2", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2", + "underscore-plus": "1.x" + }, + "dependencies": { + "async": { + "version": "1.5.2" + } + } + }, + "fs.realpath": { + "version": "1.0.0" + }, + "fuzzaldrin": { + "version": "2.1.0" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "grim": { + "version": "1.5.0", + "requires": { + "emissary": "^1.2.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3" + }, + "invert-kv": { + "version": "1.0.0" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "key-path-helpers": { + "version": "0.1.0" + }, + "lcid": { + "version": "1.0.0", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8" + }, + "mixto": { + "version": "1.0.0" + }, + "mkdirp": { + "version": "0.5.1", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1" + }, + "once": { + "version": "1.4.0", + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "os-locale": { + "version": "1.4.0", + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2" + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "pegjs": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", + "integrity": "sha512-qI5+oFNEGi3L5HAxDwN2LA4Gg7irF70Zs25edhjld9QemOgp0CbvMtbFcMvFtEo1OityPrcCzkQFB8JP/hxgow==" + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "property-accessors": { + "version": "1.1.3", + "requires": { + "es6-weak-map": "^0.1.2", + "mixto": "1.x" + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "2.6.2", + "requires": { + "glob": "^7.0.5" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "scoped-property-store": { + "version": "0.17.0", + "requires": { + "atom-slick": "^2", + "event-kit": "^1.0.0", + "grim": "^1.2.1", + "key-path-helpers": "^0.1.0", + "underscore-plus": "^1.6.3" + } + }, + "season": { + "version": "6.0.2", + "requires": { + "cson-parser": "^1.3.0", + "fs-plus": "^3.0.0", + "yargs": "^3.23.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "temp": { + "version": "0.8.3", + "requires": { + "os-tmpdir": "^1.0.0", + "rimraf": "~2.2.6" + }, + "dependencies": { + "rimraf": { + "version": "2.2.8" + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "underscore": { + "version": "1.6.0" + }, + "underscore-plus": { + "version": "1.6.6", + "requires": { + "underscore": "~1.6.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "window-size": { + "version": "0.1.4" + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2" + }, + "y18n": { + "version": "3.2.1" + }, + "yargs": { + "version": "3.32.0", + "requires": { + "camelcase": "^2.0.1", + "cliui": "^3.0.3", + "decamelize": "^1.1.1", + "os-locale": "^1.4.0", + "string-width": "^1.0.1", + "window-size": "^0.1.4", + "y18n": "^3.2.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/packages/snippets/package.json b/packages/snippets/package.json new file mode 100644 index 000000000..ef5fe4355 --- /dev/null +++ b/packages/snippets/package.json @@ -0,0 +1,31 @@ +{ + "name": "snippets", + "version": "1.8.0", + "main": "./lib/snippets", + "description": "Expand snippets matching the current prefix with `tab`.", + "repository": "https://github.com/pulsar-edit/snippets", + "license": "MIT", + "engines": { + "atom": "*" + }, + "dependencies": { + "async": "~0.2.6", + "atom-select-list": "^0.7.0", + "pegjs": "^0.10.0", + "scoped-property-store": "^0.17.0", + "season": "^6.0.2", + "temp": "~0.8.0", + "underscore-plus": "^1.0.0" + }, + "providedServices": { + "snippets": { + "description": "Snippets are text shortcuts that can be expanded to their definition.", + "versions": { + "0.1.0": "provideSnippets" + } + } + }, + "devDependencies": { + "eslint": "^8.35.0" + } +} diff --git a/packages/snippets/spec/.eslintrc b/packages/snippets/spec/.eslintrc new file mode 100644 index 000000000..65bf2aaca --- /dev/null +++ b/packages/snippets/spec/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "semi": ["error", "always"] + } +} diff --git a/packages/snippets/spec/body-parser-spec.js b/packages/snippets/spec/body-parser-spec.js new file mode 100644 index 000000000..41511d062 --- /dev/null +++ b/packages/snippets/spec/body-parser-spec.js @@ -0,0 +1,704 @@ +const BodyParser = require('../lib/snippet-body-parser'); + +function expectMatch (input, tree) { + expect(BodyParser.parse(input)).toEqual(tree); +} + +describe("Snippet Body Parser", () => { + it("parses a snippet with no special behavior", () => { + const bodyTree = BodyParser.parse('${} $ n $}1} ${/upcase/} \n world ${||}'); + expect(bodyTree).toEqual([ + '${} $ n $}1} ${/upcase/} \n world ${||}' + ]); + }); + + describe('for snippets with variables', () => { + it('parses simple variables', () => { + expectMatch('$f_o_0', [{variable: 'f_o_0'}]); + expectMatch('$_FOO', [{variable: '_FOO'}]); + }); + + it('parses verbose variables', () => { + expectMatch('${foo}', [{variable: 'foo'}]); + expectMatch('${FOO}', [{variable: 'FOO'}]); + }); + + it('parses variables with placeholders', () => { + expectMatch( + '${f:placeholder}', + [{variable: 'f', content: ['placeholder']}] + ); + + expectMatch( + '${f:foo$1 $VAR}', + [ + { + variable: 'f', + content: [ + 'foo', + {index: 1, content: []}, + ' ', + {variable: 'VAR'} + ] + } + ] + ); + + // Allows a colon as part of the placeholder value. + expectMatch( + '${TM_SELECTED_TEXT:foo:bar}', + [ + { + variable: 'TM_SELECTED_TEXT', + content: [ + 'foo:bar' + ] + } + ] + ); + }); + + it('parses simple transformations like /upcase', () => { + const bodyTree = BodyParser.parse("lorem ipsum ${CLIPBOARD:/upcase} dolor sit amet"); + expectMatch( + "lorem ipsum ${CLIPBOARD:/upcase} dolor sit amet", + [ + "lorem ipsum ", + { + variable: 'CLIPBOARD', + substitution: {flag: 'upcase'} + }, + " dolor sit amet" + ] + ); + }); + + it('parses variables with transforms', () => { + expectMatch('${f/.*/$0/}', [ + { + variable: 'f', + substitution: { + find: /.*/, + replace: [ + {backreference: 0} + ] + } + } + ]); + }); + }); + + + describe('for snippets with tabstops', () => { + it('parses simple tabstops', () => { + expectMatch('hello$1world$2', [ + 'hello', + {index: 1, content: []}, + 'world', + {index: 2, content: []} + ]); + }); + + it('parses verbose tabstops', () => { + expectMatch('hello${1}world${2}', [ + 'hello', + {index: 1, content: []}, + 'world', + {index: 2, content: []} + ]); + }); + + it('skips escaped tabstops', () => { + expectMatch('$1 \\$2 $3 \\\\$4 \\\\\\$5 $6', [ + {index: 1, content: []}, + ' $2 ', + {index: 3, content: []}, + ' \\', + {index: 4, content: []}, + ' \\$5 ', + {index: 6, content: []} + ]); + }); + + describe('for tabstops with placeholders', () => { + it('parses them', () => { + expectMatch('hello${1:placeholder}world', [ + 'hello', + {index: 1, content: ['placeholder']}, + 'world' + ]); + }); + + it('allows escaped back braces', () => { + expectMatch('${1:{}}', [ + {index: 1, content: ['{']}, + '}' + ]); + expectMatch('${1:{\\}}', [ + {index: 1, content: ['{}']} + ]); + }); + }); + + it('parses tabstops with transforms', () => { + expectMatch('${1/.*/$0/}', [ + { + index: 1, + content: [], + substitution: { + find: /.*/, + replace: [{backreference: 0}] + } + } + ]); + }); + + it('parses tabstops with choices', () => { + expectMatch('${1|on}e,t\\|wo,th\\,ree|}', [ + {index: 1, content: ['on}e'], choice: ['on}e', 't|wo', 'th,ree']} + ]); + }); + + it('parses if-else syntax', () => { + expectMatch( + '$1 ${1/(?:(wat)|^.*$)$/${1:+hey}/}', + [ + {index: 1, content: []}, + " ", + { + index: 1, + content: [], + substitution: { + find: /(?:(wat)|^.*$)$/, + replace: [ + { + backreference: 1, + iftext: "hey", + elsetext: "" + } + ], + }, + }, + ] + ); + + expectMatch( + '$1 ${1/(?:(wat)|^.*$)$/${1:?hey:nah}/}', + [ + {index: 1, content: []}, + " ", + { + index: 1, + content: [], + substitution: { + find: /(?:(wat)|^.*$)$/, + replace: [ + { + backreference: 1, + iftext: "hey", + elsetext: "nah" + } + ], + }, + }, + ] + ); + + // else with `:` syntax + expectMatch( + '$1 ${1/(?:(wat)|^.*$)$/${1:fallback}/}', + [ + {index: 1, content: []}, + " ", + { + index: 1, + content: [], + substitution: { + find: /(?:(wat)|^.*$)$/, + replace: [ + { + backreference: 1, + iftext: "", + elsetext: "fallback" + } + ], + }, + }, + ] + ); + + + // else with `:-` syntax; should be same as above + expectMatch( + '$1 ${1/(?:(wat)|^.*$)$/${1:-fallback}/}', + [ + {index: 1, content: []}, + " ", + { + index: 1, + content: [], + substitution: { + find: /(?:(wat)|^.*$)$/, + replace: [ + { + backreference: 1, + iftext: "", + elsetext: "fallback" + } + ], + }, + }, + ] + ); + + }); + + it('parses alternative if-else syntax', () => { + expectMatch( + '$1 ${1/(?:(wat)|^.*$)$/(?1:hey:)/}', + [ + {index: 1, content: []}, + " ", + { + index: 1, + content: [], + substitution: { + find: /(?:(wat)|^.*$)$/, + replace: [ + { + backreference: 1, + iftext: ["hey"], + elsetext: "" + } + ], + }, + }, + ] + ); + + + expectMatch( + '$1 ${1/(?:(wat)|^.*$)$/(?1:\\u$1:)/}', + [ + {index: 1, content: []}, + " ", + { + index: 1, + content: [], + substitution: { + find: /(?:(wat)|^.*$)$/, + replace: [ + { + backreference: 1, + iftext: [ + {escape: 'u'}, + {backreference: 1} + ], + elsetext: "" + } + ], + }, + }, + ] + ); + + expectMatch( + '$1 ${1/(?:(wat)|^.*$)$/(?1::hey)/}', + [ + {index: 1, content: []}, + " ", + { + index: 1, + content: [], + substitution: { + find: /(?:(wat)|^.*$)$/, + replace: [ + { + backreference: 1, + iftext: "", + elsetext: ["hey"] + } + ], + }, + }, + ] + ); + + expectMatch( + 'class ${1:${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} < ${2:Application}Controller\n $3\nend', + [ + 'class ', + { + index: 1, + content: [ + { + variable: 'TM_FILENAME', + substitution: { + find: /(?:\A|_)([A-Za-z0-9]+)(?:\.rb)?/g, + replace: [ + { + backreference: 2, + iftext: '', + elsetext: [ + {escape: 'u'}, + {backreference: 1} + ] + } + ] + } + } + ] + }, + ' < ', + { + index: 2, + content: ['Application'] + }, + 'Controller\n ', + {index: 3, content : []}, + '\nend' + ] + ); + }); + + it('recognizes escape characters in if/else syntax', () => { + + expectMatch( + '$1 ${1/(?:(wat)|^.*$)$/${1:?hey\\:hey:nah}/}', + [ + {index: 1, content: []}, + " ", + { + index: 1, + content: [], + substitution: { + find: /(?:(wat)|^.*$)$/, + replace: [ + { + backreference: 1, + iftext: "hey:hey", + elsetext: "nah" + } + ], + }, + }, + ] + ); + + expectMatch( + '$1 ${1/(?:(wat)|^.*$)$/${1:?hey:n\\}ah}/}', + [ + {index: 1, content: []}, + " ", + { + index: 1, + content: [], + substitution: { + find: /(?:(wat)|^.*$)$/, + replace: [ + { + backreference: 1, + iftext: "hey", + elsetext: "n}ah" + } + ], + }, + }, + ] + ); + + }); + + + it('parses nested tabstops', () => { + expectMatch( + '${1:place${2:hol${3:der}}}', + [ + { + index: 1, + content: [ + 'place', + {index: 2, content: [ + 'hol', + {index: 3, content: ['der']} + ]} + ] + } + ] + ); + + expectMatch( + '${1:${foo:${1}}}', + [ + { + index: 1, + content: [ + { + variable: 'foo', + content: [ + { + index: 1, + content: [] + } + ] + } + ] + } + ] + ); + }); + }); + + + it("breaks a snippet body into lines, with each line containing tab stops at the appropriate position", () => { + const bodyTree = BodyParser.parse(`\ +the quick brown $1fox \${2:jumped \${3:over} +}the \${4:lazy} dog\ +` + ); + + expect(bodyTree).toEqual([ + "the quick brown ", + {index: 1, content: []}, + "fox ", + { + index: 2, + content: [ + "jumped ", + {index: 3, content: ["over"]}, + "\n" + ], + }, + "the ", + {index: 4, content: ["lazy"]}, + " dog" + ]); + }); + + + it('handles a snippet with a transformed variable', () => { + expectMatch( + 'module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/\\u$1/g}}', + [ + 'module ', + { + index: 1, + content: [ + 'ActiveRecord::', + { + variable: 'TM_FILENAME', + substitution: { + find: /(?:\A|_)([A-Za-z0-9]+)(?:\.rb)?/g, + replace: [ + {escape: 'u'}, + {backreference: 1} + ] + } + } + ] + } + ] + ); + }); + + it("skips escaped tabstops", () => { + const bodyTree = BodyParser.parse("snippet $1 escaped \\$2 \\\\$3"); + expect(bodyTree).toEqual([ + "snippet ", + { + index: 1, + content: [] + }, + " escaped $2 \\", + { + index: 3, + content: [] + } + ]); + }); + + it("includes escaped right-braces", () => { + const bodyTree = BodyParser.parse("snippet ${1:{\\}}"); + expect(bodyTree).toEqual([ + "snippet ", + { + index: 1, + content: ["{}"] + } + ]); + }); + + it("parses a snippet with transformations", () => { + const bodyTree = BodyParser.parse("<${1:p}>$0"); + expect(bodyTree).toEqual([ + '<', + {index: 1, content: ['p']}, + '>', + {index: 0, content: []}, + '' + ]); + }); + + it("parses a snippet with transformations and a global flag", () => { + const bodyTree = BodyParser.parse("<${1:p}>$0"); + expect(bodyTree).toEqual([ + '<', + {index: 1, content: ['p']}, + '>', + {index: 0, content: []}, + '' + ]); + }); + + it("parses a snippet with multiple tab stops with transformations", () => { + const bodyTree = BodyParser.parse("${1:placeholder} ${1/(.)/\\u$1/g} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2"); + expect(bodyTree).toEqual([ + {index: 1, content: ['placeholder']}, + ' ', + { + index: 1, + content: [], + substitution: { + find: /(.)/g, + replace: [ + {escape: 'u'}, + {backreference: 1} + ] + } + }, + ' ', + {index: 1, content: []}, + ' ', + {index: 2, content: ['ANOTHER']}, + ' ', + { + index: 2, + content: [], + substitution: { + find: /^(.*)$/, + replace: [ + {escape: 'L'}, + {backreference: 1} + ] + } + }, + ' ', + {index: 2, content: []}, + ]); + }); + + + it("parses a snippet with transformations and mirrors", () => { + const bodyTree = BodyParser.parse("${1:placeholder}\n${1/(.)/\\u$1/g}\n$1"); + expect(bodyTree).toEqual([ + {index: 1, content: ['placeholder']}, + '\n', + { + index: 1, + content: [], + substitution: { + find: /(.)/g, + replace: [ + {escape: 'u'}, + {backreference: 1} + ] + } + }, + '\n', + {index: 1, content: []} + ]); + }); + + it("parses a snippet with a format string and case-control flags", () => { + const bodyTree = BodyParser.parse("<${1:p}>$0"); + expect(bodyTree).toEqual([ + '<', + {index: 1, content: ['p']}, + '>', + {index: 0, content: []}, + '' + ]); + }); + + it("parses a snippet with an escaped forward slash in a transform", () => { + // Annoyingly, a forward slash needs to be double-backslashed just like the + // other escapes. + const bodyTree = BodyParser.parse("<${1:p}>$0"); + expect(bodyTree).toEqual([ + '<', + {index: 1, content: ['p']}, + '>', + {index: 0, content: []}, + '' + ]); + }); + + it("parses a snippet with a placeholder that mirrors another tab stop's content", () => { + const bodyTree = BodyParser.parse("$4console.${3:log}('${2:$1}', $1);$0"); + expect(bodyTree).toEqual([ + {index: 4, content: []}, + 'console.', + {index: 3, content: ['log']}, + '(\'', + { + index: 2, content: [ + {index: 1, content: []} + ] + }, + '\', ', + {index: 1, content: []}, + ');', + {index: 0, content: []} + ]); + }); + + it("parses a snippet with a placeholder that mixes text and tab stop references", () => { + const bodyTree = BodyParser.parse("$4console.${3:log}('${2:uh $1}', $1);$0"); + expect(bodyTree).toEqual([ + {index: 4, content: []}, + 'console.', + {index: 3, content: ['log']}, + '(\'', + { + index: 2, content: [ + 'uh ', + {index: 1, content: []} + ] + }, + '\', ', + {index: 1, content: []}, + ');', + {index: 0, content: []} + ]); + }); +}); diff --git a/packages/snippets/spec/fixtures/package-with-broken-snippets/snippets/.hidden-file b/packages/snippets/spec/fixtures/package-with-broken-snippets/snippets/.hidden-file new file mode 100644 index 000000000..35b867918 --- /dev/null +++ b/packages/snippets/spec/fixtures/package-with-broken-snippets/snippets/.hidden-file @@ -0,0 +1 @@ +I am hidden so I shouldn't be loaded diff --git a/packages/snippets/spec/fixtures/package-with-broken-snippets/snippets/invalid.json b/packages/snippets/spec/fixtures/package-with-broken-snippets/snippets/invalid.json new file mode 100644 index 000000000..7c82ed121 --- /dev/null +++ b/packages/snippets/spec/fixtures/package-with-broken-snippets/snippets/invalid.json @@ -0,0 +1 @@ +I am not a valid JSON file but that shouldn't cause a crisis diff --git a/packages/snippets/spec/fixtures/package-with-snippets/snippets/.hidden-file b/packages/snippets/spec/fixtures/package-with-snippets/snippets/.hidden-file new file mode 100644 index 000000000..7aa86d685 --- /dev/null +++ b/packages/snippets/spec/fixtures/package-with-snippets/snippets/.hidden-file @@ -0,0 +1 @@ +This is a hidden file. Don't even try to load it as a snippet diff --git a/packages/snippets/spec/fixtures/package-with-snippets/snippets/junk-file b/packages/snippets/spec/fixtures/package-with-snippets/snippets/junk-file new file mode 100644 index 000000000..5549cb956 --- /dev/null +++ b/packages/snippets/spec/fixtures/package-with-snippets/snippets/junk-file @@ -0,0 +1 @@ +This file isn't CSON, but shouldn't be a big deal \ No newline at end of file diff --git a/packages/snippets/spec/fixtures/package-with-snippets/snippets/test.cson b/packages/snippets/spec/fixtures/package-with-snippets/snippets/test.cson new file mode 100644 index 000000000..cb28534a5 --- /dev/null +++ b/packages/snippets/spec/fixtures/package-with-snippets/snippets/test.cson @@ -0,0 +1,31 @@ +".test": + "Test Snippet": + prefix: "test" + body: "testing 123" + "Test Snippet With Description": + prefix: "testd" + body: "testing 456" + description: "a description" + descriptionMoreURL: "http://google.com" + "Test Snippet With A Label On The Left": + prefix: "testlabelleft" + body: "testing 456" + leftLabel: "a label" + "Test Snippet With HTML Labels": + prefix: "testhtmllabels" + body: "testing 456" + leftLabelHTML: "Label" + rightLabelHTML: "Label" + +".package-with-snippets-unique-scope": + "Test Snippet": + prefix: "test" + body: "testing 123" + +".source.js": + "Overrides a core package's snippet": + prefix: "log" + body: "from-a-community-package" + "Maps to a command": + body: 'lorem ipsum $0 dolor sit amet' + command: 'test-command-name' diff --git a/packages/snippets/spec/fixtures/sample.js b/packages/snippets/spec/fixtures/sample.js new file mode 100644 index 000000000..566ae67db --- /dev/null +++ b/packages/snippets/spec/fixtures/sample.js @@ -0,0 +1,13 @@ +var quicksort = function () { + var sort = function(items) { + if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + return sort(left).concat(pivot).concat(sort(right)); + }; + + return sort(Array.apply(this, arguments)); +}; diff --git a/packages/snippets/spec/insertion-spec.js b/packages/snippets/spec/insertion-spec.js new file mode 100644 index 000000000..83fac925c --- /dev/null +++ b/packages/snippets/spec/insertion-spec.js @@ -0,0 +1,134 @@ +const Insertion = require('../lib/insertion') +const { Range } = require('atom') + +const range = new Range(0, 0) + +describe('Insertion', () => { + it('returns what it was given when it has no substitution', () => { + let insertion = new Insertion({ + range, + substitution: undefined + }) + let transformed = insertion.transform('foo!') + + expect(transformed).toEqual('foo!') + }) + + it('transforms what it was given when it has a regex transformation', () => { + let insertion = new Insertion({ + range, + substitution: { + find: /foo/g, + replace: ['bar'] + } + }) + let transformed = insertion.transform('foo!') + + expect(transformed).toEqual('bar!') + }) + + it('transforms the case of the next character when encountering a \\u or \\l flag', () => { + let uInsertion = new Insertion({ + range, + substitution: { + find: /(.)(.)(.*)/g, + replace: [ + { backreference: 1 }, + { escape: 'u' }, + { backreference: 2 }, + { backreference: 3 } + ] + } + }) + + expect(uInsertion.transform('foo!')).toEqual('fOo!') + expect(uInsertion.transform('fOo!')).toEqual('fOo!') + expect(uInsertion.transform('FOO!')).toEqual('FOO!') + + let lInsertion = new Insertion({ + range, + substitution: { + find: /(.{2})(.)(.*)/g, + replace: [ + { backreference: 1 }, + { escape: 'l' }, + { backreference: 2 }, + { backreference: 3 } + ] + } + }) + + expect(lInsertion.transform('FOO!')).toEqual('FOo!') + expect(lInsertion.transform('FOo!')).toEqual('FOo!') + expect(lInsertion.transform('FoO!')).toEqual('Foo!') + expect(lInsertion.transform('foo!')).toEqual('foo!') + }) + + it('transforms the case of all remaining characters when encountering a \\U or \\L flag, up until it sees a \\E flag', () => { + let uInsertion = new Insertion({ + range, + substitution: { + find: /(.)(.*)/, + replace: [ + { backreference: 1 }, + { escape: 'U' }, + { backreference: 2 } + ] + } + }) + + expect(uInsertion.transform('lorem ipsum!')).toEqual('lOREM IPSUM!') + expect(uInsertion.transform('lOREM IPSUM!')).toEqual('lOREM IPSUM!') + expect(uInsertion.transform('LOREM IPSUM!')).toEqual('LOREM IPSUM!') + + let ueInsertion = new Insertion({ + range, + substitution: { + find: /(.)(.{3})(.*)/, + replace: [ + { backreference: 1 }, + { escape: 'U' }, + { backreference: 2 }, + { escape: 'E' }, + { backreference: 3 } + ] + } + }) + + expect(ueInsertion.transform('lorem ipsum!')).toEqual('lOREm ipsum!') + expect(ueInsertion.transform('lOREm ipsum!')).toEqual('lOREm ipsum!') + expect(ueInsertion.transform('LOREM IPSUM!')).toEqual('LOREM IPSUM!') + + let lInsertion = new Insertion({ + range, + substitution: { + find: /(.{4})(.)(.*)/, + replace: [ + { backreference: 1 }, + { escape: 'L' }, + { backreference: 2 }, + 'WHAT' + ] + } + }) + + expect(lInsertion.transform('LOREM IPSUM!')).toEqual('LOREmwhat') + + let leInsertion = new Insertion({ + range, + substitution: { + find: /^([A-Fa-f])(.*)(.)$/, + replace: [ + { backreference: 1 }, + { escape: 'L' }, + { backreference: 2 }, + { escape: 'E' }, + { backreference: 3 } + ] + } + }) + + expect(leInsertion.transform('LOREM IPSUM!')).toEqual('LOREM IPSUM!') + expect(leInsertion.transform('CONSECUETUR')).toEqual('ConsecuetuR') + }) +}) diff --git a/packages/snippets/spec/snippet-loading-spec.js b/packages/snippets/spec/snippet-loading-spec.js new file mode 100644 index 000000000..78b86645d --- /dev/null +++ b/packages/snippets/spec/snippet-loading-spec.js @@ -0,0 +1,345 @@ +const path = require('path'); +const fs = require('fs'); +const temp = require('temp').track(); + +describe("Snippet Loading", () => { + let configDirPath, snippetsService; + + beforeEach(() => { + configDirPath = temp.mkdirSync('atom-config-dir-'); + spyOn(atom, 'getConfigDirPath').andReturn(configDirPath); + + spyOn(console, 'warn'); + if (atom.notifications != null) { spyOn(atom.notifications, 'addError'); } + + spyOn(atom.packages, 'getLoadedPackages').andReturn([ + atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-snippets')), + atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-broken-snippets')), + ]); + }); + + afterEach(() => { + waitsForPromise(() => Promise.resolve(atom.packages.deactivatePackages('snippets'))); + runs(() => { + jasmine.unspy(atom.packages, 'getLoadedPackages'); + }); + }); + + const activateSnippetsPackage = () => { + waitsForPromise(() => atom.packages.activatePackage("snippets").then(({mainModule}) => { + snippetsService = mainModule.provideSnippets(); + mainModule.loaded = false; + })); + + waitsFor("all snippets to load", 3000, () => snippetsService.bundledSnippetsLoaded()); + }; + + it("loads the bundled snippet template snippets", () => { + activateSnippetsPackage(); + + runs(() => { + const jsonSnippet = snippetsService.snippetsForScopes(['.source.json'])['snip']; + expect(jsonSnippet.name).toBe('Atom Snippet'); + expect(jsonSnippet.prefix).toBe('snip'); + expect(jsonSnippet.body).toContain('"prefix":'); + expect(jsonSnippet.body).toContain('"body":'); + expect(jsonSnippet.tabStopList.length).toBeGreaterThan(0); + + const csonSnippet = snippetsService.snippetsForScopes(['.source.coffee'])['snip']; + expect(csonSnippet.name).toBe('Atom Snippet'); + expect(csonSnippet.prefix).toBe('snip'); + expect(csonSnippet.body).toContain("'prefix':"); + expect(csonSnippet.body).toContain("'body':"); + expect(csonSnippet.tabStopList.length).toBeGreaterThan(0); + }); + }); + + it("loads non-hidden snippet files from atom packages with snippets directories", () => { + activateSnippetsPackage(); + + runs(() => { + let snippet = snippetsService.snippetsForScopes(['.test'])['test']; + expect(snippet.prefix).toBe('test'); + expect(snippet.body).toBe('testing 123'); + + snippet = snippetsService.snippetsForScopes(['.test'])['testd']; + expect(snippet.prefix).toBe('testd'); + expect(snippet.body).toBe('testing 456'); + expect(snippet.description).toBe('a description'); + expect(snippet.descriptionMoreURL).toBe('http://google.com'); + + snippet = snippetsService.snippetsForScopes(['.test'])['testlabelleft']; + expect(snippet.prefix).toBe('testlabelleft'); + expect(snippet.body).toBe('testing 456'); + expect(snippet.leftLabel).toBe('a label'); + + snippet = snippetsService.snippetsForScopes(['.test'])['testhtmllabels']; + expect(snippet.prefix).toBe('testhtmllabels'); + expect(snippet.body).toBe('testing 456'); + expect(snippet.leftLabelHTML).toBe('Label'); + expect(snippet.rightLabelHTML).toBe('Label'); + }); + }); + + it("registers a command if a package snippet defines one", () => { + waitsForPromise(() => { + return atom.packages.activatePackage("snippets").then( + ({mainModule}) => { + return new Promise((resolve) => { + mainModule.onDidLoadSnippets(resolve); + }); + } + ); + }); + + runs(() => { + expect( + 'package-with-snippets:test-command-name' in atom.commands.registeredCommands + ).toBe(true); + }); + }); + + it("logs a warning if package snippets files cannot be parsed", () => { + activateSnippetsPackage(); + + runs(() => { + // Warn about invalid-file, but don't even try to parse a hidden file + expect(console.warn.calls.length).toBeGreaterThan(0); + expect(console.warn.mostRecentCall.args[0]).toMatch(/Error reading.*package-with-broken-snippets/); + }); + }); + + describe("::loadPackageSnippets(callback)", () => { + const jsPackage = () => { + const pack = atom.packages.loadPackage('language-javascript') + pack.path = path.join( + atom.getLoadSettings().resourcePath, + 'node_modules', 'language-javascript' + ) + return pack + } + + beforeEach(() => { // simulate a list of packages where the javascript core package is returned at the end + atom.packages.getLoadedPackages.andReturn([ + atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-snippets')), + jsPackage() + ]); + }); + + // NOTE: This spec will fail if you're hacking on the Pulsar source code + // with `ATOM_DEV_RESOURCE_PATH`. Just make sure it passes in CI and you'll + // be fine. + it("allows other packages to override core packages' snippets", () => { + waitsForPromise(() => atom.packages.activatePackage("language-javascript")); + + activateSnippetsPackage(); + + runs(() => { + const snippet = snippetsService.snippetsForScopes(['.source.js'])['log']; + expect(snippet.body).toBe("from-a-community-package"); + }); + }); + }); + + describe("::onDidLoadSnippets(callback)", () => { + it("invokes listeners when all snippets are loaded", () => { + let loadedCallback = null; + + waitsFor("package to activate", done => atom.packages.activatePackage("snippets").then(({mainModule}) => { + mainModule.onDidLoadSnippets(loadedCallback = jasmine.createSpy('onDidLoadSnippets callback')); + done(); + })); + + waitsFor("onDidLoad callback to be called", () => loadedCallback.callCount > 0); + }); + }); + + describe("when ~/.atom/snippets.json exists", () => { + beforeEach(() => { + fs.mkdirSync(configDirPath, {recursive: true}); + fs.writeFileSync(path.join(configDirPath, 'snippets.json'), `\ +{ + ".foo": { + "foo snippet": { + "prefix": "foo", + "body": "bar1" + } + } +}\ +` + ); + activateSnippetsPackage(); + }); + + it("loads the snippets from that file", () => { + let snippet = null; + + waitsFor(() => snippet = snippetsService.snippetsForScopes(['.foo'])['foo']); + + runs(() => { + expect(snippet.name).toBe('foo snippet'); + expect(snippet.prefix).toBe("foo"); + expect(snippet.body).toBe("bar1"); + }); + }); + + describe("when that file changes", () => { + it("reloads the snippets", () => { + fs.mkdirSync(configDirPath, {recursive: true}); + fs.writeFileSync(path.join(configDirPath, 'snippets.json'), `\ +{ +".foo": { + "foo snippet": { + "prefix": "foo", + "body": "bar2" + } +} +}\ +` + ); + + waitsFor("snippets to be changed", () => { + const snippet = snippetsService.snippetsForScopes(['.foo'])['foo']; + return snippet && snippet.body === 'bar2'; + }); + + runs(() => { + fs.mkdirSync(configDirPath, {recursive: true}); + fs.writeFileSync(path.join(configDirPath, 'snippets.json'), ""); + }); + + waitsFor("snippets to be removed", () => !snippetsService.snippetsForScopes(['.foo'])['foo']); + }); + }); + }); + + describe("when ~/.atom/snippets.cson exists", () => { + beforeEach(() => { + fs.mkdirSync(configDirPath, {recursive: true}); + fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), `\ +".foo": + "foo snippet": + "prefix": "foo" + "body": "bar1"\ +` + ); + activateSnippetsPackage(); + }); + + it("loads the snippets from that file", () => { + let snippet = null; + + waitsFor(() => snippet = snippetsService.snippetsForScopes(['.foo'])['foo']); + + runs(() => { + expect(snippet.name).toBe('foo snippet'); + expect(snippet.prefix).toBe("foo"); + expect(snippet.body).toBe("bar1"); + }); + }); + + describe("when that file changes", () => { + it("reloads the snippets", () => { + fs.mkdirSync(configDirPath, {recursive: true}); + fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), `\ +".foo": + "foo snippet": + "prefix": "foo" + "body": "bar2"\ +` + ); + + waitsFor("snippets to be changed", () => { + const snippet = snippetsService.snippetsForScopes(['.foo'])['foo']; + return snippet && snippet.body === 'bar2'; + }); + + runs(() => { + fs.mkdirSync(configDirPath, {recursive: true}); + fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), ""); + }); + + waitsFor("snippets to be removed", () => { + const snippet = snippetsService.snippetsForScopes(['.foo'])['foo']; + return snippet == null; + }); + }); + }); + }); + + it("notifies the user when the user snippets file cannot be loaded", () => { + fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), '".junk":::'); + + activateSnippetsPackage(); + + runs(() => { + expect(console.warn).toHaveBeenCalled(); + if (atom.notifications != null) { + expect(atom.notifications.addError).toHaveBeenCalled(); + } + }); + }); + + describe("packages-with-snippets-disabled feature", () => { + it("disables no snippets if the config option is empty", () => { + const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled'); + atom.config.set('core.packagesWithSnippetsDisabled', []); + + activateSnippetsPackage(); + runs(() => { + const snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(1); + atom.config.set('core.packagesWithSnippetsDisabled', originalConfig); + }); + }); + + it("still includes a disabled package's snippets in the list of unparsed snippets", () => { + let originalConfig = atom.config.get('core.packagesWithSnippetsDisabled'); + atom.config.set('core.packagesWithSnippetsDisabled', []); + + activateSnippetsPackage(); + runs(() => { + atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']); + const allSnippets = snippetsService.getUnparsedSnippets(); + const scopedSnippet = allSnippets.find(s => s.selectorString === '.package-with-snippets-unique-scope'); + expect(scopedSnippet).not.toBe(undefined); + atom.config.set('core.packagesWithSnippetsDisabled', originalConfig); + }); + }); + + it("never loads a package's snippets when that package is disabled in config", () => { + const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled'); + atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']); + + activateSnippetsPackage(); + runs(() => { + const snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(0); + atom.config.set('core.packagesWithSnippetsDisabled', originalConfig); + }); + }); + + it("unloads and/or reloads snippets from a package if the config option is changed after activation", () => { + const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled'); + atom.config.set('core.packagesWithSnippetsDisabled', []); + + activateSnippetsPackage(); + runs(() => { + let snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(1); + + // Disable it. + atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']); + snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(0); + + // Re-enable it. + atom.config.set('core.packagesWithSnippetsDisabled', []); + snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(1); + + atom.config.set('core.packagesWithSnippetsDisabled', originalConfig); + }); + }); + }); +}); diff --git a/packages/snippets/spec/snippets-spec.js b/packages/snippets/spec/snippets-spec.js new file mode 100644 index 000000000..637a55e44 --- /dev/null +++ b/packages/snippets/spec/snippets-spec.js @@ -0,0 +1,2017 @@ +const path = require('path'); +const temp = require('temp').track(); +const Snippets = require('../lib/snippets'); +const {TextEditor} = require('atom'); +const crypto = require('crypto'); + +const SUPPORTS_UUID = ('randomUUID' in crypto) && (typeof crypto.randomUUID === 'function'); + +describe("Snippets extension", () => { + let editorElement, editor, languageMode; + let modernTreeSitterIsDefault = null; + + const simulateTabKeyEvent = (param) => { + if (param == null) { + param = {}; + } + const {shift} = param; + const event = atom.keymaps.constructor.buildKeydownEvent('tab', {shift, target: editorElement}); + atom.keymaps.handleKeyboardEvent(event); + }; + + beforeEach(async () => { + if (modernTreeSitterIsDefault === null) { + let oldSetting = atom.config.getSchema('core.useExperimentalModernTreeSitter'); + if (oldSetting?.type === 'boolean') { + modernTreeSitterIsDefault = false; + } + } + if (!modernTreeSitterIsDefault) { + atom.config.set('core.useExperimentalModernTreeSitter', true); + } + if (atom.notifications != null) { spyOn(atom.notifications, 'addError'); } + spyOn(Snippets, 'loadAll'); + spyOn(Snippets, 'getUserSnippetsPath').andReturn(''); + + await atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.js')); + await atom.packages.activatePackage('language-javascript'); + await atom.packages.activatePackage('language-python'); + await atom.packages.activatePackage('language-html'); + await atom.packages.activatePackage('snippets'); + + editor = atom.workspace.getActiveTextEditor(); + editorElement = atom.views.getView(editor); + languageMode = editor.getBuffer().getLanguageMode(); + await languageMode.ready; + languageMode.useAsyncParsing = false; + }); + + afterEach(async () => { + if (languageMode) { + await languageMode.atTransactionEnd(); + } + await atom.packages.deactivatePackage('snippets'); + }); + + describe("provideSnippets interface", () => { + let snippetsInterface = null; + + beforeEach(() => { + snippetsInterface = Snippets.provideSnippets(); + }); + + describe("bundledSnippetsLoaded", () => { + it("indicates the loaded state of the bundled snippets", () => { + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false); + Snippets.doneLoading(); + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true); + }); + + it("resets the loaded state after snippets is deactivated", async () => { + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false); + Snippets.doneLoading(); + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true); + + await atom.packages.deactivatePackage('snippets'); + await atom.packages.activatePackage('snippets'); + + runs(() => { + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false); + Snippets.doneLoading(); + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true); + }); + }); + }); + + describe("insertSnippet", () => { + it("can insert a snippet", () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]); + snippetsInterface.insertSnippet("hello ${1:world}", editor); + expect(editor.lineTextForBufferRow(0)).toBe("var hello world = function () {"); + }); + }); + }); + + it("returns false for snippetToExpandUnderCursor if getSnippets returns {}", () => { + const snippets = atom.packages.getActivePackage('snippets').mainModule; + expect(snippets.snippetToExpandUnderCursor(editor)).toEqual(false); + }); + + it("ignores invalid snippets in the config", () => { + const snippets = atom.packages.getActivePackage('snippets').mainModule; + + let invalidSnippets = null; + spyOn(snippets.scopedPropertyStore, 'getPropertyValue').andCallFake(() => invalidSnippets); + expect(snippets.getSnippets(editor)).toEqual({}); + + invalidSnippets = 'test'; + expect(snippets.getSnippets(editor)).toEqual({}); + + invalidSnippets = []; + expect(snippets.getSnippets(editor)).toEqual({}); + + invalidSnippets = 3; + expect(snippets.getSnippets(editor)).toEqual({}); + + invalidSnippets = {a: null}; + expect(snippets.getSnippets(editor)).toEqual({}); + }); + + describe("when null snippets are present", () => { + beforeEach(() => Snippets.add(__filename, { + ".source.js": { + "some snippet": { + prefix: "t1", + body: "this is a test" + } + }, + + ".source.js .nope": { + "some snippet": { + prefix: "t1", + body: null + } + } + })); + + it("overrides the less-specific defined snippet", () => { + const snippets = Snippets.provideSnippets(); + expect(snippets.snippetsForScopes(['.source.js'])['t1']).toBeTruthy(); + expect(snippets.snippetsForScopes(['.source.js .nope.not-today'])['t1']).toBeFalsy(); + }); + }); + + describe("when 'tab' is triggered on the editor", () => { + beforeEach(() => { + Snippets.add(__filename, { + ".source.js": { + "without tab stops": { + prefix: "t1", + body: "this is a test" + }, + + "with only an end tab stop": { + prefix: "t1a", + body: "something $0 strange" + }, + + "overlapping prefix": { + prefix: "tt1", + body: "this is another test" + }, + + "special chars": { + prefix: "@unique", + body: "@unique see" + }, + + "tab stops": { + prefix: "t2", + body: `\ +go here next:($2) and finally go here:($0) +go here first:($1) +\ +` + }, + + "indented second line": { + prefix: "t3", + body: `\ +line 1 +\tline 2$1 +$2\ +` + }, + + "multiline with indented placeholder tabstop": { + prefix: "t4", + body: `\ +line \${1:1} + \${2:body...}\ +` + }, + + "multiline starting with tabstop": { + prefix: "t4b", + body: `\ +$1 = line 1 { + line 2 +}\ +` + }, + + "nested tab stops": { + prefix: "t5", + body: '${1:"${2:key}"}: ${3:value}' + }, + + "caused problems with undo": { + prefix: "t6", + body: `\ +first line$1 +\${2:placeholder ending second line}\ +` + }, + + "tab stops at beginning and then end of snippet": { + prefix: "t6b", + body: "$1expanded$0" + }, + + "tab stops at end and then beginning of snippet": { + prefix: "t6c", + body: "$0expanded$1" + }, + + "contains empty lines": { + prefix: "t7", + body: `\ +first line $1 + + +fourth line after blanks $2\ +` + }, + "with/without placeholder": { + prefix: "t8", + body: `\ +with placeholder \${1:test} +without placeholder \${2}\ +` + }, + + "multi-caret": { + prefix: "t9", + body: `\ +with placeholder \${1:test} +without placeholder $1\ +` + }, + + "multi-caret-multi-tabstop": { + prefix: "t9b", + body: `\ +with placeholder \${1:test} +without placeholder $1 +second tabstop $2 +third tabstop $3\ +` + }, + + "large indices": { + prefix: "t10", + body: "hello${10} ${11:large} indices${1}" + }, + + "no body": { + prefix: "bad1" + }, + + "number body": { + prefix: "bad2", + body: 100 + }, + + "many tabstops": { + prefix: "t11", + body: "$0one${1} ${2:two} three${3}" + }, + + "simple transform": { + prefix: "t12", + body: "[${1:b}][/${1/[ ]+.*$//}]" + }, + "transform with non-transforming mirrors": { + prefix: "t13", + body: "${1:placeholder}\n${1/(.)/\\u$1/g}\n$1" + }, + "multiple tab stops, some with transforms and some without": { + prefix: "t14", + body: "${1:placeholder} ${1/(.)/\\u$1/g} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2" + }, + "has a transformed tab stop without a corresponding ordinary tab stop": { + prefix: 't15', + body: "${1/(.)/\\u$1/g} & $2" + }, + "has a transformed tab stop that occurs before the corresponding ordinary tab stop": { + prefix: 't16', + body: "& ${1/(.)/\\u$1/g} & ${1:q}" + }, + "has a placeholder that mirrors another tab stop's content": { + prefix: 't17', + body: "$4console.${3:log}('${2:uh $1}', $1);$0" + }, + "has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step": { + prefix: 't18', + body: '// $1\n// ${1/./=/g}' + }, + "has two tab stops adjacent to one another": { + prefix: 't19', + body: '${2:bar}${3:baz}' + }, + "has several adjacent tab stops, one of which has a placeholder with reference to another tab stop at its edge": { + prefix: 't20', + body: '${1:foo}${2:bar}${3:baz $1}$4' + }, + "banner without global flag": { + prefix: "bannerWrong", + body: "// $1\n// ${1/./=/}" + }, + "banner with globalFlag": { + prefix: "bannerCorrect", + body: "// $1\n// ${1/./=/g}" + }, + "transform with simple flag on replacement (upcase)": { + prefix: 't_simple_upcase', + body: "$1 ${1/(.*)/${1:/upcase}/}" + }, + "transform with simple flag on replacement (downcase)": { + prefix: 't_simple_downcase', + body: "$1 ${1/(.*)/${1:/downcase}/}" + }, + "transform with simple flag on replacement (capitalize)": { + prefix: 't_simple_capitalize', + body: "$1 ${1/(.*)/${1:/capitalize}/}" + }, + "transform with simple flag on replacement (camelcase)": { + prefix: 't_simple_camelcase', + body: "$1 ${1/(.*)/${1:/camelcase}/}" + }, + "transform with simple flag on replacement (pascalcase)": { + prefix: 't_simple_pascalcase', + body: "$1 ${1/(.*)/${1:/pascalcase}/}" + }, + "transform with simple flag on replacement (snakecase)": { + prefix: 't_simple_snakecase', + body: "$1 ${1/(.*)/${1:/snakecase}/}" + }, + "transform with simple flag on replacement (kebabcase)": { + prefix: 't_simple_kebabcase', + body: "$1 ${1/(.*)/${1:/kebabcase}/}" + }, + "variable reference with simple flag on replacement (upcase)": { + prefix: 'v_simple_upcase', + body: "$CLIPBOARD ${CLIPBOARD/(\\S*)(.*)/${1}${2:/upcase}/}$0" + }, + "variable reference with simple flag on replacement (pascal)": { + prefix: 'v_simple_pascalcase', + body: "$CLIPBOARD ${CLIPBOARD/(\\S*)(.*)/${1} ${2:/pascalcase}/}$0" + }, + "variable reference with simple flag on replacement (snakecase)": { + prefix: 'v_simple_snakecase', + body: "$CLIPBOARD ${CLIPBOARD/(\\S*)(.*)/${1} ${2:/snakecase}/}$0" + }, + 'TM iftext but no elsetext': { + prefix: 'ifelse1', + body: '$1 ${1/(wat)/(?1:hey:)/}' + }, + 'TM elsetext but no iftext': { + prefix: 'ifelse2', + body: '$1 ${1/(?:(wat)|^.*$)$/(?1::hey)/}' + }, + 'TM both iftext and elsetext': { + prefix: 'ifelse3', + body: '$1 ${1/^\\w+\\s(?:(wat)|\\w*?)$/(?1:Y:N)/}' + }, + 'VS iftext but no elsetext': { + prefix: 'vsifelse1', + body: '$1 ${1/(?:(wat)|^.*?$)/${1:+WAT}/}' + }, + 'VS elsetext but no iftext': { + prefix: 'vsifelse2', + body: '$1 ${1/(?:(wat)|^.*?$)/${1:-nah}/}' + }, + 'VS elsetext but no iftext (alt)': { + prefix: 'vsifelse2a', + body: '$1 ${1/(?:(wat)|^.*?$)/${1:nah}/}' + }, + 'VS both iftext and elsetext': { + prefix: 'vsifelse3', + body: '$1 ${1/(?:(wat)|^.*?$)/${1:?WAT:nah}/}' + }, + 'choice syntax': { + prefix: 'choice', + body: '${1|one, two, three|}' + } + } + }); + + Snippets.add(__filename, { + ".source, .text": { + "banner with generic comment delimiters": { + prefix: "bannerGeneric", + body: "$LINE_COMMENT $1\n$LINE_COMMENT ${1/./=/g}" + } + } + }); + }); + + it("parses snippets once, reusing cached ones on subsequent queries", () => { + spyOn(Snippets, "getBodyParser").andCallThrough(); + + editor.insertText("t1"); + simulateTabKeyEvent(); + + expect(Snippets.getBodyParser).toHaveBeenCalled(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a testvar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 14]); + + Snippets.getBodyParser.reset(); + + editor.setText(""); + editor.insertText("t1"); + simulateTabKeyEvent(); + + expect(Snippets.getBodyParser).not.toHaveBeenCalled(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a test"); + expect(editor.getCursorScreenPosition()).toEqual([0, 14]); + + Snippets.getBodyParser.reset(); + + Snippets.add(__filename, { + ".source.js": { + "invalidate previous snippet": { + prefix: "t1", + body: "new snippet" + } + } + }); + + editor.setText(""); + editor.insertText("t1"); + simulateTabKeyEvent(); + + expect(Snippets.getBodyParser).toHaveBeenCalled(); + expect(editor.lineTextForBufferRow(0)).toBe("new snippet"); + expect(editor.getCursorScreenPosition()).toEqual([0, 11]); + }); + + describe("when the snippet body is invalid or missing", () => { + it("does not register the snippet", () => { + editor.setText(''); + editor.insertText('bad1'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.getText()).toBe('bad1'); + + editor.setText(''); + editor.setText('bad2'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.getText()).toBe('bad2'); + }); + }); + + describe("when the letters preceding the cursor trigger a snippet", () => { + describe("when the snippet contains no tab stops", () => { + it("replaces the prefix with the snippet text and places the cursor at its end", () => { + editor.insertText("t1"); + expect(editor.getCursorScreenPosition()).toEqual([0, 2]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a testvar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 14]); + }); + + it("inserts a real tab the next time a tab is pressed after the snippet is expanded", () => { + editor.insertText("t1"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a testvar quicksort = function () {"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a test var quicksort = function () {"); + }); + }); + + describe("when the snippet contains tab stops", () => { + it("places the cursor at the first tab-stop, and moves the cursor in response to 'next-tab-stop' events", () => { + const markerCountBefore = editor.getMarkerCount(); + editor.setCursorScreenPosition([2, 0]); + editor.insertText('t2'); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(2)).toBe("go here next:() and finally go here:()"); + expect(editor.lineTextForBufferRow(3)).toBe("go here first:()"); + expect(editor.lineTextForBufferRow(4)).toBe(" if (items.length <= 1) return items;"); + expect(editor.getSelectedBufferRange()).toEqual([[3, 15], [3, 15]]); + + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[2, 14], [2, 14]]); + editor.insertText('abc'); + + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[2, 40], [2, 40]]); + + // tab backwards + simulateTabKeyEvent({shift: true}); + expect(editor.getSelectedBufferRange()).toEqual([[2, 14], [2, 17]]); // should highlight text typed at tab stop + + simulateTabKeyEvent({shift: true}); + expect(editor.getSelectedBufferRange()).toEqual([[3, 15], [3, 15]]); + + // shift-tab on first tab-stop does nothing + simulateTabKeyEvent({shift: true}); + expect(editor.getCursorScreenPosition()).toEqual([3, 15]); + + // tab through all tab stops, then tab on last stop to terminate snippet + simulateTabKeyEvent(); + simulateTabKeyEvent(); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(2)).toBe("go here next:(abc) and finally go here:( )"); + expect(editor.getMarkerCount()).toBe(markerCountBefore); + }); + + describe("when tab stops are nested", () => { + it("destroys the inner tab stop if the outer tab stop is modified", () => { + editor.setText(''); + editor.insertText('t5'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.lineTextForBufferRow(0)).toBe('"key": value'); + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 5]]); + editor.insertText("foo"); + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 10]]); + }); + }); + + describe("when the only tab stop is an end stop", () => { + it("terminates the snippet immediately after moving the cursor to the end stop", () => { + editor.setText(''); + editor.insertText('t1a'); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("something strange"); + expect(editor.getCursorBufferPosition()).toEqual([0, 10]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("something strange"); + expect(editor.getCursorBufferPosition()).toEqual([0, 12]); + }); + }); + + describe("when tab stops are separated by blank lines", () => { + it("correctly places the tab stops (regression)", () => { + editor.setText(''); + editor.insertText('t7'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getCursorBufferPosition()).toEqual([3, 25]); + }); + }); + + describe("when the cursor is moved beyond the bounds of the current tab stop", () => { + it("terminates the snippet", () => { + editor.setCursorScreenPosition([2, 0]); + editor.insertText('t2'); + simulateTabKeyEvent(); + + editor.moveUp(); + editor.moveLeft(); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(2)).toBe("go here next:( ) and finally go here:()"); + expect(editor.getCursorBufferPosition()).toEqual([2, 16]); + + // test we can terminate with shift-tab + editor.setCursorScreenPosition([4, 0]); + editor.insertText('t2'); + simulateTabKeyEvent(); + simulateTabKeyEvent(); + + editor.moveRight(); + simulateTabKeyEvent({shift: true}); + expect(editor.getCursorBufferPosition()).toEqual([4, 15]); + }); + }); + + describe("when the cursor is moved within the bounds of the current tab stop", () => { + it("should not terminate the snippet", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t8'); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + editor.moveRight(); + editor.moveLeft(); + editor.insertText("foo"); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder tesfoot"); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + editor.insertText("test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder testvar quicksort = function () {"); + editor.moveLeft(); + editor.insertText("foo"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder tesfootvar quicksort = function () {"); + }); + }); + + describe("when the backspace is press within the bounds of the current tab stop", () => { + it("should not terminate the snippet", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t8'); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + editor.moveRight(); + editor.backspace(); + editor.insertText("foo"); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder tesfoo"); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + editor.insertText("test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder testvar quicksort = function () {"); + editor.backspace(); + editor.insertText("foo"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder tesfoovar quicksort = function () {"); + }); + }); + }); + + describe("when the snippet contains hard tabs", () => { + describe("when the edit session is in soft-tabs mode", () => { + it("translates hard tabs in the snippet to the appropriate number of spaces", () => { + expect(editor.getSoftTabs()).toBeTruthy(); + editor.insertText("t3"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(1)).toBe(" line 2"); + expect(editor.getCursorBufferPosition()).toEqual([1, 8]); + }); + }); + + describe("when the edit session is in hard-tabs mode", () => { + it("inserts hard tabs in the snippet directly", () => { + editor.setSoftTabs(false); + editor.insertText("t3"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(1)).toBe("\tline 2"); + expect(editor.getCursorBufferPosition()).toEqual([1, 7]); + }); + }); + }); + + describe("when the snippet prefix is indented", () => { + describe("when the snippet spans a single line", () => { + it("does not indent the next line", () => { + editor.setCursorScreenPosition([2, Infinity]); + editor.insertText(' t1'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.lineTextForBufferRow(3)).toBe(" var pivot = items.shift(), current, left = [], right = [];"); + }); + }); + + describe("when the snippet spans multiple lines", () => { + it("indents the subsequent lines of the snippet to be even with the start of the first line", () => { + expect(editor.getSoftTabs()).toBeTruthy(); + editor.setCursorScreenPosition([2, Infinity]); + editor.insertText(' t3'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.lineTextForBufferRow(2)).toBe(" if (items.length <= 1) return items; line 1"); + expect(editor.lineTextForBufferRow(3)).toBe(" line 2"); + expect(editor.getCursorBufferPosition()).toEqual([3, 12]); + }); + }); + }); + + describe("when the snippet spans multiple lines", () => { + beforeEach(async () => { + editor.update({autoIndent: true}); + // editor.update() returns a Promise that never gets resolved, so we + // need to return undefined to avoid a timeout in the spec. + // TODO: Figure out why `editor.update({autoIndent: true})` never gets resolved. + }); + + it("places tab stops correctly", () => { + expect(editor.getSoftTabs()).toBeTruthy(); + editor.setCursorScreenPosition([2, Infinity]); + editor.insertText(' t3'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.getCursorBufferPosition()).toEqual([3, 12]); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getCursorBufferPosition()).toEqual([4, 4]); + }); + + it("indents the subsequent lines of the snippet based on the indent level before the snippet is inserted", async () => { + editor.setCursorScreenPosition([2, Infinity]); + editor.insertNewline(); + await languageMode.atTransactionEnd(); + editor.insertText('t4b'); + await languageMode.atTransactionEnd(); + atom.commands.dispatch(editorElement, 'snippets:expand'); + + expect(editor.lineTextForBufferRow(3)).toBe(" = line 1 {"); // 4 + 1 spaces (because the tab stop is invisible) + expect(editor.lineTextForBufferRow(4)).toBe(" line 2"); + expect(editor.lineTextForBufferRow(5)).toBe(" }"); + expect(editor.getCursorBufferPosition()).toEqual([3, 4]); + }); + + it("does not change the relative positioning of the tab stops when inserted multiple times", async () => { + editor.setCursorScreenPosition([2, Infinity]); + editor.insertNewline(); + await languageMode.atTransactionEnd(); + editor.insertText('t4'); + await languageMode.atTransactionEnd(); + atom.commands.dispatch(editorElement, 'snippets:expand'); + + expect(editor.getSelectedBufferRange()).toEqual([[3, 9], [3, 10]]); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getSelectedBufferRange()).toEqual([[4, 6], [4, 13]]); + + editor.insertText('t4'); + await languageMode.atTransactionEnd(); + atom.commands.dispatch(editorElement, 'snippets:expand'); + + expect(editor.getSelectedBufferRange()).toEqual([[4, 11], [4, 12]]); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getSelectedBufferRange()).toEqual([[5, 8], [5, 15]]); + + editor.setText(''); // Clear editor + await languageMode.atTransactionEnd(); + editor.insertText('t4'); + await languageMode.atTransactionEnd(); + atom.commands.dispatch(editorElement, 'snippets:expand'); + + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 6]]); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getSelectedBufferRange()).toEqual([[1, 2], [1, 9]]); + }); + }); + + describe("when multiple snippets match the prefix", () => { + it("expands the snippet that is the longest match for the prefix", async () => { + editor.insertText('t113'); + await languageMode.atTransactionEnd(); + expect(editor.getCursorScreenPosition()).toEqual([0, 4]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("t113 var quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 6]); + + editor.undo(); + editor.undo(); + + editor.insertText("tt1"); + await languageMode.atTransactionEnd(); + expect(editor.getCursorScreenPosition()).toEqual([0, 3]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("this is another testvar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 20]); + + editor.undo(); + editor.undo(); + await languageMode.atTransactionEnd(); + + editor.insertText("@t1"); + await languageMode.atTransactionEnd(); + expect(editor.getCursorScreenPosition()).toEqual([0, 3]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("@this is a testvar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 15]); + }); + }); + }); + + describe("when the word preceding the cursor ends with a snippet prefix", () => { + it("inserts a tab as normal", () => { + editor.insertText("t1t1t1"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("t1t1t1 var quicksort = function () {"); + }); + }); + + describe("when the letters preceding the cursor don't match a snippet", () => { + it("inserts a tab as normal", () => { + editor.insertText("xxte"); + expect(editor.getCursorScreenPosition()).toEqual([0, 4]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("xxte var quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 6]); + }); + }); + + describe("when text is selected", () => { + it("inserts a tab as normal", () => { + editor.insertText("t1"); + editor.setSelectedBufferRange([[0, 0], [0, 2]]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe(" t1var quicksort = function () {"); + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 4]]); + }); + }); + + describe("when a previous snippet expansion has just been undone", () => { + describe("when the tab stops appear in the middle of the snippet", () => { + it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => { + editor.insertText('t6\n'); + editor.setCursorBufferPosition([0, 2]); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("first line"); + editor.undo(); + expect(editor.lineTextForBufferRow(0)).toBe("t6"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("first line"); + }); + }); + + describe("when the tab stops appear at the beginning and then the end of snippet", () => { + it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => { + editor.insertText('t6b\n'); + editor.setCursorBufferPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("expanded"); + editor.undo(); + expect(editor.lineTextForBufferRow(0)).toBe("t6b"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("expanded"); + expect(editor.getCursorBufferPosition()).toEqual([0, 0]); + }); + }); + + describe("when the tab stops appear at the end and then the beginning of snippet", () => { + it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => { + editor.insertText('t6c\n'); + editor.setCursorBufferPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("expanded"); + editor.undo(); + expect(editor.lineTextForBufferRow(0)).toBe("t6c"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("expanded"); + expect(editor.getCursorBufferPosition()).toEqual([0, 8]); + }); + }); + }); + + describe("when the prefix contains non-word characters", () => { + it("selects the non-word characters as part of the prefix", () => { + editor.insertText("@unique"); + expect(editor.getCursorScreenPosition()).toEqual([0, 7]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("@unique seevar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 11]); + + editor.setCursorBufferPosition([10, 0]); + editor.insertText("'@unique"); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(10)).toBe("'@unique see"); + expect(editor.getCursorScreenPosition()).toEqual([10, 12]); + }); + + it("does not select the whitespace before the prefix", () => { + editor.insertText("a; @unique"); + expect(editor.getCursorScreenPosition()).toEqual([0, 10]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("a; @unique seevar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 14]); + }); + }); + + describe("when snippet contains tabstops with or without placeholder", () => { + it("should create two markers", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t8'); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + + expect(editor.getSelectedBufferRange()).toEqual([[0, 17], [0, 21]]); + + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[1, 20], [1, 20]]); + }); + }); + + describe("when snippet contains multi-caret tabstops with or without placeholder", () => { + it("should create two markers", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t9'); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + editor.insertText('hello'); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder hello"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder hellovar quicksort = function () {"); + }); + + it("terminates the snippet when cursors are destroyed", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t9b'); + simulateTabKeyEvent(); + editor.getCursors()[0].destroy(); + editor.getCursorBufferPosition(); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(1)).toEqual("without placeholder "); + }); + + it("terminates the snippet expansion if a new cursor moves outside the bounds of the tab stops", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t9b'); + simulateTabKeyEvent(); + editor.insertText('test'); + + editor.getCursors()[0].destroy(); + editor.moveDown(); // this should destroy the previous expansion + editor.moveToBeginningOfLine(); + + // this should insert whitespace instead of going through tabstops of the previous destroyed snippet + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(2).indexOf(" second")).toBe(0); + }); + + it("moves to the second tabstop after a multi-caret tabstop", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t9b'); + simulateTabKeyEvent(); + editor.insertText('line 1'); + + simulateTabKeyEvent(); + editor.insertText('line 2'); + + simulateTabKeyEvent(); + editor.insertText('line 3'); + + expect(editor.lineTextForBufferRow(2).indexOf("line 2 ")).toBe(-1); + }); + + it("mirrors input properly when a tabstop's placeholder refers to another tabstop", () => { + editor.setText('t17'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + editor.insertText("foo"); + expect(editor.getText()).toBe("console.log('uh foo', foo);"); + simulateTabKeyEvent(); + editor.insertText("bar"); + expect(editor.getText()).toBe("console.log('bar', foo);"); + }); + }); + + describe("when the snippet contains tab stops with transformations", () => { + it("transforms the text typed into the first tab stop before setting it in the transformed tab stop", async () => { + editor.setText('t12'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("[b][/b]"); + await languageMode.atTransactionEnd(); + editor.insertText('img src'); + expect(editor.getText()).toBe("[img src][/img]"); + }); + + it("bundles the transform mutations along with the original manual mutation for the purposes of undo and redo", async () => { + editor.setText('t12'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + editor.insertText('i'); + expect(editor.getText()).toBe("[i][/i]"); + + editor.insertText('mg src'); + expect(editor.getText()).toBe("[img src][/img]"); + + editor.undo(); + expect(editor.getText()).toBe("[i][/i]"); + + editor.redo(); + expect(editor.getText()).toBe("[img src][/img]"); + }); + + it("can pick the right insertion to use as the primary even if a transformed insertion occurs first in the snippet", () => { + editor.setText('t16'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("& Q & q"); + expect(editor.getCursorBufferPosition()).toEqual([0, 7]); + + editor.insertText('rst'); + expect(editor.lineTextForBufferRow(0)).toBe("& RST & rst"); + }); + + it("silently ignores a tab stop without a non-transformed insertion to use as the primary", () => { + editor.setText('t15'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + editor.insertText('a'); + expect(editor.lineTextForBufferRow(0)).toBe(" & a"); + expect(editor.getCursorBufferPosition()).toEqual([0, 4]); + }); + }); + + describe("when the snippet contains mirrored tab stops and tab stops with transformations", () => { + it("adds cursors for the mirrors but not the transformations", () => { + editor.setText('t13'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getCursors().length).toBe(2); + expect(editor.getText()).toBe(`\ +placeholder +PLACEHOLDER +\ +` + ); + + editor.insertText('foo'); + + expect(editor.getText()).toBe(`\ +foo +FOO +foo\ +` + ); + }); + }); + + describe("when the snippet contains a transformation without a global flag", () => { + it("should transform only the first character", () => { + editor.setText('bannerWrong'); + editor.setCursorScreenPosition([0, 11]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("// \n// "); + editor.insertText('TEST'); + expect(editor.getText()).toBe("// TEST\n// =EST"); + }); + }); + + describe("when the snippet contains a transformation with a global flag", () => { + it("should transform all characters", () => { + editor.setText('bannerCorrect'); + editor.setCursorScreenPosition([0, 13]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("// \n// "); + editor.insertText('TEST'); + expect(editor.getText()).toBe("// TEST\n// ===="); + }); + }); + + describe("when the snippet contains generic line comment delimiter variables", () => { + describe("and the document is JavaScript", () => { + it("uses the right delimiters", () => { + editor.setText('bannerGeneric'); + editor.setCursorScreenPosition([0, 13]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("// \n// "); + editor.insertText('TEST'); + expect(editor.getText()).toBe("// TEST\n// ===="); + }); + }); + + describe("and the document is HTML", () => { + beforeEach(() => { + atom.grammars.assignLanguageMode(editor, 'text.html.basic'); + editor.setText(''); + }); + + it("falls back to an empty string, for HTML has no line comment", () => { + editor.setText('bannerGeneric'); + editor.setCursorScreenPosition([0, 13]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe(" \n "); + editor.insertText('TEST'); + expect(editor.getText()).toBe(" TEST\n ===="); + }); + }); + + describe("and the document is Python", () => { + beforeEach(() => { + atom.grammars.assignLanguageMode(editor, 'source.python'); + editor.setText(''); + }); + it("uses the right delimiters", () => { + editor.setText('bannerGeneric'); + editor.setCursorScreenPosition([0, 13]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("# \n# "); + editor.insertText('TEST'); + expect(editor.getText()).toBe("# TEST\n# ===="); + }); + }); + }); + + describe("when the snippet contains a transformation with a simple transform flag on a substitution", () => { + let expectations = { + upcase: `LOREM IPSUM DOLOR`, + downcase: `lorem ipsum dolor`, + capitalize: `Lorem Ipsum Dolor`, + camelcase: 'loremIpsumDolor', + pascalcase: 'LoremIpsumDolor', + snakecase: 'lorem_ipsum_dolor', + kebabcase: 'lorem-ipsum-dolor' + }; + for (let [flag, expected] of Object.entries(expectations)) { + it(`should transform ${flag} correctly`, () => { + let trigger = `t_simple_${flag}`; + editor.setText(trigger); + editor.setCursorScreenPosition([0, trigger.length]); + simulateTabKeyEvent(); + editor.insertText('lorem Ipsum Dolor'); + expect(editor.getText()).toBe(`lorem Ipsum Dolor ${expected}`); + }); + } + }); + + describe("when the snippet contains a variable with a simple transform flag within a sed-style substitution", () => { + let expectations = { + upcase: 'lorem IPSUM DOLOR', + pascalcase: 'lorem IpsumDolor', + snakecase: 'lorem ipsum_dolor', + }; + for (let [flag, expected] of Object.entries(expectations)) { + it(`should transform ${flag} correctly`, () => { + atom.clipboard.write('lorem Ipsum Dolor'); + let trigger = `v_simple_${flag}`; + console.log('expanding:', trigger); + editor.setText(trigger); + editor.setCursorScreenPosition([0, trigger.length]); + simulateTabKeyEvent(); + console.log('TEXT:', editor.getText()); + expect(editor.getText()).toBe(`lorem Ipsum Dolor ${expected}`); + }); + } + }); + + describe("when the snippet contains multiple tab stops, some with transformations and some without", () => { + it("does not get confused", () => { + editor.setText('t14'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getCursors().length).toBe(2); + expect(editor.getText()).toBe("placeholder PLACEHOLDER ANOTHER another "); + simulateTabKeyEvent(); + expect(editor.getCursors().length).toBe(2); + editor.insertText('FOO'); + expect(editor.getText()).toBe("placeholder PLACEHOLDER FOO foo FOO"); + }); + }); + + describe("when the snippet contains a tab stop with choices", () => { + it("uses the first option as the placeholder", () => { + editor.setText(''); + editor.insertText('choice'); + simulateTabKeyEvent(); + + expect(editor.getText()).toBe('one'); + }); + }); + + describe("when the snippet contains VSCode-style if-else syntax", () => { + + it('understands if but no else', () => { + editor.setText(''); + editor.insertText('vsifelse1'); + simulateTabKeyEvent(); + + editor.insertText('wat'); + expect(editor.getText()).toEqual('wat WAT'); + simulateTabKeyEvent(); + + editor.setText(''); + editor.insertText('vsifelse1'); + simulateTabKeyEvent(); + + editor.insertText('foo'); + expect(editor.getText()).toEqual('foo '); + }); + + it('understands else but no if', () => { + editor.setText(''); + editor.insertText('vsifelse2'); + simulateTabKeyEvent(); + + editor.insertText('wat'); + expect(editor.getText()).toEqual('wat '); + simulateTabKeyEvent(); + + editor.setText(''); + editor.insertText('vsifelse2'); + simulateTabKeyEvent(); + + editor.insertText('foo'); + expect(editor.getText()).toEqual('foo nah'); + simulateTabKeyEvent(); + + // There are two syntaxes for this. + editor.setText(''); + editor.insertText('vsifelse2a'); + simulateTabKeyEvent(); + + editor.insertText('wat'); + expect(editor.getText()).toEqual('wat '); + simulateTabKeyEvent(); + + editor.setText(''); + editor.insertText('vsifelse2a'); + simulateTabKeyEvent(); + + editor.insertText('foo'); + expect(editor.getText()).toEqual('foo nah'); + }); + + it('understands both if and else', () => { + editor.setText(''); + editor.insertText('vsifelse3'); + simulateTabKeyEvent(); + + editor.insertText('wat'); + expect(editor.getText()).toEqual('wat WAT'); + simulateTabKeyEvent(); + + editor.setText(''); + editor.insertText('vsifelse3'); + simulateTabKeyEvent(); + + editor.insertText('foo'); + expect(editor.getText()).toEqual('foo nah'); + }); + }); + + describe("when the snippet contains TextMate-style if-else syntax", () => { + + it('understands if but no else', () => { + editor.setText(''); + editor.insertText('ifelse1'); + simulateTabKeyEvent(); + + editor.insertText('wat'); + expect(editor.getText()).toEqual('wat hey'); + simulateTabKeyEvent(); + + editor.setText(''); + editor.insertText('ifelse1'); + simulateTabKeyEvent(); + + editor.insertText('foo'); + expect(editor.getText()).toEqual('foo foo'); + }); + + it('understands else but no if', () => { + editor.setText(''); + editor.insertText('ifelse2'); + simulateTabKeyEvent(); + + editor.insertText('wat'); + expect(editor.getText()).toEqual('wat '); + simulateTabKeyEvent(); + + editor.setText(''); + editor.insertText('ifelse2'); + simulateTabKeyEvent(); + + editor.insertText('foo'); + expect(editor.getText()).toEqual('foo hey'); + }); + + it('understands both if and else', () => { + editor.setText(''); + editor.insertText('ifelse3'); + simulateTabKeyEvent(); + + editor.insertText('something wat'); + expect(editor.getText()).toEqual('something wat Y'); + simulateTabKeyEvent(); + + editor.setText(''); + editor.insertText('ifelse3'); + simulateTabKeyEvent(); + + editor.insertText('something foo'); + expect(editor.getText()).toEqual('something foo N'); + }); + }); + + describe("when the snippet has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step", () => { + it("terminates the snippet upon such a cursor move", () => { + editor.setText('t18'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("// \n// "); + expect(editor.getCursorBufferPosition()).toEqual([0, 3]); + editor.insertText('wat'); + expect(editor.getText()).toBe("// wat\n// ==="); + // Move the cursor down one line, then up one line. This puts the cursor + // back in its previous position, but the snippet should no longer be + // active, so when we type more text, it should not be mirrored. + editor.setCursorScreenPosition([1, 6]); + editor.setCursorScreenPosition([0, 6]); + editor.insertText('wat'); + expect(editor.getText()).toBe("// watwat\n// ==="); + }); + }); + + describe("when the snippet has two adjacent tab stops", () => { + it("ensures insertions are treated as part of the active tab stop", () => { + editor.setText('t19'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe('barbaz'); + expect( + editor.getSelectedBufferRange() + ).toEqual([ + [0, 0], + [0, 3] + ]); + editor.insertText('w'); + expect(editor.getText()).toBe('wbaz'); + editor.insertText('at'); + expect(editor.getText()).toBe('watbaz'); + simulateTabKeyEvent(); + expect( + editor.getSelectedBufferRange() + ).toEqual([ + [0, 3], + [0, 6] + ]); + editor.insertText('foo'); + expect(editor.getText()).toBe('watfoo'); + }); + }); + + describe("when the snippet has a placeholder with a tabstop mirror at its edge", () => { + it("allows the associated marker to include the inserted text", () => { + editor.setText('t20'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe('foobarbaz '); + expect(editor.getCursors().length).toBe(2); + let selections = editor.getSelections(); + expect(selections[0].getBufferRange()).toEqual([[0, 0], [0, 3]]); + expect(selections[1].getBufferRange()).toEqual([[0, 10], [0, 10]]); + editor.insertText('nah'); + expect(editor.getText()).toBe('nahbarbaz nah'); + simulateTabKeyEvent(); + editor.insertText('meh'); + simulateTabKeyEvent(); + editor.insertText('yea'); + expect(editor.getText()).toBe('nahmehyea'); + }); + }); + + describe("when the snippet contains tab stops with an index >= 10", () => { + it("parses and orders the indices correctly", () => { + editor.setText('t10'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("hello large indices"); + expect(editor.getCursorBufferPosition()).toEqual([0, 19]); + simulateTabKeyEvent(); + expect(editor.getCursorBufferPosition()).toEqual([0, 5]); + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[0, 6], [0, 11]]); + }); + }); + + describe("when there are multiple cursors", () => { + describe("when the cursors share a common snippet prefix", () => { + it("expands the snippet for all cursors and allows simultaneous editing", () => { + editor.insertText('t9'); + editor.setCursorBufferPosition([12, 2]); + editor.insertText(' t9'); + editor.addCursorAtBufferPosition([0, 2]); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + expect(editor.lineTextForBufferRow(13)).toBe("}; with placeholder test"); + expect(editor.lineTextForBufferRow(14)).toBe("without placeholder "); + + editor.insertText('hello'); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder hello"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder hellovar quicksort = function () {"); + expect(editor.lineTextForBufferRow(13)).toBe("}; with placeholder hello"); + expect(editor.lineTextForBufferRow(14)).toBe("without placeholder hello"); + }); + + it("applies transformations identically to single-expansion mode", () => { + editor.setText('t14\nt14'); + editor.setCursorBufferPosition([1, 3]); + editor.addCursorAtBufferPosition([0, 3]); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("placeholder PLACEHOLDER ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("placeholder PLACEHOLDER ANOTHER another "); + + editor.insertText("testing"); + + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another "); + + simulateTabKeyEvent(); + editor.insertText("AGAIN"); + + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing AGAIN again AGAIN"); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing AGAIN again AGAIN"); + }); + + it("bundles transform-induced mutations into a single history entry along with their triggering edit, even across multiple snippets", () => { + editor.setText('t14\nt14'); + editor.setCursorBufferPosition([1, 3]); + editor.addCursorAtBufferPosition([0, 3]); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("placeholder PLACEHOLDER ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("placeholder PLACEHOLDER ANOTHER another "); + + editor.insertText("testing"); + + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another "); + + simulateTabKeyEvent(); + editor.insertText("AGAIN"); + + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing AGAIN again AGAIN"); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing AGAIN again AGAIN"); + + editor.undo(); + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another "); + + editor.undo(); + expect(editor.lineTextForBufferRow(0)).toBe("placeholder PLACEHOLDER ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("placeholder PLACEHOLDER ANOTHER another "); + + editor.redo(); + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another "); + + editor.redo(); + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing AGAIN again AGAIN"); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing AGAIN again AGAIN"); + }); + + describe("when there are many tabstops", () => { + it("moves the cursors between the tab stops for their corresponding snippet when tab and shift-tab are pressed", () => { + editor.addCursorAtBufferPosition([7, 5]); + editor.addCursorAtBufferPosition([12, 2]); + editor.insertText('t11'); + simulateTabKeyEvent(); + + const cursors = editor.getCursors(); + expect(cursors.length).toEqual(3); + + expect(cursors[0].getBufferPosition()).toEqual([0, 3]); + expect(cursors[1].getBufferPosition()).toEqual([7, 8]); + expect(cursors[2].getBufferPosition()).toEqual([12, 5]); + expect(cursors[0].selection.isEmpty()).toBe(true); + expect(cursors[1].selection.isEmpty()).toBe(true); + expect(cursors[2].selection.isEmpty()).toBe(true); + + simulateTabKeyEvent(); + expect(cursors[0].getBufferPosition()).toEqual([0, 7]); + expect(cursors[1].getBufferPosition()).toEqual([7, 12]); + expect(cursors[2].getBufferPosition()).toEqual([12, 9]); + expect(cursors[0].selection.isEmpty()).toBe(false); + expect(cursors[1].selection.isEmpty()).toBe(false); + expect(cursors[2].selection.isEmpty()).toBe(false); + expect(cursors[0].selection.getText()).toEqual('two'); + expect(cursors[1].selection.getText()).toEqual('two'); + expect(cursors[2].selection.getText()).toEqual('two'); + + simulateTabKeyEvent(); + expect(cursors[0].getBufferPosition()).toEqual([0, 13]); + expect(cursors[1].getBufferPosition()).toEqual([7, 18]); + expect(cursors[2].getBufferPosition()).toEqual([12, 15]); + expect(cursors[0].selection.isEmpty()).toBe(true); + expect(cursors[1].selection.isEmpty()).toBe(true); + expect(cursors[2].selection.isEmpty()).toBe(true); + + simulateTabKeyEvent(); + expect(cursors[0].getBufferPosition()).toEqual([0, 0]); + expect(cursors[1].getBufferPosition()).toEqual([7, 5]); + expect(cursors[2].getBufferPosition()).toEqual([12, 2]); + expect(cursors[0].selection.isEmpty()).toBe(true); + expect(cursors[1].selection.isEmpty()).toBe(true); + expect(cursors[2].selection.isEmpty()).toBe(true); + }); + }); + }); + + describe("when the cursors do not share common snippet prefixes", () => { + it("inserts tabs as normal", () => { + editor.insertText('t9'); + editor.setCursorBufferPosition([12, 2]); + editor.insertText(' t8'); + editor.addCursorAtBufferPosition([0, 2]); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("t9 var quicksort = function () {"); + expect(editor.lineTextForBufferRow(12)).toBe("}; t8 "); + }); + }); + + describe("when a snippet is triggered within an existing snippet expansion", () => { + it("ignores the snippet expansion and goes to the next tab stop", () => { + editor.addCursorAtBufferPosition([7, 5]); + editor.addCursorAtBufferPosition([12, 2]); + editor.insertText('t11'); + simulateTabKeyEvent(); + simulateTabKeyEvent(); + + editor.insertText('t1'); + simulateTabKeyEvent(); + + const cursors = editor.getCursors(); + expect(cursors.length).toEqual(3); + + expect(cursors[0].getBufferPosition()).toEqual([0, 12]); + expect(cursors[1].getBufferPosition()).toEqual([7, 17]); + expect(cursors[2].getBufferPosition()).toEqual([12, 14]); + expect(cursors[0].selection.isEmpty()).toBe(true); + expect(cursors[1].selection.isEmpty()).toBe(true); + expect(cursors[2].selection.isEmpty()).toBe(true); + expect(editor.lineTextForBufferRow(0)).toBe("one t1 threevar quicksort = function () {"); + expect(editor.lineTextForBufferRow(7)).toBe(" }one t1 three"); + expect(editor.lineTextForBufferRow(12)).toBe("};one t1 three"); + }); + }); + }); + + describe("when the editor is not a pane item (regression)", () => { + it("handles tab stops correctly", async () => { + editor = new TextEditor(); + atom.grammars.assignLanguageMode(editor, 'source.js'); + let languageMode = editor.getBuffer().getLanguageMode(); + editorElement = editor.getElement(); + await languageMode.ready; + + editor.insertText('t2'); + await languageMode.atTransactionEnd(); + simulateTabKeyEvent(); + editor.insertText('ABC'); + await languageMode.atTransactionEnd(); + expect(editor.getText()).toContain('go here first:(ABC)'); + + editor.undo(); + editor.undo(); + await languageMode.atTransactionEnd(); + expect(editor.getText()).toBe('t2'); + simulateTabKeyEvent(); + editor.insertText('ABC'); + expect(editor.getText()).toContain('go here first:(ABC)'); + }); + }); + }); + + describe("when atom://.pulsar/snippets is opened", () => { + it("opens ~/.pulsar/snippets.cson", () => { + jasmine.unspy(Snippets, 'getUserSnippetsPath'); + atom.workspace.destroyActivePaneItem(); + const configDirPath = temp.mkdirSync('atom-config-dir-'); + spyOn(atom, 'getConfigDirPath').andReturn(configDirPath); + atom.workspace.open('atom://.pulsar/snippets'); + + waitsFor(() => atom.workspace.getActiveTextEditor() != null); + + runs(() => { + expect(atom.workspace.getActiveTextEditor().getURI()).toBe(path.join(configDirPath, 'snippets.cson')); + }); + }); + }); + + describe("snippet insertion API", () => { + it("will automatically parse snippet definition and replace selection", () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]); + Snippets.insert("hello ${1:world}", editor); + + expect(editor.lineTextForBufferRow(0)).toBe("var hello world = function () {"); + expect(editor.getSelectedBufferRange()).toEqual([[0, 10], [0, 15]]); + }); + }); + + describe("when a user snippet maps to a command", () => { + beforeEach(() => { + editor.setText(''); + Snippets.add( + __filename, { + ".source.js": { + "some command snippet": { + body: "lorem ipsum dolor $1 sit ${2:amet}$0", + command: "some-command-snippet" + }, + "another command snippet with a prefix": { + prefix: 'prfx', + command: 'command-with-prefix', + body: 'this had $0 a prefix' + }, + "another snippet with neither command nor prefix": { + body: 'useless' + }, + "another snippet with a malformed command name": { + command: 'i flout the RULES', + body: 'inconsiderate' + } + }, + ".source.python": { + "some python command snippet": { + body: "consecuetur $0 adipiscing", + command: "some-python-command-snippet" + } + }, + ".source, .text": { + "wrap in block comment": { + body: "$BLOCK_COMMENT_START $TM_SELECTED_TEXT ${BLOCK_COMMENT_END}${0}", + command: 'wrap-in-block-comment' + } + }, + ".text.html": { + "wrap in tag": { + "command": "wrap-in-html-tag", + "body": "<${1:div}>$0" + } + } + }, + 'snippets' + ); + }); + + afterEach(() => { + Snippets.clearSnippetsForPath(__filename); + }); + + it("registers the command", () => { + expect( + "snippets:some-command-snippet" in atom.commands.registeredCommands + ).toBe(true); + }); + + it("complains about a malformed command name", () => { + const expectedMessage = `Cannot register \`i flout the RULES\` for snippet “another snippet with a malformed command name” because the command name isn’t valid. Command names must be all lowercase and use hyphens between words instead of spaces.`; + expect(atom.notifications.addError).toHaveBeenCalledWith( + `Snippets error`, + { + description: expectedMessage, + dismissable: true + } + ); + }); + + describe("and the command is invoked", () => { + beforeEach(() => { + editor.setText(''); + }); + + it("expands the snippet when the scope matches", () => { + atom.commands.dispatch(editor.element, 'snippets:some-command-snippet'); + let cursor = editor.getLastCursor(); + let pos = cursor.getBufferPosition(); + expect(cursor.getBufferPosition()).toEqual([0, 18]); + + expect(editor.getText()).toBe('lorem ipsum dolor sit amet'); + editor.insertText("virus"); + expect(editor.getText()).toBe('lorem ipsum dolor virus sit amet'); + + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[0, 28], [0, 32]]); + }); + + it("expands the snippet even when a prefix is defined", () => { + atom.commands.dispatch(editor.element, 'snippets:command-with-prefix'); + let cursor = editor.getLastCursor(); + let pos = cursor.getBufferPosition(); + expect(pos.toArray().join(',')).toBe('0,9'); + expect(editor.getText()).toBe('this had a prefix'); + }); + + it("does nothing when the scope does not match", () => { + atom.commands.dispatch(editor.element, 'snippets:some-python-command-snippet'); + expect(editor.getText()).toBe(""); + }); + + it("uses language-specific comment delimiters", () => { + editor.setText("something"); + editor.selectAll(); + atom.commands.dispatch(editor.element, 'snippets:wrap-in-block-comment'); + expect(editor.getText()).toBe("/* something */"); + }); + + }); + + describe("and the command is invoked in an HTML document", () => { + beforeEach(() => { + atom.grammars.assignLanguageMode(editor, 'text.html.basic'); + editor.setText(''); + }); + + it("expands tab stops correctly", () => { + atom.commands.dispatch(editor.element, 'snippets:wrap-in-html-tag'); + let cursor = editor.getLastCursor(); + expect(cursor.getBufferPosition()).toEqual([0, 4]); + expect(editor.getSelectedText()).toEqual('div'); + + editor.insertText("aside class=\"wat\""); + + expect(editor.getText()).toBe(""); + + simulateTabKeyEvent(); + expect(cursor.getBufferPosition()).toEqual([0, 19]); + }); + + it("uses language-specific comment delimiters", () => { + editor.setText("something"); + editor.selectAll(); + atom.commands.dispatch(editor.element, 'snippets:wrap-in-block-comment'); + expect(editor.getText()).toBe(""); + }); + + }); + + describe("and the command is invoked in a Python document", () => { + beforeEach(() => { + atom.grammars.assignLanguageMode(editor, 'source.python'); + editor.setText(''); + }); + + it("uses language-specific comment delimiters, or empty strings if those delimiters don't exist in Python", () => { + editor.setText("something"); + editor.selectAll(); + atom.commands.dispatch(editor.element, 'snippets:wrap-in-block-comment'); + expect(editor.getText()).toBe(" something "); + }); + + }); + }); + + describe("when a snippet contains variables", () => { + + beforeEach(() => { + atom.grammars.assignLanguageMode(editor, 'source.js'); + Snippets.add( + __filename, { + ".source.js": { + "Uses TM_SELECTED_TEXT": { + body: 'lorem ipsum $TM_SELECTED_TEXT dolor sit amet', + command: 'test-command-tm-selected-text', + prefix: 'tmSelectedText' + }, + "Uses CLIPBOARD": { + body: 'lorem ipsum $CLIPBOARD dolor sit amet', + command: 'test-command-clipboard' + }, + "Transforms CLIPBOARD removing digits": { + body: 'lorem ipsum ${CLIPBOARD/\\d//g} dolor sit amet', + command: 'test-command-clipboard-transformed' + }, + "Transforms CLIPBOARD with casing flags": { + body: 'lorem ipsum ${CLIPBOARD:/upcase} dolor sit amet\n${CLIPBOARD:/downcase}\n${CLIPBOARD:/camelcase}\n${CLIPBOARD:/pascalcase}\n${CLIPBOARD:/capitalize}', + command: 'test-command-clipboard-upcased' + }, + "Transforms day, month, year": { + body: 'Today is $CURRENT_MONTH $CURRENT_DATE, $CURRENT_YEAR', + command: 'test-command-date' + }, + "Transforms line numbers": { + prefix: 'ln', + body: 'line is $TM_LINE_NUMBER and index is $TM_LINE_INDEX' + }, + "Transforms workspace name": { + prefix: 'wn', + body: 'the name of this project is $WORKSPACE_NAME' + }, + "Gives random value": { + prefix: 'rndm', + body: 'random number is:\n$RANDOM' + }, + "Gives random hex vallue": { + prefix: 'rndmhex', + body: 'random hex is:\n$RANDOM_HEX' + }, + "Gives random UUID": { + prefix: 'rndmuuid', + body: 'random UUID is:\n$UUID' + }, + "Gives file paths": { + prefix: 'fpath', + body: 'file paths:\n$TM_FILEPATH\n$TM_FILENAME\n$TM_FILENAME_BASE' + }, + }, + ".text.html": { + "wrap in tag": { + "command": "wrap-in-html-tag", + "body": "<${1:div}>${2:$TM_SELECTED_TEXT}$0" + } + } + }, + 'test-package' + ); + + editor.setText(''); + }); + + it("interpolates the variables into the snippet expansion", () => { + editor.insertText('(selected text)'); + editor.selectToBeginningOfLine(); + + expect(editor.getSelectedText()).toBe('(selected text)'); + atom.commands.dispatch(editor.element, 'test-package:test-command-tm-selected-text'); + expect(editor.getText()).toBe('lorem ipsum (selected text) dolor sit amet'); + }); + + it("does not consider the tab trigger to be part of $TM_SELECTED_TEXT when a snippet is invoked via tab trigger", () => { + editor.insertText('tmSelectedText'); + simulateTabKeyEvent(); + + expect(editor.getText()).toBe('lorem ipsum dolor sit amet'); + }); + + it("interpolates line number variables correctly", () => { + editor.insertText('ln'); + simulateTabKeyEvent(); + expect(editor.getText()).toBe('line is 1 and index is 0'); + editor.setText(''); + editor.insertText("\n\n\nln"); + simulateTabKeyEvent(); + let cursor = editor.getLastCursor(); + let lineText = editor.lineTextForBufferRow(cursor.getBufferRow()); + expect(lineText).toBe('line is 4 and index is 3'); + }); + + it("interpolates WORKSPACE_NAME correctly", () => { + editor.insertText('wn'); + simulateTabKeyEvent(); + expect(editor.getText()).toBe('the name of this project is fixtures'); + }); + + it("interpolates date variables correctly", () => { + function pad (val) { + let str = String(val); + return str.length === 1 ? `0${str}` : str; + } + let now = new Date(); + let month = pad(now.getMonth() + 1); + let day = pad(now.getDate()); + let year = now.getFullYear(); + + let expected = `Today is ${month} ${day}, ${year}`; + + atom.commands.dispatch(editor.element, 'test-package:test-command-date'); + expect(editor.getText()).toBe(expected); + }); + + it("interpolates a CLIPBOARD variable into the snippet expansion", () => { + atom.clipboard.write('(clipboard text)'); + atom.commands.dispatch(editor.element, 'test-package:test-command-clipboard'); + expect(editor.getText()).toBe('lorem ipsum (clipboard text) dolor sit amet'); + }); + + it("interpolates a transformed variable into the snippet expansion", () => { + atom.clipboard.write('(clipboard 19283 text)'); + atom.commands.dispatch(editor.element, 'test-package:test-command-clipboard-transformed'); + expect(editor.getText()).toBe('lorem ipsum (clipboard text) dolor sit amet'); + }); + + it("interpolates an upcased variable", () => { + atom.clipboard.write('(clipboard Text is Multiple words)'); + atom.commands.dispatch(editor.element, 'test-package:test-command-clipboard-upcased'); + expect(editor.lineTextForBufferRow(0)).toBe('lorem ipsum (CLIPBOARD TEXT IS MULTIPLE WORDS) dolor sit amet'); + expect(editor.lineTextForBufferRow(1)).toBe('(clipboard text is multiple words)'); + expect(editor.lineTextForBufferRow(2)).toBe('clipboardTextIsMultipleWords'); + expect(editor.lineTextForBufferRow(3)).toBe('ClipboardTextIsMultipleWords'); + // The /capitalize flag will only uppercase the first character, so none + // of this clipboard value will be changed. + expect(editor.lineTextForBufferRow(4)).toBe('(clipboard Text is Multiple words)'); + }); + + it("interpolates file path variables", () => { + editor.insertText('fpath'); + simulateTabKeyEvent(); + let filePath = editor.getPath(); + + expect(editor.lineTextForBufferRow(0)).toEqual("file paths:"); + expect(editor.lineTextForBufferRow(1)).toEqual(filePath); + expect(editor.lineTextForBufferRow(2)).toEqual('sample.js'); + expect(editor.lineTextForBufferRow(3)).toEqual('sample'); + }); + + it("generates truly random values for RANDOM, RANDOM_HEX, and UUID", () => { + let reUUID = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/; + let reRandom = /^\d{6}$/; + let reRandomHex = /^[0-9a-f]{6}$/; + + editor.insertText('rndm'); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toEqual("random number is:"); + let randomFirst = editor.lineTextForBufferRow(1); + expect(reRandom.test(randomFirst)).toBe(true); + + editor.setText(''); + editor.insertText('rndm'); + simulateTabKeyEvent(); + + let randomSecond = editor.lineTextForBufferRow(1); + expect(reRandom.test(randomSecond)).toBe(true); + expect(randomSecond).not.toEqual(randomFirst); + + editor.setText(''); + editor.insertText('rndmhex'); + simulateTabKeyEvent(); + let randomHex1 = editor.lineTextForBufferRow(1); + expect(reRandomHex.test(randomHex1)).toBe(true); + + editor.setText(''); + editor.insertText('rndmhex'); + simulateTabKeyEvent(); + let randomHex2 = editor.lineTextForBufferRow(1); + expect(reRandomHex.test(randomHex2)).toBe(true); + expect(randomHex2).not.toEqual(randomHex1); + + // TODO: These tests will start running when we use a version of Electron + // that supports `crypto.randomUUID`. + if (SUPPORTS_UUID) { + editor.setText(''); + editor.insertText('rndmuuid'); + simulateTabKeyEvent(); + let randomUUID1 = editor.lineTextForBufferRow(1); + expect(reUUID.test(randomUUID1)).toBe(true); + + editor.setText(''); + editor.insertText('rndmuuid'); + simulateTabKeyEvent(); + let randomUUID2 = editor.lineTextForBufferRow(1); + expect(reUUID.test(randomUUID2)).toBe(true); + expect(randomUUID2).not.toEqual(randomUUID1); + } + }); + + describe("and the command is invoked in an HTML document", () => { + beforeEach(() => { + atom.grammars.assignLanguageMode(editor, 'text.html.basic'); + editor.setText(''); + }); + + it("combines transformations and variable references", () => { + editor.insertText('lorem'); + editor.selectToBeginningOfLine(); + + atom.commands.dispatch(editor.element, 'test-package:wrap-in-html-tag'); + + expect(editor.getText()).toBe( + `
lorem
` + ); + + editor.insertText("aside class=\"wat\""); + + expect(editor.getText()).toBe(""); + + simulateTabKeyEvent(); + expect(editor.getSelectedText()).toEqual('lorem'); + }); + }); + + }); + + describe("when the 'snippets:available' command is triggered", () => { + let availableSnippetsView = null; + + beforeEach(() => { + atom.grammars.assignLanguageMode(editor, 'source.js'); + Snippets.add(__filename, { + ".source.js": { + "test": { + prefix: "test", + body: "${1:Test pass you will}, young " + }, + + "challenge": { + prefix: "chal", + body: "$1: ${2:To pass this challenge}" + } + } + }); + + delete Snippets.availableSnippetsView; + + atom.commands.dispatch(editorElement, "snippets:available"); + + waitsFor(() => atom.workspace.getModalPanels().length === 1); + + runs(() => { + availableSnippetsView = atom.workspace.getModalPanels()[0].getItem(); + }); + }); + + it("renders a select list of all available snippets", () => { + expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe('test'); + expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe('test'); + expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe('${1:Test pass you will}, young '); + + availableSnippetsView.selectListView.selectNext(); + + expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe('chal'); + expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe('challenge'); + expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe('$1: ${2:To pass this challenge}'); + }); + + it("writes the selected snippet to the editor as snippet", () => { + availableSnippetsView.selectListView.confirmSelection(); + + expect(editor.getCursorScreenPosition()).toEqual([0, 18]); + expect(editor.getSelectedText()).toBe('Test pass you will'); + expect(editor.lineTextForBufferRow(0)).toBe('Test pass you will, young var quicksort = function () {'); + }); + + it("closes the dialog when triggered again", () => { + atom.commands.dispatch(availableSnippetsView.selectListView.refs.queryEditor.element, 'snippets:available'); + expect(atom.workspace.getModalPanels().length).toBe(0); + }); + }); +}); diff --git a/packages/snippets/spec/variable-spec.js b/packages/snippets/spec/variable-spec.js new file mode 100644 index 000000000..e6dcff11a --- /dev/null +++ b/packages/snippets/spec/variable-spec.js @@ -0,0 +1,67 @@ +const Variable = require('../lib/variable'); +const {Point} = require('atom'); + +describe('Variable', () => { + + let fakeCursor = { + getCurrentWordBufferRange () { return true; }, + getBufferRow () { return 9; }, + }; + + let fakeSelectionRange = { + isEmpty: () => false + }; + + let fakeEditor = { + getTitle () { return 'foo.rb'; }, + getPath () { return '/Users/pulsar/code/foo.rb'; }, + getTextInBufferRange (x) { + return x === true ? 'word' : 'this text is selected'; + }, + lineTextForBufferRow () { + return `this may be considered an entire line for the purposes of variable tests`; + } + }; + + let fakeParams = {editor: fakeEditor, cursor: fakeCursor, selectionRange: fakeSelectionRange}; + + it('resolves to the right value', () => { + const expected = { + 'TM_FILENAME': 'foo.rb', + 'TM_FILENAME_BASE': 'foo', + 'TM_CURRENT_LINE': `this may be considered an entire line for the purposes of variable tests`, + 'TM_CURRENT_WORD': 'word', + 'TM_LINE_INDEX': '9', + 'TM_LINE_NUMBER': '10', + 'TM_DIRECTORY': '/Users/pulsar/code', + 'TM_SELECTED_TEXT': 'this text is selected' + }; + + for (let variable in expected) { + let vrbl = new Variable({variable}); + expect( + vrbl.resolve(fakeParams) + ).toEqual(expected[variable]); + } + + }); + + it('transforms', () => { + let vrbl = new Variable({ + variable: 'TM_FILENAME', + substitution: { + find: /(?:^|_)([A-Za-z0-9]+)(?:\.rb)?/g, + replace: [ + {escape: 'u'}, + {backreference: 1} + ] + }, + point: new Point(0, 0), + snippet: {} + }); + + expect( + vrbl.resolve({editor: fakeEditor}) + ).toEqual('Foo'); + }); +}); From 15dd4f29c081ddaf3fdaf83c4eb07d9b639293bc Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 28 Apr 2024 13:22:01 -0700 Subject: [PATCH 2/4] Update packages info --- package.json | 4 ++-- packages/README.md | 8 ++++---- packages/snippets/package.json | 2 +- yarn.lock | 3 +-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index c48ff6149..45c899cb7 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "service-hub": "^0.7.4", "settings-view": "file:packages/settings-view", "sinon": "9.2.1", - "snippets": "github:pulsar-edit/snippets#v1.8.0", + "snippets": "file:./packages/snippets", "solarized-dark-syntax": "file:packages/solarized-dark-syntax", "solarized-light-syntax": "file:packages/solarized-light-syntax", "spell-check": "file:packages/spell-check", @@ -235,7 +235,7 @@ "package-generator": "file:./packages/package-generator", "pulsar-updater": "file:./packages/pulsar-updater", "settings-view": "file:./packages/settings-view", - "snippets": "1.8.0", + "snippets": "file:./packages/snippets", "spell-check": "file:./packages/spell-check", "status-bar": "file:./packages/status-bar", "styleguide": "file:./packages/styleguide", diff --git a/packages/README.md b/packages/README.md index 0dbf9f0c6..fc13bbda9 100644 --- a/packages/README.md +++ b/packages/README.md @@ -86,13 +86,15 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate | **settings-view** | [`./settings-view`](./settings-view) | | | **package-generator** | [`./package-generator`](./package-generator) | | | **pulsar-updater** | [`./pulsar-updater`](./pulsar-updater) | | -| **snippets** | [`pulsar-edit/snippets`][snippets] | | +| **snippets** | [`./snippets`][./snippets] | | | **solarized-dark-syntax** | [`./solarized-dark-syntax`](./solarized-dark-syntax) | | | **solarized-light-syntax** | [`./solarized-light-syntax`](./solarized-light-syntax) | | | **spell-check** | [`./spell-check`](./spell-check) | | | **status-bar** | [`./status-bar`](./status-bar) | | +| **symbol-provider-ctags** | [`./symbol-provider-ctags`](./symbol-provider-ctags) | | +| **symbol-provider-tree-sitter** | [`./symbol-provider-tree-sitter`](./symbol-provider-tree-sitter) | | | **styleguide** | [`./styleguide`](./styleguide) | | -| **symbols-view** | [`pulsar-edit/symbols-view`][symbols-view] | | +| **symbols-view** | [`./symbols-view`][./symbols-view] | | | **tabs** | [`./tabs`](./tabs) | | | **timecop** | [`./timecop`](./timecop) | | | **tree-view** | [`./tree-view`](./tree-view) | | @@ -102,5 +104,3 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate | **wrap-guide** | [`./wrap-guide`](./wrap-guide) | | [github]: https://github.com/pulsar-edit/github -[snippets]: https://github.com/pulsar-edit/snippets -[symbols-view]: https://github.com/pulsar-edit/symbols-view diff --git a/packages/snippets/package.json b/packages/snippets/package.json index ef5fe4355..e68decb79 100644 --- a/packages/snippets/package.json +++ b/packages/snippets/package.json @@ -3,7 +3,7 @@ "version": "1.8.0", "main": "./lib/snippets", "description": "Expand snippets matching the current prefix with `tab`.", - "repository": "https://github.com/pulsar-edit/snippets", + "repository": "https://github.com/pulsar-edit/pulsar", "license": "MIT", "engines": { "atom": "*" diff --git a/yarn.lock b/yarn.lock index 7fbdb5877..5382fb9d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8650,9 +8650,8 @@ smart-buffer@^4.0.2, smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -"snippets@github:pulsar-edit/snippets#v1.8.0": +"snippets@file:./packages/snippets": version "1.8.0" - resolved "https://codeload.github.com/pulsar-edit/snippets/tar.gz/31a21d2d6c7e10756f204c2fbb7bb8d140f0744d" dependencies: async "~0.2.6" atom-select-list "^0.7.0" From b2eff8751040a5920262bef53004e90dd377f068 Mon Sep 17 00:00:00 2001 From: confused_techie Date: Tue, 7 May 2024 07:40:33 -0700 Subject: [PATCH 3/4] Update packages/README.md Co-authored-by: DeeDeeG --- packages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/README.md b/packages/README.md index fc13bbda9..296971e12 100644 --- a/packages/README.md +++ b/packages/README.md @@ -86,7 +86,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate | **settings-view** | [`./settings-view`](./settings-view) | | | **package-generator** | [`./package-generator`](./package-generator) | | | **pulsar-updater** | [`./pulsar-updater`](./pulsar-updater) | | -| **snippets** | [`./snippets`][./snippets] | | +| **snippets** | [`./snippets`](./snippets) | | | **solarized-dark-syntax** | [`./solarized-dark-syntax`](./solarized-dark-syntax) | | | **solarized-light-syntax** | [`./solarized-light-syntax`](./solarized-light-syntax) | | | **spell-check** | [`./spell-check`](./spell-check) | | From 93ff81912269ff785eb48848f49cb7cab18c7bc6 Mon Sep 17 00:00:00 2001 From: confused_techie Date: Tue, 7 May 2024 07:40:39 -0700 Subject: [PATCH 4/4] Update packages/README.md Co-authored-by: DeeDeeG --- packages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/README.md b/packages/README.md index 296971e12..f24453c50 100644 --- a/packages/README.md +++ b/packages/README.md @@ -94,7 +94,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate | **symbol-provider-ctags** | [`./symbol-provider-ctags`](./symbol-provider-ctags) | | | **symbol-provider-tree-sitter** | [`./symbol-provider-tree-sitter`](./symbol-provider-tree-sitter) | | | **styleguide** | [`./styleguide`](./styleguide) | | -| **symbols-view** | [`./symbols-view`][./symbols-view] | | +| **symbols-view** | [`./symbols-view`](./symbols-view) | | | **tabs** | [`./tabs`](./tabs) | | | **timecop** | [`./timecop`](./timecop) | | | **tree-view** | [`./tree-view`](./tree-view) | |