From 4f02ca3b71c3fa2cc0b279ca5ca64647fccc8664 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 28 Apr 2024 13:16:43 -0700 Subject: [PATCH 01/31] 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 02/31] 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 8064ea6c6b355b0938aae9bda8fd596b9f561676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Sun, 5 May 2024 20:40:55 -0300 Subject: [PATCH 03/31] Debugging when a package service is incorrect This PR is basically to add some debugging info for providers and consumers. Currently, when a package registers as a service provider or a service consumer, and doesn't actually implements the right interface (a function, basically) the package loader silently ignores it. This commit adds a warning to console so that we know a package did the wrong thing. --- src/package.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/package.js b/src/package.js index 0071472dc..1831e919d 100644 --- a/src/package.js +++ b/src/package.js @@ -428,6 +428,8 @@ module.exports = class Package { methodName = versions[version]; if (typeof this.mainModule[methodName] === 'function') { servicesByVersion[version] = this.mainModule[methodName](); + } else { + console.warn(`Package ${this.name} declares it provides ${name}@${version} but it doesn't expose a function in ${methodName}`) } } this.activationDisposables.add( @@ -447,6 +449,8 @@ module.exports = class Package { this.mainModule[methodName].bind(this.mainModule) ) ); + } else { + console.warn(`Package ${this.name} declares it consumes ${name}@${version} but it doesn't expose a function in ${methodName}`) } } } From 70f48e27702eb7c5a94cf1dae246a72622d94f82 Mon Sep 17 00:00:00 2001 From: DeeDeeG Date: Sun, 5 May 2024 20:26:53 -0400 Subject: [PATCH 04/31] CI: Pin to macOS 12 runner images instead of macos-latest (GiHub Actions) There is an issue with the libiconv library not being available in macOS 14 out of the box. We can get it from Homebrew perhaps, but for now pin to macos-12 runner images so we can be assured of having working CI faster, without having to R&D a libiconv- related workaround for macOS 14. --- .github/workflows/build.yml | 2 +- .github/workflows/editor-tests.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d381edcba..d7718e8b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: build: strategy: matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] + os: [ ubuntu-latest, macos-12, windows-latest ] include: - os: ubuntu-latest image: "debian:10" diff --git a/.github/workflows/editor-tests.yml b/.github/workflows/editor-tests.yml index 936c19aa3..c40264941 100644 --- a/.github/workflows/editor-tests.yml +++ b/.github/workflows/editor-tests.yml @@ -15,8 +15,8 @@ jobs: !startsWith(github.event.pull_request.title, '[skip-editor-ci]') strategy: matrix: - # os: [ubuntu-20.04, macos-latest, windows-2019] - os: [ubuntu-20.04, macos-latest] + # os: [ubuntu-20.04, macos-12, windows-2019] + os: [ubuntu-20.04, macos-12] fail-fast: false runs-on: ${{ matrix.os }} steps: From da1912d1a70c9a9687fddddcfc8ae98b4d49a0f9 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 14 Apr 2024 14:57:06 -0700 Subject: [PATCH 05/31] [language-sass] Add SCSS Tree-sitter grammar --- .../grammars/modern-tree-sitter-scss.cson | 28 ++ .../grammars/tree-sitter/folds.scm | 2 + .../grammars/tree-sitter/highlights.scm | 365 ++++++++++++++++++ .../grammars/tree-sitter/indents.scm | 3 + .../grammars/tree-sitter/tags.scm | 7 + .../tree-sitter/tree-sitter-scss.wasm | Bin 0 -> 245877 bytes packages/language-sass/lib/main.js | 12 + packages/language-sass/package.json | 15 +- packages/language-sass/snippets/scss.cson | 49 +++ 9 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 packages/language-sass/grammars/modern-tree-sitter-scss.cson create mode 100644 packages/language-sass/grammars/tree-sitter/folds.scm create mode 100644 packages/language-sass/grammars/tree-sitter/highlights.scm create mode 100644 packages/language-sass/grammars/tree-sitter/indents.scm create mode 100644 packages/language-sass/grammars/tree-sitter/tags.scm create mode 100755 packages/language-sass/grammars/tree-sitter/tree-sitter-scss.wasm create mode 100644 packages/language-sass/lib/main.js create mode 100644 packages/language-sass/snippets/scss.cson diff --git a/packages/language-sass/grammars/modern-tree-sitter-scss.cson b/packages/language-sass/grammars/modern-tree-sitter-scss.cson new file mode 100644 index 000000000..853e8b167 --- /dev/null +++ b/packages/language-sass/grammars/modern-tree-sitter-scss.cson @@ -0,0 +1,28 @@ +name: 'SCSS' +scopeName: 'source.css.scss' +type: 'modern-tree-sitter' +# Built from the fork at savetheclocktower/tree-sitter-scss. +parser: 'tree-sitter-scss' + +fileTypes: [ + 'scss' + 'css.scss' + 'css.scss.erb' + 'scss.erb' + 'scss.liquid' +] + +injectionRegex: '^(scss|SCSS)$' + +treeSitter: + parserSource: 'github:savetheclocktower/tree-sitter-scss#e91dcb2c91b3e8853cccf7079b6a09fdeead75a3' + grammar: 'tree-sitter/tree-sitter-scss.wasm' + highlightsQuery: 'tree-sitter/highlights.scm' + foldsQuery: 'tree-sitter/folds.scm' + indentsQuery: 'tree-sitter/indents.scm' + tagsQuery: 'tree-sitter/tags.scm' + +comments: + start: '//' + line: '//' + block: ['/*', '*/'] diff --git a/packages/language-sass/grammars/tree-sitter/folds.scm b/packages/language-sass/grammars/tree-sitter/folds.scm new file mode 100644 index 000000000..a00841142 --- /dev/null +++ b/packages/language-sass/grammars/tree-sitter/folds.scm @@ -0,0 +1,2 @@ + +(block) @fold diff --git a/packages/language-sass/grammars/tree-sitter/highlights.scm b/packages/language-sass/grammars/tree-sitter/highlights.scm new file mode 100644 index 000000000..a04a4384d --- /dev/null +++ b/packages/language-sass/grammars/tree-sitter/highlights.scm @@ -0,0 +1,365 @@ +; WORKAROUND: +; +; When you're typing a new property name inside of a list, tree-sitter-css will +; assume the thing you're typing is a descendant selector tag name until you +; get to the colon. This prevents it from highlighting the incomplete line like +; a selector tag name. + +(ERROR + (descendant_selector + (tag_name) @_IGNORE_ + (#set! capture.final true))) + +(ERROR + (attribute_name) @_IGNORE_ + (#set! capture.final true)) + +((ERROR + (attribute_name) @invalid.illegal) + (#set! capture.final true)) + +; COMMENTS +; ======== + +(comment) @comment.block.scss + +; Scope the block-comment delimiters (`/*` and `*/`). +((comment) @punctuation.definition.comment.begin.scss + (#set! adjust.startAndEndAroundFirstMatchOf "^/\\*")) +((comment) @punctuation.definition.comment.end.scss + (#set! adjust.startAndEndAroundFirstMatchOf "\\*/$")) + +(single_line_comment) @comment.line.double-slash.scss + +((single_line_comment) @punctuation.definition.comment.scss + (#set! adjust.startAndEndAroundFirstMatchOf "^//")) + + +; SELECTORS +; ========= + +; (selectors "," @punctuation.separator.list.comma.scss) + +; The "div" in `div.foo {`. +(tag_name) @entity.name.tag.scss +; The "*" in `div > * {`. +(universal_selector) @entity.name.tag.universal.scss +; The "&" in `&:hover {`. +(nesting_selector) @entity.name.tag.reference.scss + +; The "foo" in `div[attr=foo] {`. +(attribute_selector (plain_value) @string.unquoted.scss) + +[ + (child_selector ">") + (sibling_selector "~") + (adjacent_sibling_selector "+") +] @keyword.operator.combinator.scss + +; The '.' in `.foo`. +(class_selector "." @punctuation.definition.entity.scss) + +; The '.foo' in `.foo`. +((class_selector) @entity.other.attribute-name.class.scss + (#set! adjust.startAt lastChild.previousSibling.startPosition)) + +; The '%' in `%foo`. +(placeholder_selector "%" @punctuation.definition.entity.scss) + +; The '%foo' in `%foo`. +(placeholder_selector) @entity.other.attribute-name.class.scss + + +(pseudo_class_selector [":" "::"] @punctuation.definition.entity.scss) + +; Pseudo-classes without arguments: the ":first-of-type" in `li:first-of-type`. +((pseudo_class_selector (class_name) (arguments) .) @entity.other.attribute-name.pseudo-class.scss + (#set! adjust.startAt lastChild.previousSibling.previousSibling.startPosition) + (#set! adjust.endAt lastChild.previousSibling.endPosition) + (#set! capture.final true)) + +; Pseudo-classes with arguments: the ":nth-of-type" in `li:nth-of-type(2n-1)`. +((pseudo_class_selector (class_name) .) @entity.other.attribute-name.pseudo-class.scss + (#set! adjust.startAt lastChild.previousSibling.startPosition) + (#set! adjust.endAt lastChild.endPosition)) + +(arguments + "(" @punctuation.definition.arguments.begin.bracket.round.scss + ")" @punctuation.definition.arguments.end.bracket.round.scss) + +(attribute_selector + "[" @punctuation.definition.entity.begin.bracket.square.scss + (attribute_name) @entity.other.attribute-name.scss + "]" @punctuation.definition.entity.end.bracket.square.scss) + +(attribute_selector + ["=" "^=" "$=" "~=" "|="] @keyword.operator.pattern.scss) + + +; CSS VARIABLES +; ============= + +(declaration + (property_name) @variable.other.assignment.scss + (#match? @variable.other.assignment.scss "^--" ) + (#set! capture.final true)) + + +; SCSS VARIABLES +; ============== + +(variable_name) @variable.declaration.scss +[(variable_value)] @variable.scss +(argument_name) @variable.parameter.scss +(each_statement (value) @variable.declaration.scss) + +; PROPERTIES +; ========== + +; TODO: Is it worth it to try to maintain a list of recognized property names? +; Would be useful to know if you've typo'd something, but it would be a +; maintenance headache. +(declaration + (property_name) @support.type.property-name.scss) + +(important) @keyword.other.important.css.scss +(default) @keyword.other.default.scss + +; VALUES +; ====== + +; Strings +; ------- + +((string_value) @string.quoted.double.scss + (#match? @string.quoted.double.scss "^\"") + (#match? @string.quoted.double.scss "\"$")) + +((string_value) @string.quoted.single.scss + (#match? @string.quoted.single.scss "^'") + (#match? @string.quoted.single.scss "'$")) + +((string_value) @punctuation.definition.string.begin.scss + (#set! adjust.startAndEndAroundFirstMatchOf "^[\"']")) + +((string_value) @punctuation.definition.string.end.scss + (#set! adjust.startAndEndAroundFirstMatchOf "[\"']$")) + + +; Property value constants +; ------------------------ + +; TODO: Is this worth it? +((plain_value) @support.constant.property-value.scss + (#match? @support.constant.property-value.scss "^(above|absolute|active|add|additive|after-edge|alias|all|all-petite-caps|all-scroll|all-small-caps|alpha|alphabetic|alternate|alternate-reverse|always|antialiased|auto|auto-pos|available|avoid|avoid-column|avoid-page|avoid-region|backwards|balance|baseline|before-edge|below|bevel|bidi-override|blink|block|block-axis|block-start|block-end|bold|bolder|border|border-box|both|bottom|bottom-outside|break-all|break-word|bullets|butt|capitalize|caption|cell|center|central|char|circle|clip|clone|close-quote|closest-corner|closest-side|col-resize|collapse|color|color-burn|color-dodge|column|column-reverse|common-ligatures|compact|condensed|contain|content|content-box|contents|context-menu|contextual|copy|cover|crisp-edges|crispEdges|crosshair|cyclic|dark|darken|dashed|decimal|default|dense|diagonal-fractions|difference|digits|disabled|disc|discretionary-ligatures|distribute|distribute-all-lines|distribute-letter|distribute-space|dot|dotted|double|double-circle|downleft|downright|e-resize|each-line|ease|ease-in|ease-in-out|ease-out|economy|ellipse|ellipsis|embed|end|evenodd|ew-resize|exact|exclude|exclusion|expanded|extends|extra-condensed|extra-expanded|fallback|farthest-corner|farthest-side|fill|fill-available|fill-box|filled|fit-content|fixed|flat|flex|flex-end|flex-start|flip|flow-root|forwards|freeze|from-image|full-width|geometricPrecision|georgian|grab|grabbing|grayscale|grid|groove|hand|hanging|hard-light|help|hidden|hide|historical-forms|historical-ligatures|horizontal|horizontal-tb|hue|icon|ideograph-alpha|ideograph-numeric|ideograph-parenthesis|ideograph-space|ideographic|inactive|infinite|inherit|initial|inline|inline-axis|inline-block|inline-end|inline-flex|inline-grid|inline-list-item|inline-start|inline-table|inset|inside|inter-character|inter-ideograph|inter-word|intersect|invert|isolate|isolate-override|italic|jis04|jis78|jis83|jis90|justify|justify-all|kannada|keep-all|landscape|large|larger|left|light|lighten|lighter|line|line-edge|line-through|linear|linearRGB|lining-nums|list-item|local|loose|lowercase|lr|lr-tb|ltr|luminance|luminosity|main-size|mandatory|manipulation|manual|margin-box|match-parent|match-source|mathematical|max-content|medium|menu|message-box|middle|min-content|miter|mixed|move|multiply|n-resize|narrower|ne-resize|nearest-neighbor|nesw-resize|newspaper|no-change|no-clip|no-close-quote|no-common-ligatures|no-contextual|no-discretionary-ligatures|no-drop|no-historical-ligatures|no-open-quote|no-repeat|none|nonzero|normal|not-allowed|nowrap|ns-resize|numbers|numeric|nw-resize|nwse-resize|oblique|oldstyle-nums|open|open-quote|optimizeLegibility|optimizeQuality|optimizeSpeed|optional|ordinal|outset|outside|over|overlay|overline|padding|padding-box|page|painted|pan-down|pan-left|pan-right|pan-up|pan-x|pan-y|paused|petite-caps|pixelated|plaintext|pointer|portrait|pre|pre-line|pre-wrap|preserve-3d|progress|progressive|proportional-nums|proportional-width|proximity|radial|recto|region|relative|remove|repeat|repeat-[xy]|reset-size|reverse|revert|ridge|right|rl|rl-tb|round|row|row-resize|row-reverse|row-severse|rtl|ruby|ruby-base|ruby-base-container|ruby-text|ruby-text-container|run-in|running|s-resize|saturation|scale-down|screen|scroll|scroll-position|se-resize|semi-condensed|semi-expanded|separate|sesame|show|sideways|sideways-left|sideways-lr|sideways-right|sideways-rl|simplified|slashed-zero|slice|small|small-caps|small-caption|smaller|smooth|soft-light|solid|space|space-around|space-between|space-evenly|spell-out|square|sRGB|stacked-fractions|start|static|status-bar|swap|step-end|step-start|sticky|stretch|strict|stroke|stroke-box|style|sub|subgrid|subpixel-antialiased|subtract|super|sw-resize|symbolic|table|table-caption|table-cell|table-column|table-column-group|table-footer-group|table-header-group|table-row|table-row-group|tabular-nums|tb|tb-rl|text|text-after-edge|text-before-edge|text-bottom|text-top|thick|thin|titling-caps|top|top-outside|touch|traditional|transparent|triangle|ultra-condensed|ultra-expanded|under|underline|unicase|unset|upleft|uppercase|upright|use-glyph-orientation|use-script|verso|vertical|vertical-ideographic|vertical-lr|vertical-rl|vertical-text|view-box|visible|visibleFill|visiblePainted|visibleStroke|w-resize|wait|wavy|weight|whitespace|wider|words|wrap|wrap-reverse|x|x-large|x-small|xx-large|xx-small|y|zero|zoom-in|zoom-out)$")) + +; All property values that have special meaning in `font-family`. +; TODO: Restrict these to be meaningful only when the property name is font-related? +((plain_value) @support.constant.property-value.font-name.scss + (#match? @support.constant.property-value.font-name.scss "^(serif|sans-serif|monospace|cursive|fantasy|system-ui|ui-serif|ui-sans-serif|ui-monospace|ui-rounded|emoji|math|fangsong)$")) + +; All property values that have special meaning in `list-style-type`. +; TODO: Restrict these to be meaningful only when the property name is `list-style-type`? +((plain_value) @support.constant.property-value.list-style-type.scss + (#match? @support.constant.property-value.list-style-type.scss "^(arabic-indic|armenian|bengali|cambodian|circle|cjk-decimal|cjk-earthly-branch|cjk-heavenly-stem|cjk-ideographic|decimal|decimal-leading-zero|devanagari|disc|disclosure-closed|disclosure-open|ethiopic-halehame-am|ethiopic-halehame-ti-e[rt]|ethiopic-numeric|georgian|gujarati|gurmukhi|hangul|hangul-consonant|hebrew|hiragana|hiragana-iroha|japanese-formal|japanese-informal|kannada|katakana|katakana-iroha|khmer|korean-hangul-formal|korean-hanja-formal|korean-hanja-informal|lao|lower-alpha|lower-armenian|lower-greek|lower-latin|lower-roman|malayalam|mongolian|myanmar|oriya|persian|simp-chinese-formal|simp-chinese-informal|square|tamil|telugu|thai|tibetan|trad-chinese-formal|trad-chinese-informal|upper-alpha|upper-armenian|upper-latin|upper-roman|urdu)$")) + +; Numbers & units +; --------------- + +; This node type appears to always be a hex color. +(color_value) @constant.other.color.rgb-value.hex.scss + +[(integer_value) (float_value)] @constant.numeric.scss + +; All unit types with valid scope names. +((unit) @keyword.other.unit._TEXT_.scss + (#match? @keyword.other.unit._TEXT_.scss "^(deg|grad|rad|turn|ch|cm|em|ex|fr|in|mm|mozmm|pc|pt|px|q|rem|vh|vmax|vmin|vw|dpi|dpcm|dpps|s|ms)$")) + +((unit) @keyword.other.unit.percentage.scss + (#eq? @keyword.other.unit.percentage.scss "%")) + +; The magic color value `currentColor`. +((plain_value) @support.constant.color.current.scss + (#eq? @support.constant.color.current.scss "currentColor")) + +; Match the TM bundle's special treatment of named colors. +((plain_value) @support.constant.color.w3c-standard-color-name.scss + (#match? @support.constant.color.w3c-standard-color-name.scss "^(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)$")) + +((plain_value) @support.constant.color.w3c-extended-color-name.scss + (#match? @support.constant.color.w3c-extended-color-name.scss "^(aliceblue|antiquewhite|aquamarine|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|gold|goldenrod|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|magenta|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olivedrab|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rebeccapurple|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|transparent|turquoise|violet|wheat|whitesmoke|yellowgreen)$")) + +((plain_value) @invalid.deprecated.color.system.scss + (#match? @invalid.deprecated.color.system.scss "^(ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)$")) + +; Builtins +; -------- + +(boolean_value) @constant.boolean._TEXT_.scss +(null_value) @constant.language.null.scss + + +; FUNCTIONS +; ========= + +((function_name) @support.function.var.css.scss + (arguments (plain_value) @variable.css.scss) + (#eq? @support.function.var.css.scss "var") + (#set! capture.final true)) + +((function_name) @support.function._TEXT_.css.scss + (#match? @support.function._TEXT_.css.scss "^(abs|acos|annotation|asin|atan2?attr|blur|brightness|calc|character-variant|circle|clamp|color-contrast|color-mix|conic-gradient|contrast|cos|counters|cross-fade|cubic-bezier|device-cmyk|drop-shadow|element|ellipse|env|exp|format|grayscale|hsla?|hue-rotate|hwp|hypot|image|image-set|inset|invert|lab|lch|linear-gradient|local|log|matrix|matrix3d|max|min|minmax|mod|oklab|oklch|opacity|ornaments|paint|path|perspective|polygon|pow|radial-gradient|ray|rect|rem|repeat|repeating-(conic|linear|radial)-gradient|rgba?|rotate(3d)?|rotate(X|Y|Z)|round|saturate|scale(3d)?|scale(X|Y|Z)|sepia|sign|sin|skew(X|Y)?|sqrt|steps|styleset|stylistic|swash|symbols|tan|translate(3d)?|translate(X|Y|Z)|url)$") + (#set! capture.final true)) + +((function_name) @support.other.function._TEXT_.scss) + +((function_name) @_IGNORE_ + (arguments (plain_value) @string.unquoted.scss) + (#eq? @_IGNORE_ "url")) + +((module) @support.module._TEXT_.scss + (#match? @support.module._TEXT_.scss "^(color|list|map|math|meta|selector|string)$") + (#set! capture.final true)) + +(module) @support.other.module.scss + + +; MIXINS +; ====== + +(mixin_statement + (name) @entity.name.function.mixin.scss) + +(include_statement + (mixin_name) @support.other.function.mixin.scss) + + +; AT-RULES +; ======== + +"@media" @keyword.control.at-rule.media.css.scss +"@import" @keyword.control.at-rule.import.css.scss +"@charset" @keyword.control.at-rule.charset.css.scss +"@namespace" @keyword.control.at-rule.namespace.css.scss +"@supports" @keyword.control.at-rule.supports.css.scss +"@keyframes" @keyword.control.at-rule.keyframes.css.scss + +"@include" @keyword.control.at-rule.include.scss +"@mixin" @keyword.control.at-rule.mixin.scss +"@if" @keyword.control.at-rule.if.scss +"@else" @keyword.control.at-rule.else.scss +"@for" @keyword.control.at-rule.for.scss +"@use" @keyword.control.at-rule.use.scss +"@forward" @keyword.control.at-rule.forward.scss +"@extend" @keyword.control.at-rule.extend.scss +"@function" @keyword.control.at-rule.function.scss +"@return" @keyword.control.at-rule.return.scss +"@each" @keyword.control.at-rule.each.scss +"@at-root" @keyword.control.at-rule.at-root.scss + +"@error" @keyword.directive.error.scss +"@warn" @keyword.directive.warn.scss +"@debug" @keyword.directive.debug.scss + +(each_statement "in" @keyword.control.in.scss) + +; The parser is permissive and supports at-rule keywords that don't currently +; exist, so we'll set a fallback scope for those. +((at_keyword) @keyword.control.at-rule.other.scss + (#set! capture.shy true)) + +[(to) (from)] @keyword.control._TYPE_.css.scss + +(keyword_query) @support.constant.css.scss +(feature_name) @support.constant.css.scss + +[ + "as" + "from" + "through" +] @keyword.control._TYPE_.scss + + +(id_selector + "#" @punctuation.definition.entity.id.scss) @entity.other.attribute-name.id.scss + +((use_alias) @variable.language.alias.expanded.scss + (#eq? @variable.language.alias.expanded.scss "*") + (#set! capture.final true)) + +(use_alias) @variable.other.alias.scss + +; FUNCTIONS +; ========= + +(function_statement (name) @entity.name.function.scss) + + +; OPERATORS +; ========= + +; Used in `@media` queries. +["and" "not" "only" "or"] @keyword.operator.logical._TYPE_.scss + +; Used in `calc()` and elsewhere. +(binary_expression ["+" "-" "*" "/"] @keyword.operator.arithmetic.scss) + +"..." @keyword.operator.spread.scss + +; When `ERROR` is present here, it's typically because a rest parameter or +; argument is not the last in the list. Indicate this to the user by marking +; the '...' itself as invalid. +(ERROR + [ + (rest_parameter "..." @invalid.illegal.spread.scss) + (rest_argument "..." @invalid.illegal.spread.scss) + ] +) + + +; INTERPOLATION +; ============= + +(interpolation) @meta.embedded.line.interpolation.scss +(interpolation "#{" @punctuation.section.embedded.begin.scss) +(interpolation "}" @punctuation.section.embedded.end.scss) + +; OTHER STUFF +; =========== + +(keyframes_statement + name: (keyframes_name) @entity.name.keyframes.css.scss) + +(nesting_value) @entity.other.tag.reference.scss + +; PUNCTUATION +; =========== + +(parameters "(") @punctuation.definition.parameters.begin.brace.round.scss +(parameters ")") @punctuation.definition.parameters.end.brace.round.scss + +"," @punctuation.separator.comma.scss +":" @punctuation.separator.colon.scss +";" @punctuation.separator.semicolon.scss + +("{" @punctuation.brace.curly.begin.scss + (#set! capture.shy)) +("}" @punctuation.brace.curly.end.scss + (#set! capture.shy)) + +("(" @punctuation.brace.round.begin.scss + (#set! capture.shy)) +(")" @punctuation.brace.round.end.scss + (#set! capture.shy)) + +(":" @punctuation.separator.key-value.scss + (#set! capture.shy)) + + +; SECTIONS +; ======== + +(rule_set (block) @meta.block.inside-selector.scss) +((block) @meta.block.scss + (#set! capture.shy)) +(selectors) @meta.selector.scss diff --git a/packages/language-sass/grammars/tree-sitter/indents.scm b/packages/language-sass/grammars/tree-sitter/indents.scm new file mode 100644 index 000000000..dae6dde83 --- /dev/null +++ b/packages/language-sass/grammars/tree-sitter/indents.scm @@ -0,0 +1,3 @@ + +"{" @indent +"}" @dedent diff --git a/packages/language-sass/grammars/tree-sitter/tags.scm b/packages/language-sass/grammars/tree-sitter/tags.scm new file mode 100644 index 000000000..87679070c --- /dev/null +++ b/packages/language-sass/grammars/tree-sitter/tags.scm @@ -0,0 +1,7 @@ +(rule_set (selectors) @name) @definition.selector + +(keyframes_statement (keyframes_name) @name) @definition.keyframes + +(mixin_statement (name) @name) @definition.mixin + +(function_statement (name) @name) @definition.function diff --git a/packages/language-sass/grammars/tree-sitter/tree-sitter-scss.wasm b/packages/language-sass/grammars/tree-sitter/tree-sitter-scss.wasm new file mode 100755 index 0000000000000000000000000000000000000000..51fbdd4eafb0e612f4db1d530095b03f0cc91db1 GIT binary patch literal 245877 zcmeEP3Ah!-vF@2U7epXHj0?uZ3&yB%i!tsodQAj1Jd8_r5+H`eeMNo7B%-3CqJpA; zqJSWvprE2Gf`WkT0s@M%D<~)`?kJ+Ks(QLk*PJ=$O!v9>M*ZH)_lE0#s;mF1`n#uR zSDi08FHrD*Ew=mH73ZFH{y7KyIB3xQ$%eT=1^aB*DY#0({|8rf3a^5{U;}?D)k(oW z(N*+c_>T%7iJC@T&cAHCvo5*(k_$VZ(M1L3KRcg$;hCN7|2+TF^Yni<>)7#~ zr{jeeoOOQJE*D2vQKP0EJD%6&ybCVAqT^pXU(zMIN};DF9XodI{MU247;L+?C!c=6 z-!DD?jJ>;F+@(v$OU~*F4mw_P#wC|@?DD_x=lPw_h5zb&{`r8OadDT3Z=6SJl zS?B-ltc$yx(X}Hcs_R)7oUd~jTnPDZ-Yl1^Qc=SOLDZls3WA_vuc%3jjvX%tJ#;*y z^SS4CJfrIc7hj^HU1F^Bg%_TCMaRE&?%KKI#a+(5;EYRDE_3cxRrZXl278qc{$dEe zt7_nqUe(YoscO5Ov6>zKegWvJ>&0iC|BsHBbos}5UC!^S8hwX<(OJ2y^SPIHQQNm* z__EGWRFJD`oKZBYN%LgXs%fK!f!ZT%9DHm0Nz)oOi&W6K&z?cUtM&+LHjHX`HvBVY zYSr0?h6?`LM1cN#v;J|-id_8ThZ^Yisj=RofrSq@JiE2hjXsYXJ<%|3)D3QC4bW2qW3Ds(^opC@Xn;N%=ysC<`e|VEtp*r?K&Uy5&_AvjZGIf3 zfrVoXFb080TaU%RbNcV`_;;25dlLTLK>s}j|8A)No{oP9HJi1wYRuxR!%Xx`^p8*V zGr%$pJOpXd%~xn(z;FYs(ZKucW1R+`9bharXy6&P*rb6W_Zo}M8khqXrZ6AIjrtm! zEgG1}z~>rRI2alQnq0R>D7ImPOr!1^Sj}8|08ky*zNh|i)=*>JO9PK^uzfV};z(oB zPXjAB`T-hP%CQa7z!MzXPz`JanwsPY4UFU_qcm_oCw+_tZsRb+|O+0X=bn=s`MHSp>{<8+G#x1|H=meKc?@bM2>rZQShu4UA{i4${CV&e>26 zOt{ZPGXj9>IP;_QkN2_O#%N$XHy^8kt=!{y4fNwClQi%pTTIcwb!;(R1J^JxO9Q~w<$)^P_qia6bd1G;lLlW{d{zW?-xa`Z6$H1Gh3T zNdr9?m=asC(5C}nrp;OU$2U3U^E9xEfrT1)nzgV*18;E+mTBNQuF?tss^d9ojs9^A z%V3=b-scWBXrNbrlc!DCJkI-O{o^V~BJWJ_=XloNqJLh>s{336OSlT%9wj|I!rr@U zpch;8(7+hf9o(B4H zFAFuWnG3x{18;J1mua9oYi)%F?qDBlH1H++Sf_!vIk_7&Fq321q=Ah<%w%V?hL&;x zKGZ;On0U?Jaf^oTW-gy=;7fMg?J?5E00z2i;4#ig4-GubKu--!VxX4>wlV!a8W_bu zKMjoIUIu936}A|pfr$(Z)xa}(foNa=Ta41cTn5HypeOe-Hn!k$j@Q5k3{29%M=aVY z8d%50ny!JHpjc*AHcLZ`G12iDou{EW2*rzug#a1(FTuZK(`EQ~Y_mc)yN8vqMgzlm z=eZ88<9TF*{_z_AagzqNGTF@<7|+0m*xXDM`p5p<+2VZGTG4#a0;~08r!%x^tAN(=yv!DL)O%R~T-T?gBObhzw+gZayHL#Iq+7TMKjx9!M zU_M)n(ZEy&#%kad2F7dP3*H}2iktATn4*C#JfBb3zfthTv zL<4i!$1)8JWMG8`o?>8)1{SlAb+H8l8#M4bH`%0t$*kbb8koSqhZ=Z-nQqa*0}Onw zf#J}^RH@roD*s&w#YOK9kSSab{M(2D|Bmb3OE+7{Gf$NRG|+>o4${E0 z`SpSZKITe|(7>V*W~x+6o@R@M8d(02>1Bxq)^U?%8W_w>S7_im_OV6-*Rzjx8raCd1`TXwV3P(0 z@}g|B2BtCap$0~AFIzM)m3@4!foGXmH#nUDlfm6=(Om;?@IIi22JT~__SC>fY|%>t z8F83T>SwW*vh~l4crIvGD01yp>Aw5LIa=h@E@gtN7!PF2KwMqBi8U( z4SfMNX8slQx{pJF?CAV;szf_;)-zZ_$P?VH)RDjcSai zr}&T0f}rZEYPG*ot)gmGt@aL^w+K|LhT*Z*(ec&Ml%H?XLUzB!#$1*uKo4# zO@Vs^DGsSY&5;l=#(=#eVs+azh>q1>QEX6=Z3-j@?GTLRg#=+tv}qdI4w|UOkVX6r zlY;>U7@(1Nu~5LiwJObsoYowPDhDrERv6D^9LLGl8U`*6abAFb8-QG}s$uiS-~)gN z@StX|ny7gr4de)DU6VtAvIJIDRIBFOW50j~jT_;K;3;rgq4C&T8wSBud#IY~b6ST# zfXd>8x(oaROm~6=sw35o`k(LUe|A9WMDYKNR)>&VNW`IG6KIG3(VEnZ`94ySJ+}G( z|NVak{yzi%pMkG?2CBmYgPp$hMotA`l&fmcaJxp^H*V7OTg{rc`1TIp*>R_xcWJrn zcX#{V_jj*u)q0N~?D@mJ_TJ}5`|h{@0SEs0pr0K4(?bqD?C>LwJnCmh|NNL^YmWQH zFOP52_Jk8p`c=FCX@BysPdW9p(|_~Z-~Ij%fBaL2KmVm;r_O&p<8NL5{*N=yI{Tb+ z&pZEu3op9(lCGCtcKQEa@y{#&_3!^&)h)O>xF)zZ=pI}bTp!#J^aySYZVGM=dIq-y zw+6Qby@KAs?ZF*EpWx2muApzwFX$iK9o!QP2<{E;3kC**g2BQ4!H{5R@IWvu7#@rW z9t<7|Mh2sThl59g(ZQJD(crP*@nCH5MDS!VE*Kw72qp$k1(Slw!PCJr!IWTX@N6(G zm>$dso(pCMvx3>doM3J+FPI-ZA1nwK28)6hg2lm-U}^AT@KUfWcsY0_SRSkhRtBqr z)xnxzZSZRFTCgryAG{vC5o`!H25$y$1)GAmgLi^=gU!Kv!TZ4n!H2;|!N_VA9d zPk3i|SJ*e~7xoYD4(|yEg!hK`g#*Ju;o$K8a7Z{bd>|Ya4i86!4~7qgBg0YQ!{H<0 z=x|Kfz( zPB=H57tRl#4;O?B!$si>;o@*fxHNn*d?{QOz8t<1E)Q3PE5lXc>TpfCHheXFEnF9_ z4_^=82seZq!#BgX!cF1Z;XC2G;pXtY@crtY~&LCz>10i{?kqM+>5b z(W2;uXmPY8S{l6=y%a5rUXEUgmPae1mC>qbb+jg08@(F67OjicN3Tb3L>r=w(VNj* z(WdC_=$+`@Xmj*l^nUa~^kMW-^l|h_v?cm9`YhTSeI9L#zKFhzy5+9UU6Z>u*FAS# z?)uyfxgNP2b2sH~&h^aQlDjo`Tdr5GckcGw9l1WaJ9BsC`sVuO`seP>-IE)TyEk`V zZeVUuZgB4Y+>qSR+yl8`x#77Hxd(F(o6LU}HCgmpQp3Xg!o06NFdp0*MH$68a_grpfZdPt~Zcc7)ZeDJF?)lt;+``#s~L^!uO@FG3QKS}d?IuvEVnJRwMOT{V%B|Yq^(bdxShZ@}? z(Y-V}NA|Y2MpsJPeKfjCqCe8;REh4Z(N840pGF^*=>8ggJQ5KfpwZ>h_CSq}leR}{ z^d)I~ltv$u=+87dQF=I9qupd4ey-6Eq=zX=!}Da?m&foI(t2eKUoTy+is8A^$=VqH zT*9x%@B#^Mh~Z5V-WbELN%)-@zD5#!H-^8E)*r<1S_yv`!y6_1Q4IH%zCVfKx1}{4 zsgd-@OY6^LxQ9#?9Pg3!1Zn+641Z)`Jbb}%HAA&Ud;D`fo+g3eR&!LWEnKMRpBmvw z0W#X2|7yyARmbguBNZMajmr07+D}#OO)t->cGL^@_^LVq4hqTdI(3rnd_=yr{^iFf zn7)rV3ID2jEgvz?7(2p@airS42~8jcQ^o6M0ufUMqhhLHG@cYjk?a{2lLDh+QeZTm z6c$r3jK-6~5{br>!W4;$Nr63xNrBOLQWz`w#*@NO=~qk&>_H5FM#b=FG#>tQNT!U6 z;m@cX=M1lo<+w>QHUs(-sxVud0sW?g%@F-m!e)qmE@3kkw@KKH#V;i+hY>I_N=_Ru z8%JUNjcFB{Fs5WCj(1{LFlQ88W^6(fUD zF)|nxyGusJ$Y4~A3`WJsU{s6@M#acrG#(i%Xk;)dMh2tt?s7V*ol!Af7!~7%Q88W^ z72}0bIbIl+D~B`kMR%*5=ZZXf|O zEJqi^W+(Nbsf!s9X@l-*L%?&;TV$d-vh8Lq^crhx57o%c$ zF)D`_!*X~rEQc4va(FQ;hZnmBZE;fG8h#jgHbUu7!@OfQ86+Y6(fUDF)|nxBZE;fG8m0V#%DC` zF)Bs|qhfc&s2DGdit)mz7%z;9@xrJaFAU4^!mu1K49oGtupBQ8%kjdn94`#Z@xrj& z9Wg9-M-0o+#jxBR0c=K7@j*vM#>{EZ(eB9VqsX8W9XJmbp~0{m8Vt)Zz_1(x3`?;C zY~)&8)nsJMoKy|B$n4a_>Z6FNMF&Mxy*7U)j?WwDLl|f+RZS~5>pa_Yr0O+B)k43F z3Z*g{Yf_&&XH;mCQK3mjV@-ZcOD9H!CK-)2`6R7x8I3jhiu52f$+od3^{I14g(evl z`vFG9et=Q2A7E6fn&Fl4xifu^oME{iU|7!V49f))!*U8|SgM*~IfVo4c__Xe>R2={ zqEZ;p=3KCK>Np`-qGzAzqE%0UU{|LKmQf*CMujvP71Cr>NRv?^O-6+@8I7g+DM^!2 zAx%bwG#M4rWK>9#Q6Wu6g)|wJ(qvdllVK@MhNUzameOQcN|RwJO@^g30j@-vqIo6K zBzm?qX~|F^%~c6$eog{lR7jIiAx%bwG#M4rWK>9#(O8;~(X7d+kS3!-nv4o*GAg9W zsE{V3LYj;UX)-FM$*`0r!$z80X(z_8yjIAtQS2?UAm-}do6?C{Ic$)yyv)bGrR*7& zvS(Pzo?$6_fIVg32xnySYO9r&0tISaY1Aqd$f!^tqe6j<3I#GM6v(L18l$n+UZess zDzwI^&>EvcYm5r5F)BsHuo2Zz8Z!(V^$eG=EIeDw3Ips}%Enkq(R@$di58j5()O+T zS}3uzCBbWf_z*`g23Mp?f>9v}Muj986_Q|7NPBp8*F zVAx3FGnxw+Hj>a23d2$o3`T!|#et6fUocgCfBi>NL)rHt40gJ@(iDvl}{jmz_( zw2jMi7z&b2MI9J5$70KAs4#5O^|FM`{5wm+GK=8EGhIz$!ujS!#*MW+Eq**j!Pt4{ z$ve*b49fc}Chy{s5TkK5^vZxyao+-?;(P$3qH>IiIh;{3U>S`EtiElDQ88c{jTP|( zRfy4e6`}7?U^Ldrcxfwy!nR_-GAafvqhi1^YEBY7BfB#L_BIKda|bs_*yy#FgpFQ% zOV}(7ZkMn*rErIV%}If@a|hSwjf|Poxr1gXYDj`lO}U{t7@QK4!^g{m19s%BKEn$cL*`tAZog{m19s%BKEno*%@ zMun;wm8xdgsCp?~k7wAZdcK5>s$Z0_QS~wj8&$s~VWaAo4Q$kurs{clBV*>SY6#0T zE~09(Fg6aVUS;S>Rr3y7sCrqdsu>lkW>lz}Q88CDDpbv=P&K1M)r<;NGb&WgsF~74VX10{rK$loYAUX3GBRfFsl>#TjSnRBonVX#turdL&S(hQrCp>Ggl*l1nf^2M-RFf(jULFhY;88+t{^ex2z=hw$h>fQlYBBFT_?OMz0 zv?YhWTtW2g6L58IU8I~GkSZa`AsX>4%(V%Evi3W+DtwGxBTc>pO$%iGW8e~*xkWryQ zMui3$6&hqzXpm8%K}LlJ85J62RA`VOyH zWmE{3Q6X4HW5Md{Xp9QMGAabis1PipLa>Yq!7?fY%cu}6qf)R8OTjWM1^3^5 z$*>WuKJCr06fDD1unbGV0_-W+9dR!&n*W#|Ry!j9teU0bB9E}WZXUSNkFXI@Te`eH z5z>7jRl1A{=`t#$%czhpqe8lj3h6Q`q|2y~E~7%aj0)*8Dx}M(kS?P_x{M0xGAg9Y zsFW_lQo0OF=`w7jtFPEFETzk^lrF zoOoh1R+N5Vkx`*2M#X-bQK1q>g&r6cdSEoxgMJ8+QK1J$&1oZjgvhX|uzuQ*VRL@y zGdkJFuqpf&37czAn+@z)%J1PU1fAuZTN6&UC;W)2Ed<`T3n45_6#}E;aF)?{YSE9g zGb)6@s1O39VrpSj$b->%YSD+Yj0({(D&)bakO!ke9*i0t=#wN2n}X|;Bmn12<8*%G z`&e1gyhvp;o!=mOc9D61D2luwwaAQ$A~Pz=$*3qNqoQz(io!7}3dg7z?Tm_YGAhc+ zs3<3+vYZT?VXZGIGi<87j4oI)Y=*VIqztfU@o89#=9Ltm=-I{RJ&Gv)^QpyWR1}|4 zQG7;4@fj7xXH*oQQBizGMe!LG#b;C$pHWeKMosZwq;fKB?&o@5!lw9oh_kgR{yb@I zZroa6U{6i#juQfOmTz8(CWy7I3EpD~P0UZ#1fxO|j0#OKDm1~U&;+AG6O0N?Fe)^` zsL%wXLKBP%O)x4o!LTeo!?MZ%=gZ_Y?N?)!Me|CkO!Vw3^KMF1d0uLj85LD#R8*N! zQDsI&l^GRPW>i#}QBhAuMLii6^<>o4Q}3@Cmi1)VT)>-8qZVM#dbYxPisqHnljzy? zj+Q87Ll74>9P)RR$BPex@u8J0V2hE19E z4x3>!XX_m{!}7QcV9)CBhSe9%E2%!wv#Za$9#Q={snusxRG(2%eMUv~85PxMR8*f) zQGG^5^%)h_XH-<5QCWS4W%U`B)n`~%A7IZi({fETucXXG&n`3XWJH-~r~J08J3k}*o-;7N@iHDp8)o( zGp&+E^GfPW^z1tG9zxW4W@?=o6?JA*)R|FHXGTSx85MPARMeSKQD;U)of(yNW>^-K zVOcqVJu62mU(vjh$`L)gay;pa$~~7_IYve07!{RcR8)>pQ8`9MjTjX*VpPbM^ zF&bZz&`%06D(cCos3)VMo{XA$>gxs!n|kU?UI2R*?gzLKC9ihAdG;0go}QX-Mn%3E z75QdVky%DfX7yuL44cgA$EpDK%85NmjRAiP>ky%DX4jC0WWK`skQJF)AWex%M%ps|qyxKYBX-ed9 zYHAJ{6***7WQ$ReEk?zNfy0EK|#{S>NiXdKosW zB>k3NfISQGL!55Nt6d15)kGntq!xlvQ3yswAs7{fU{n->QBep+MIjg!g{$rXICPe8UcoMvNZSSFxlt7K>C}QUDhkS|C@7<% zpp1%wGAatns3<6-qM(e*f-)>C$FM9B!1)|IU8bP1BAQoFB4THkh-XbvqRFWxVpNof zQBfjBMTr;{#b8tvgHcfoMolsFT9aW}42I27*&rG(0DBf=U!1r^^9qVV?CfIjJS&PZ zDYY1kiefM-iovKT2BV@FjEZ6~DvH6VDTZEFGHi;Wmz4~gV(8ay09;8i$g7=1o;t;I z*-x1yil?F&6}Rg#DxURYRGigkR2(rg8W%@Dfz4=K9Q`(UM&oOAKSzl%C@RLNynl{i za~qX@X*jluLV_PQUt@jEtGnoVS0R z1Jk(3^K_zvBBw`+Kj%-O*~;OmTPSB@s&W_=%3)L}hf$#%MukQg6&hhQ)`)%{pHU$a zMx{s?mLg$TiiBY)5{9Kn7?vUd*c7n1NXW>Txr@Z=qlidE2hJja*7BiC-lYhUOh^?8 zqe3K%3Xw1>CK5))=?6x|+|Q^uNM;FAX!iEq;5N4WmLdj0$-$D&)bakO!ke9*hckFlx(#K_L%Dg*+IQ@?coXgJCHThNV0h zmhxa&$^+m^3;^@CB2ih43C77~ocdk+2aB1XmXf>ALEFdElhzXXU;vFKnluDyN<5ToMQn^7}@ z^(%ZBHfs+3RuhKJnnSq`JOi;1)~hkktt85uLDHOE0H5vz|P zYYx#t5fP;+N58%VTFW&D@8iV$@Ptv0&}O4 z*mJNQjF}Y8i{vTOU?Y0=VB>jTq-<NVbo3ogE9>OSCR(Nypl8!Jv$9NM~cJV z$4wf!SLd`W9qM#UbBQMqekSU$SO@G9}v4~FH^g<<(H8pCG)rr+ZMu&34z!67G_SE4nd zXKRh7E!@8etv!~iHAaQj7!|5wRH%wkp(;j&su&fjVpOP#QCn3E3RN*GRK=)J6{A8` zj7n88ELFv@R29QgRSZj2F)URDa3!h|%_~tA(X&;>JDylo`l-lAjjHS_0cdvW`|~$5 zSDSBU4x!OaHpa?n*MBIE9?`r=oM}7u*Xipq#M%yycPFv5kH9|0Bw|}fZ3(s8^7Ih5 zJsjJPPPAP^Z4FA_9i=`9vAwY@Y;TXm-tz4TvD;BdIOyYc`jOLz6Ex^V0)Um2fnj9Fc`bJhM$}NrN>KPrt`tksmC_13+&Wf!<szI@xu0a2gw5gX90{9y7(O<>%~;5f zn6wxDtukKn6Isy#9eCkyI(Mg6hhbSAhGlgaHq{wR92l0>VOUm&VObr9 zWpx;q)nQmxhhbR;fL+Q!=B7cmGEm3%0H(oJScd-TWnfsAfnigI_eoU@%Q7%5%fPTK z1H-Zm49hYwEX%;KECa)`3;>&~W*H4+ZW?4O19fbdfhI4z4DZr>+s~B2gmXRZR{+kB zzjDiDdr@i)>BORxda@bu3}j`*RS*$do5Q^f6tjubeC2LDGB*vf<(oRT^GyT8*7w~sEPEx# z0Sye>?k2LkEosL}G6O>|K5sSklgmejO-1zbkzku8=gWBQNlUp~lD!jcWNGYH(x~p4 zv?JPRXLPfU>~2olF+XEBkD|UQX-7nCe=m{085mbYq6X`aEoRL$vMYi-_g5 z$!?go5y=~5cTLic_}SsjrhczZ+A%+qh7DxbEonzYY=5_r-Iq~f7e+)1?Y=-egw4&4 zPf>G*%|3ZR9=5YXv~91tSlg1l6K%AUW99Rt9nrS!mQxnDCheG?@z;m=eU`K%BIs{Z zyr;aK>^@D}5s^Z>Es^IM#M;mB8bqH+0+(`aL9y9g4WcNO=yR(*_S>?-4JY#9O75l z{H^rn0Q)V5(}bwc&{9t}Bg#Y}qc>~6Yj7#Y043M%dNbvGqe+IDg>EN_5^OW)oSTx} zMLFM)?1qzT?D~`48%aCjXVd9RcCRPxn4g(E?;yMNhPGKZeL~R?Y||!w*o|qf=iYV6 zZit_4cOP~0TGEdA*>+2)-&d1%%+K`uDcP+}+7S`k-#ygtnxq{OvF&=1-Rh(r5wY!V zCA(EgJ0fD+btAi#NjoBfcAMkLy$9K?NZJt*v@ox#J0PE_$^D?5fR(2AKAT>v?C(6-FsBG7n62G1ntZew1(`KChdp_+L>FE`cWK9 zl6FMIwtJoYEl%1I5!>!2^7lg0j)>THHf_7${uP1-=l6FMIwp&ehbCY&N#J2mG?B*oxhzQ!51F!|e zZ+6m-h@jo4@%Xrx@;obPM?}!hT*+BarwL{z?SP0mVB423Q12g~0Ubshp2Ifi%Dz&> zpt)Ug8O6o0S(x{fE{rO#m9V^v5}f3Bj7}HC2OGC0rh@CB>nL4B)1I~=&!$QKK$ox6 z>xWln*tCiq-CDb5zILVGAPlYL{t-%6qDy^iz;w?p^|gIyExUy3n=T6yqc3GnOE#fe z=<*jd(HC={O*WxC6=|Z=KQ-Az#OZQg(8_mWO+F>r2ZWYaRi%CC<<>LFJ|GwQJ}SL* zsCDynvJXlg49DipYJ!W7r~4rEmBGnLOG<&X)K>u~B`qnC{nZZOksOYuar6|L;lPid z{hLWcoKbNbBctL@D@Mg#eT>HM4__^N6Zc=St+=C z=gGn?k9QsVD`D(I-r2>lyt9j8b2?kUE`eb)@4q0sG$%atTNK#Z9O=I%t<6&Y8X1mR z+I}Ie&5CHPgw3vIqlC?Vy|?sjcDwow4NTDNJ;zIHdH)()%iS2m^0qdB%_N+5JNVD= zo)fY$R!)1sQHH2#T`+5}r9B{#v-f~B&lF4n6U`JL20No-urn%#HKSr!Gb)BPqheSy zDuy+q;w~>n#VweOieb%YJgoI&XpD+mFd4PiKn#je&8QgFjEb8r88styDT$R~IZ_!m z&o1b73ByvX3>&fDK!s=6s8TU`h*I z$(So@eduUtTz)FfullVeB`@LiwM#oZ&&L{cTCTU238rckxhLHV&-z=EPo zi}Bd0)dFHoX;GMtv2N)gbGLLr6PXUmk43dABh|;iKI%3umrGOMyg}nsFJxGRcIoqk|HdsjZ&%FLzfC; zwb;IF3(AkxqH)<4MAK@~q-=}P33W70ZBaNqjC4ymnY*PNn#hztlA!vnRH}B$AI{XG zFt`WZf+KUc;Gl^NZd8J5v((@Usg7`?O6G1TP_kkLwWFv*P4YUKpJewk8mJBx!8 zv81)g$5KHS?@e;0w3gwT@3{`MI3UT4(rPi=scaVSOJp&vMLw1avUqorE2Xsz*L=@) zn8p4{W|UTo+0JFNcuyjWX)W@xRFK8KNv@REGF4QLUCK%;X$TX}4n8wQLJYht=Y{Wm^zUs|9@8r_2o4ClN<_3*1SHC$D-X^KnPQ z4}5JVrRTz(?QL#5TQYas*+LUph}#oX;j1f|sNU*EmCW6!LK8`~cY-Q>I3$Ity+@Z5hp7DUr(u}5l)LTTTSPimIac4TfcmTesgO=MASN~Gfl8L3|HMwQIns6rD-^~MBM zczJt@vvC zSXB2;rD_L9`LSB;liH#%xT~>igv~|FPwApFz$Sz&!-ULDgKWcuI<_O|o+xP62)emZ zC382b&_tGp>SZPSN2$rSgS#dXTw06#nyG?yWTHB(l%xKSl@H>%J?QYA%LRN*y{nW%o^MwQIn zs6rD-l@wu7h1U^gqWZBLRWf&@3QZ(cQiMhIC#h8JDW3{cwgv8b|vMsRZ_&`U$`FnG|o<(nKvTO>D zT=Z>8U?FSvL4rTLFs@AICEwEDN&LDjg_?tQ;blDa}(_#f*OeoXG>xn+#bU~RG>k}>T#ZG0KtV=YRTE59^ z*_v2|x|U|1)!5FS!Cp-`!j~(Rp}RKG1n+Au(_~Ge3EnqVrU@~%YBi&LlU0dciZsEb z#J8L27goK3lcJeA%B;)zX#;%`Ml>yQIF@#+(si^4g4VJcE1e_gM~+^`b{JQDv)V|y zkP4`KT>4GIqNzh%eFdH7&H3hceM>v>wB@%Vd%z;JWe%Ygp#yqKg$1-Ga@SAG;zX3IAz7{yB*B#{`! zqG^%%i;rSxE$cxAa*9BY;>GBwU=#zIxpo|vRgPAM8k$qGh>A^5v*t6fa7uhGdkz>pC383zO8S;1)G}#Hh^fn=)Iz zNx>+7-a!(qc0KusrbXmZd=x`#If|)3P7&x)JRcnujAB6ZvMEzLj!TbX(bOR>J&FyT zf>BIA*xIMEXA4-2;&~3C6``ML6hlAhqnLiMBUyl!i3(IMf_kaPtRyumkVOr@JWyt8o0CviktPN8 zc+S5bqN#H|ptY^_ZEcN0BB4^_b>g57E@Q9?)9Wg9=nW zXe$D}t9ce3Au8r$Kr`2l$GzUwh^7v4>0OPXQ?RR{AM6R5%APG?5!zIT(2CH{w5x%B z(swoVgB{8AEaJ;t4nNL%&YqG~70Jv>^4#)6i3(M|i6umR2IXmoy0GLlZPBzy{Ka=Q z&{_^9Dv(nIdMHhfD=vo;pqXpOap|EXnmWX#hmxUFFqG&ATYyydYypd*G|3^fBJ?v2 zCFm!8DA5mgB+sB_xkxgX!;iDpLcrIF=boRH*Vza45x(=;$x4jK#X> z1yaEULH$-7u#lRkOnP+nc!{o_p(`(BVdz*Hwcnp@4NiVf&SR#vBuaVpAo^GdqR&!S zWZ^;dIn!DaeIg;nBK0mP-lHXmPNA;I!h`4x(^?WGEqG*fObMb>sVlPZAUe~umPA2G zVsK~8=;#ter%_jA;X!nkX)TF@lClyNHz_=lCtCC-17D)qo|Lrup|zw$l`J<>O;HNepm*;>Jt^tQ5?V`2 zR7r12dPhFQlad})&{|TWN|vX@d1{ZZC&U%q5$&1=;0ynBR`mTz7F4|YW1$r@ILU(Q zRevn>P#TnEK_#j`7FwzUlPsu2^~XZ1^S&etDv`zF`SM4UluxV4g7Qr$c&iD%={V)M z)jEz@Njso>kS?O)lmVbTT25#RFUq3PauWVk!|hUc2M#a1&}UB!fr9fN^n<+@qs(Rt zQ3P|h3DLZ5auG!ia6W=Em%Odnp-6z%x25tg{4)Je{t5ylUUq=@B2kY3`+0roQinh(|tqW}S}mjF%o1p(3zc7UW=j|%op1~`%e z1h`%TG~E{jNI%#Cl4d;uyvqb=KDY7;1qg7x1ZcW12#|iT10>CQ1bAmMz%g{IDZup- zpy|FKK>EQBkTmNNV4s2jse%C4OMs^Pf&l3UJ3!K`M}T)E16)i20$eWvn(hk%q#x`6 zNwXdS-fjXkZ$^He0tC2T0yNzh1V}&F0g`4t0_<%9H1Bv`P60A(-rBsx!1WTh>AoOt z`oWHyr0fxQFB7-AoO9`oRv6H0u%IO&Ezj z(k(cXrjL2a!r0`G7|S0V7C&|Zm+1$G^n)E1Db*va8|$e+&`)|K^n)Dsc~ zAM74T6HYz6Mz8X|A#A*ds%d^sYkkJ!(6C8(Rkgx%A4i=4=M3quYW}M!|JA5X6L_r$ z{oJ%&6QD_dwmKy7yI0&T~&>043D5Sfs>HbV!w8ct7-xP9c4M$s@TiQ z{GafmcJ6YeYRAsDDrl9qk(FQQ*a??$tu?iFB@*@EorP|3pBSlPEv7N;!&^mEqKt-b#ug?L1@%|vC+biYT1064FW zd?8X_`OEh<>d-~gB7voy`qqIFA3HG8nMYuso4{U+3v&(C3t->CMAIUH6|Wfau>&Jv zdIYxB1ZGa%-$E4w*f%iI)HyKXV+Tgs^a$)T6PP(=Ka>Ik*f%iI)HyKXV+Tgs^a$+J zWMB_cU;z6DCYm}2MttnRNShvkZAk|976k^dZ(yRSb6~{B4ve(v5!ffmz&@tH0QL<` zG<6P)_}GDwHa!CS*aT)Ss&u1S5n$iIL{sO$h>sl@Y11RHkCK5sLV*G78<=S792oJj z10!vE1omM;U?fF=eFGCsodY92c3`AUkH9`i2G*S_2C#2nqN#IW#K#VdwCNGp`^msQ zpo#(P8<=S792oJj10!vE1omDsuy-jifPDiKO`QWHK6YTFO^?7fo50Lvu1ypez`lWr zrp|#8A3HG8rbl4!CIkDB0%O=*{kg-yzVU*`d{gImiH{vGN!BCYcTBwIa^BT6r!Z_z zM1Mr_0-Pu9c90{QI>$?V?08AZ9`U}N9xuSYfr+Nhfe{}&FjBHdV4ISGeL)oi*f%iI z)HyKXV+Tf(^$6@O6PO%&0Q&|enmPwYeC)tTn;wC^nGEb&8hQZx1}2(12S$ACz(|`O zfo)6%HkhOcuy0_ZsdHe&#}16N=@Hn5WMJ=8U;z6DCYm}2MttnRNShvky4jQH4rkv2U7d))+Pt{C^HzyS6QOf+>4jQH4rkv2U7Tb~SUD+LCyZ(yRS zb6~{B4ve(v5!gBtm|1zeO@RUI8<=S792oJj10!vE1ooNU|U#z`lWrrp|#8 zA3HG8rbl3}`bQv|I!6Gl<%J3op;H9K{PFrhqQ|$IuC|VK_2J_I}bEUJo2zA zH4luMOAZfE9su^ugJ|lU2g9cz5A=hb2O20Id03g62S!aE9-=$|?3)MC)Hx4^PeC5& z2RjcmP(1RmA~g?;n$zQbXnp|LHxHtza~=$zf;`X#_!Q)Uez5aE1H~f`%Tn{es5w#GgYp2dZyrQb=R6ob1$m$!>^#sw z@yNqVsd-@3oM*k0^1!e;+PT%hzUcvv`KHe4F?BpQ*N3JJ zfb+q)Ei*+^=R6ob1$m$!>^#u;@yNr{)I2b1@^BaF0bt)eh^EeYFnkK~KtI@dpz-68 zhb5_bVASNHC*=WP-#mz>&Ur9=3i3cd*m$Ge;62Y`L^AeuVo!SE@_1N~sKm|V>Kw44QxGuyUqjLyxAC; zGXnGJ15ER3i4pjbjHry+7`+jnaUMuHnp;bZz>j1^Wz5ECL(J#_Vl=0g7=a(jh|1{3 zXbx3vb}caiKavrZ(T&kYVl=Ck7=a(jh|1{3XarSlW-T!SKavrZF&m?ev5p=jM$gp} zBk&^`Q5oGB-A#;U)Dk1`BNwU!uxAIXTyn2nJ+Lc5EW z1XF5>5%`gesElrmhEa~5sU=3>M>3)^W@BWITJ9r8PuCJ7@FN*f8M84m#}5OD(d1fU z1b!qVDq}WApTtx7LYj{z)e}*Hka7>m}*W(b=;nCA54KoX^Fh zcN6UNWj%4GpGFf+9gdIm(`e9I=9>!O6hZt20d$0MiG!o@K5^;m&Z4P9T=5qKVmbxK zec)CGOl8j&un29OLuf_lXZwOcu}FY`jASALo;L{W!WUpbV&K znMV0()6^Ih;Y@CfO_B1Y-JzgsM%3)NjEY!6#Lp1z$5JC^G#*k9(M|+VLq9DK^Jv9l z85PL`Sr|tSdC>C?QM2>#1V&Ik52nNXuqx7ZnNhE+@n}+iBuIwe=(YoHl%G zey1j5O#0(Q>sMP4YR5>$clSOz;e9Q=IK!aU+>75E->C`xV0TNowz_>OJt(^gCB|y9 zEWHWl;b@(QNu^LeBW(m$>#E-~xPMI_H3}V?7#H9xdS(J$@ZQ|1d9$L#_ zMrIGj+-0|rG&UTgF+B3~K)Aav9Ju!xUBx90^8{?Uj^>x_tz6+=tnXpnYb~YN~#!KPmG}-$(Ur~#`t+!oeiod#?X&sOfqp}yp(c0u$~x0 zKaw%YBpYLMt!FMVzOSAbLqC!+$s`+Nb1i5tsp8&xVhsIA#v~Ir#%~hi0rkWf`jL!D zCT@&ZkSgw}C&ti^WK1$~WBeL1zPp|nLqC!+$s`+Nb1iBbG45YajG-UNm}KI{_zhy* zubvn~Kaw%YBpYLMH|;uN+_#<>LqC!+$;6HEqr~{GdSVRyNX8@+N5-@gy0e}bLqC!+ z$;6HETa@EI^~4zZk&HfS#0L+bGfq*pyLhJIw~Nj+|i7gOqQGpRS1iu=;c z0&u;gp1QYFPonY(xnV+BN>xS+!)Ux#@*_PG4vxDlT5NPHfy)(#Q3WESv6mk;7cNL<2#%9 z{-=KU{wKkgB$ADcX{L|u-zaU?PS*9Cv{+S?&x+qW5 zj%?#e#c#QJArY;L>QMl;sssgw%qepKT69d{>xJ1H>ix?pfdi;cwA?R6s=8WvPFu$+L5AGeImUy zHBy7hm<%dwdZ|;SXzdm$_G(9p!^tPouBnk4R7Tp>DJHabiwS$RV?y1!#dJT7r%O^} zy2Ob+TD!5wUTyZMMK|`3(>mkgRQ4A;u}5n+_Smb<9`)tM{%OknMKODG@qU0n>787!@V?uqo#ng@1Ul_+E*{MYjr|g0e`YHCGo9F@wHtfv z)n<=Ma%10<*#9Gy{Xd-8qqQ4*?A2zEO3KFG+;+W&RzH7FW&d|4_GsS5WRdr?T(t#2&5P*ki9Y zdsLDe`)i4Pr>787!@V?uqo#q=RpB>Rp|?9tkdJ@#s| zM}4`mf0fw(C6)bOoY)EWB(Sh|8pw)KRdBUYd7}TtIZyjA0+m_ zOJ)B%C-!LV#vXgM*`t!&*v}&NzfEQTTPOBt?ZzH^wb`SR+}JN9_P>J%wjyG4q<+L5B_+#;P$>`zIJ^b{xdXzj)x zd$rl4>fG4RAojmbW&dj@_Gs}A!Blhi6*|&FMkJfJNu~(ZtD#?xg78;-blgi$p=BE5nq_Y2UiV3aVV!~eSm{4Cn zF||vLshtyhw02{Uz1r+iU)k83djoAOvi9K4ovBzF*_NXKu_P{MY zhyAe$_HPsWW1ZNewHtfv)n<=M@?n2Wg8e39e~c4*w02{Uz1r+iNj~gNyQZ&35Z&qFb4IOgxBE`Bf5kNW?UT*dO6k5wteZX1f<0d$rl4zI@mpo?!nJu|M32JzBf5$6jsrs4pM( zhb7oAAohnju}5n+_Smb<9+l+7{?G*bDa8IzC-!LV#vXgM*`tzt*dLN$KZ4jF;=~@U z-PmKVHhWZ(5Br}c*gs3`f9k{@t=-sTuQq#Bk`McX6YM7udxOe(^kAo$(Aq5~?A4A5 z_2m=OPvV$1$JYfup;g6CoY*vVSiA9{i9TogPho-wHtfv)n`_TR?DtEsf0x+r=fobZ-PmKVHhWZ( z5Bq%+?B5{v`#P~lYd7}TtIZyj+Aolw> zu}5n+_Smb<9+l+7e(wbPYl!{cPVCXzjXm~ivqvTQu-_}e{&`}*mlJ!mc4Lpd+U!wD zKJ0&(U_YGL|Imp&TD!5wUTyZMBp>#BCfF|~_Io<9M{76s*sIMRmE^{MYhy88|_BRsy-JIB?wHtfv)n<=M@?rno1pC{G{db+%qqQ4*?A2zEO7da9Ys}uf z-R)hP?{{@#kJfJNu~(ZtD#?d^%LMx|H0QK*Vvp8t?6FsyJu1nE{Voai?-2W4oYN{a9kZvlDx?c4Lpd+U!wDKJ0f&uz#A^@8rZDt=-sTuQq#Bk`MbG z6YO81IcG;F_GsJzBf5$6jsrs3afuO=9-u?Q{c)eG@14Xzj)xd$rl4 zl6=@Vj@g?xo8Cq28#}Q_Yd7}TtIZyjd3^^8NB^;RxLu1-H3^clT!4+#kxHM7 zIW*i3TkYNoU%Xp=_77CG0tbzIhCy)E9;&AKIjw_gb!gZmysA3DZudI@R6&0=<-Z!4 zzm9JL`J^AWZ`UMjToZs-ZF^GgD&t{qu&z<&m+JPp*mJ9PRn-dGTm+PLs~@#%oQLwx z_OqO=RHpBm<@rC4Y6HD%AInuc_P0`5Ew|;(x2iU5vrTcQrt`*Mjcn}0#DMI7GaDZT zMYM4g*P@#LvQ*NB)zMU{qm@+_)K^{|d$T%zRG^Oi((`eY1KZP8ci47N*Tc4x>H*u% z>L%Ep2|fa~R$U6)O==Kq=ct_kpQ?6-?Gm*cY!|EF!FHmGRH*)IL=hu!$O?hN@v|xEi4z zQX|zU^{{$GjaFk|awryeT}0dyqS;j)RZnsMtwh|dQsUOty%jjNRJ~x^TJ?eLA?hyJ z95`Xgau zs(N9~cTb63YrQUNeq23Odqjz9*KfqXtfz>FI7V#i+fUiHn#X`@JE+HC+bLG|>G1a^ z)d9AP)mgAz66<>p(DWE}2dI(>NMW($9t!9AL;+@I$pJ* zJ=gDH-_?X?v8jo@mv5vv!)A+d9#}@OUw4W0`8tSnch8z;k8~kL`T}gXS4&`f9Mr)j z>aSN_b+j*1oIm>p$GMEE`U=H)7iCxM3WzFHt0=-Xu-#R?3fr?__Q18X zUN!3(Nw1WvVCA$?y$jogynZsP=A?S%>bd+3RJ)Psm~=GjD9YCM3KEiksVYdvRx>$KD8Rzu4*&&3nrm#I(HHYmfY6sY!s&<0y>8d4cJE+}Y+ez&X+s>*rY|m7?LmcbG zsFM5nT&RyflOt)1bwu(3?CT@gzZ^#^fh>FWx*Jz}LR6vJ3%0we9~Y@nbE?s(TCLG{ z<2`xpMyBVApBIT^X{HGF32_A;K|Zq1e*cYF`hA098So8?W%D;EmdC$AvApmNisi9y zP%O*8L9wj<2E}skHz<~8zd^Ay$z?fi`+BTl?)wJS<LiTvA&Hg0^0(wLMHrbsF^6QvCt8 zho}y)JzRB!?GdUAY}=_bVcTAv1KX3;`LI1jT?E@xRae-at}cgd2lY?bc7pp`@C;@* z*q#Zk)Ax?%y7ONl`*;UU@(kuFd|vFgp?yzH@v8!LIX13^SR!>DZ0mk+;u1%8J>3A@ zLv|Ht@Gm#6i}AmO z$92{{#dfMU)VYzm6Skezu`r)3g*zAUo|czjdlBpPj<6ZtBM4VIn!{D}gW-zgA#e}D zVW9T*Fk^LqdFi%rXK0aARjPq%sJ2s0)wk65)c4gM>IZ5swU63Y?Wguv2dD$#+QdOH z`~4ZL?txxAsrw50Hg)PB_u8mvzkQv1ZJ-{2{`Q3X4+C`@$JYkN@2zT;8d5gS!OlJY zcQrB`_5mTxOUZpec7@Ur3jd-?`~=Oya*fJ{{T!T$Ogo*&E|d?Vv~yr#gvc+Z!-JGJ=w zsin%!Jv-}#z;xI6enic^4RJrAmbvTuWo!5P?hC}-cTbmf_G+M(KtFq`cf}~|#IwE8 zXz@8~aGhy1d!1h_sPoUL&L`G#o$ErQ%L=&TkQ`BbIo~qu5?dj@6*mh7K z!?u&!0^82&GuWP~`aleG6dq?!R5!tPiP{@-)r04v*W;tQz2RS+gD!=C@%e~V*0J36 zVSUdaxbub!QH+1 zTuR+Dzmk}b`x-JokC@M_b>_P$Jxc3(d|m;}zv|=D);OMjny6;fdcJpypS3v<#?w#W zC@+8`MZCikj}~!;JPfvn(ixJ&=((uF)zN?)p^kxVdsPG5li|E4&Wd=P(J6j1trx7y zHmM1)U99j9#3ky-zZ^x^* z3&W;GpAnl1)@BW>BE8=ze*D=gTu=$)h>0i8ni3?FRJhz5Pj{fVRxyM zb2TSH_R_BKnHm&3*O7GvSoibpS(NMfkn6A89s|!SPgIk?hRpv=%tw3;nd6!MiE6>u zkomd9e0HrfZ=f20&)wB_u&q{y!3@8EXW%gUw)!ibR|?@Q!mhA4!M(^4s)o)xnrEy! zsa&)Jtg!Got3zQ8j?Y@Pr7IupV9k9hthMo(;orl4r8AtL+#dSblY217f=}@IisHwD zk2@YC)Wz|XJ`8jX=WJR;IaW&3 zs1fN4&yeP|)eO)xL2oUE%65pF7FAZ~dRX7%mBNW?S*_=+?)Q*?C+3U3hRn|(=8M0E z%+Ds~bH9endl2(swaz@hu5X!PUB7eWzOLU{tn2mr5$vhRV&zb zPiv9zPv2MfoOy)k8es?NyCbKU2UReix^D~f&TEtgNi(ehXa|=5q&MlO> zUfNmJWj=1JgM8Hec*c9Y@^5ZP&X-@k`RAv#%C~{~6^y_Rsy%Ew@u=$xS4D6yaUy82 z+!0e})m9vVwG``Qy!XQ#gXIeRp9U?<`Nt!&Q{dPwe~eeWhfbhx`-sYQ|N1+~!YS$x zusv1%3AP>7pJCfc-DvG!|5`HtKZ>uo{KlG}{=55EeBVRu>HE9&bpAHWncIIuZ3eKW z%$eK!OP;xv`lyTbO#71h`0C9kf2w7^b#W(v{a%Zo3q4nQ?zissfKRE^-uRPzUFWyX zFk((oXTbJU)djX4)IVU`N!@JCZ-JY&H14&=HFdmewZlUgJO;+zxwmxGNxomb{RKV`RGagIt>3!P!xbg@e)aT#&scp`@|$IShR+EX=lj)LpZ%pI->=?y zYg@~F>+bySsl>Fl&+ppvLtX6O@M-tr`u*zZ0iU?6O}=%pKHK?gygvJ5E$6qc^?N`q z^R2u20iR(u_lKMH`d9h-&&PFg{h%(!Tg$KUc>8s&>NkJA>YS+D^PgvxxL&?zktaa& z{x2x&|GbL)UsBfpMHTtKysZCAJ^b%nCwJW9HO0C*-mi=Ci1%T9o%rhW*^1^n{kCQ^ zKY#VlryN(S^GDt2SLe4$9pty%ov3HRl~a7%FW!lI2;3E+-}-A`1;sm2za8I+TKYaz zd9J@M*5ddkpxRv9+G{V@a|>DSZF;%nxv{U_y-mm0VSejE4~Kuv_~IL&zAF07a;_BL z|5BXqSMS_d`ZsEPpKzD+nd|(J<=oglCG+cgZY;m{yt3@t?Fz58n>}CF#dyXy^c2^d z>$#SEK3ZnFH*M$0_1-i)AFleyvj6yg$$WhE_8&hf$=APrSC!TCzau=Sz^K1NV!(xj$Z(`=b?bA6M4<6BT%Ws;u`36?lK9toO+kc%N3*`_u}&&n)YG zMg`vImi0cn0`KQ!(EC~5dS6hM`}_*HFD~nSQ3c*#D(ih|1>TpJ_5N}N-dC6PzOn-E zua)(_wgT^Ol=Z&80`G5?^}ewJ@9&oN{&of4KPc<{y$Za4Qr7!N6?orT*88Uwc>l7j z_iYt;k8>5S>2w~^4{C9Nb-$1p5ua3YM7h#)!>deG)BCHkg zsj%X)=x5HdM3RrARoOUpca9_Peb2Jq_o%@8K4rb{Re|@qc%yMXGj%)PUtV@ryR?E? zZU3@)-?xIiUzuS|e1-R#_>!`jxu}B798@+l2Ud`oL&|zTxB~C@WXOB}$mPAmd9@#w zEB_wRBXWMe=VstN+mrX`489-s^!-5w-|u<)jxyvs@XYs18F(-Cj3hJ5egneS&Z==&HC-%n-8$pp`w9G{W* zFFbgkn9+Az58tgaXmEE=4ep%5_l};v$7QJH6P~r)Ap`Fgp1iZ2tM=QcZOxFAPd#(e zEJIG3dgi2Y2H%aGeCOaDwSk(=?=$$HoPPI4@uMJoGsdrA{A4)}!lNv6k72_M+^d|p z=Y5}?!S^XnzH{n#aFlo&v;R$|yqxBdmu%;#U38vPe-_;Tl!^NvJh*qvkoV3`dC%AM z-3(*&ZO<{fID_v+p1$A8;CrK|@2fKS{uHrpg`M1vgBey;5?UQev?WE4ieXHnuXZ0J6&A$JFT>j0r=j77fYRwWeW|uA5v` zS~vN;UzzcE?h4pvWP4Lv-uE3Da?{%>H+kRx&af`J%5zdh)(H1Mh1+d9TjE zd!;Auff;*;%D2H!J0eP5R$-#2*X`^F5uZ}#+kO9tP!dHTL1(<-sI$11UJCeL?z zcs@RpXFQ|jdenS$CeL{O(A9HICeL{GFq`L{U_}wAzo>iQ*t4a&7q+{q!LU7CJpkKw zYB+4$s|R6wvKk57U#o{@)9IW*Q5443$j_`BC;-CeLdNIjRaMD7NTXVqoANbllqvp(nd8rUZkow+V-L?QGI?(1;d#eQo_EOX`8)B^l0MdH z3AJpgc8BfZYERg8FxZ~1j)ZLobu?_{aZ>9l zd1SP66&~|Mu-@3MD$6mF;iiw|Dqri^$8gD`wqu!Fw&S-3>NtpZclAryR;$5qT$ukR zUk<*5hWGFuOyAl&Qq|DeY4e3W^ToapjuQ2^`i@dRgYUfl9KP~;Ebo0gsoIXd4KUqR z9cdlMCUcN2^4jKg0`WTWE63|3;&pVzyc(#-z(*(54*YfCqyFE-U-UUGY8-3ja3z&; zO8i|Bt&>CPtBz?(X`tGZ=Tl&tznk3nKe4EP^WBni{%gD5r&C_{uZ6tIJ4VW_UVmGK zR{?PTF?assdx<)!-vgeus;%v~|44EFtm3#EiW$2Tc->w76}HvtdhoqKb*O3*$BA!Y zI$V6A@h`CV&|d+yzX1Acn9uERfbJ0YP+Rlv$rZ=^?G#3S=kfjqnQ9lG$164e{jKPk zyX2LpgCWN?>XKYTy7Gf_x*n^!Xge6$M<(X}CZNEyQQS$@UW|Xw7~|iI#+Y9t?Hn~J zGCy@t=8Ao}83bd)`-YlBZff%@=jIIRnXt|XRU>7-f%7gLj}B2o)i5<&jZhD%k!qBB zSUsXft1)V79dkK5!DaJ*k;}OWF7K7(g1W}@=6WS~K4|DPbs8VB;Z^tKZ1Nkp`hrg? zm6}WBZ1pFY31+K{tL*a(U8|Dk87{2a&YG$940c%+ez_LUv$umasGhGnz_*HR&f5F` zR+*#BG|vC5D$iMZ{TF584<$44rwo~pUNzVMl;qki16O<&E?>2VCH*yW_V;$lsNc;H z^>*qS$lsCbde|NdTtiS;M|D9iqV;i&x(NPiPT!vH4ywq{jOXOG6Z7fe@HWBo01M;! z)SRViQKvJDnNjAK967QEGfL)cW`>+ef4?Z{?|5f_>r483!`a_4CH>Vn`@5o~zbl>n z9aYlb(a!$bmh^X`v%eimj@ccZ$L!RS{HA5#SLz$#Eus)c?%~ROD}0Bj%39$xgEbGX zu$x;)vgQlo2h;JcUK`cGyS40jt69k$v~bQrH+(gNcDtmOLaSJPgH$i+dl#UT%f|{)wgPSz!Y;Rz0DfRO8fmH9^&09^ZlW&=U0#Y!@>- zv(H)Qu+RCZWF9}skjL6yq3;11AEWjZc{h=b44V{LpHHpD+)kjZ>I|E8hhb29C0u*; zEybbqkwN`%D1D?P=bzanH8wYc#-zUqCH*~>!C!4_wiRi%wX8at-sB3~Lyvqq|D_>zBb_{p9E& zha62Vsl8`1XwR%43Rezy(~6=t)(^`P>xTxg`oJ~I09v7#eN%6~B9K$N!L=hit9VNE z9XbleHFYC6|58ooTg(~8Bdz1*V-;MbCx3%so4*cVyq*YOtTkTSI(aR1ovRV>ITV=Y z?-A*sLU{8A-UE+E0ea1U7#zFT@an1)FntL2&7IUx5&vBrKb{Y=eV$U_(HupXvj7d% zFCe2G;=InKRh7xBIZ|m6MR8V}@|6|LDW1Vf&!~MCG@ree@JKu^LmTUqx@F=UTw_n6f?rFFMJ2iitGemgNG}v>PHaYv5+5KBa^Yb zj|i@O{)EoJ1XaF1&SD<_@aFLh`B16 zef0MBaWwll%G*Z``#8qi$6WR?+uO(S?Bf^SK2BsGZM}WG%Rb)r_HiZqxWe1V8|-7f zw~rlpe%`@*er~}&ntA(Z%03!<`)I^I8hZQa%Q?N%JEsfS$9!)eU$T#F-afXnk59dQ ze8N6H^7dhCeMes1nDra3w($DPv9O9Owu;yp&iu+%8(tr7o>&Pc*KN2u8bzz4(PDKp zK3)r+7|AnnZQ+T{Qu~=I__|a8dmpp*y9{!pSJGkB2(CiquP+@7tHn-m)xL2YZE_`; z-`CjlvtMrP+1bSdUNO|y%Pxawj4-=7$cNnT;`P=1J=++OeyUlXZU4Xa&I3SeV(s^n z(k7Fb{vSll8a0(R`Z>#-aQdh8w>HpGHmkYd4JvG-$F z?r&x?ndI5LEW7vk-TQs_X7``usq;)tGAVC~T9I<;nfR;3pFV4B!<(`CT-IyrdhV-6 zQ!cuk^*nyD8c&b)jmecS+4M-?7!RfKobTPH<+w6(ZDz1&GlLpzGxy=Av-*^)hMSWT zdVo3NEhve5c#_hd?PptvE~jjJDp3y`KTGg)SK{Z<*w2HF@MGgyoQdc0*w15)@MGh7 zFB8w4*w2&8?Z?;GbFu9+CE2S|e+5;)RJ`>|vGog$urA?Fak||e8MoVUv~t}#vu*PT zo;%NL&82D{HPcDWH}%shOh2m`GHz*4e)eDgLR~BKl$7?a?-T46zyC0l(W>~>a4}Er zS}c3U?8~LSxdKf0>JD51E^CxapH06Km+Hbslxjh}MoIW@3LR1?iO=Wtm~aqZpUg{r zNOA>VqTa@SEA=kxt&^7iW*o_4o-xbz7<=@{_Lfq$4kOYq^*%oQQ;~DyaeokHlC71& zj8u6gS*kuX9+t57vmVaRfO>v*E8@r3#}{!vK5Ie``6jmgstL9~#J1lx!S?6a_QxjJ z{vO-@+5}rwYDdXGm)o|WPBwSs+^AiVoV|?T4T`*Wm+j-eo(gf=Vd?UY(B8Hez&p5tVgT&DD^Ac9bL-%^`yMz zeck)TylX*ihzCFJ4atmqgBu+8^nAKKA9MK}M4t3GmColdrN^rJIn|?_v+ov{_r}Xz z-rjce*tUBUZ2QM;#C_AU;Wm;#)^8cd(6e-T#(LT3_e%Qw9*ykt+s5f`-GsdF7~5{& z1lvBb?M_Xw-6gi&xe2zr##v09Dd%2=10UZ3e6R@zv8Z8Jk@pRuMXrDQz3pj$Bl}6#Ao%{(R8n( zX%Ekg<_G0kA5zqMWX5`MuJw>2){VaoXkpG?v)2GJ|08l^s3;PHZ!H6I?RIO>?%%m~ zKQ(CAjAyayAN5-B9!*{w@o_$to8Hq6rgu@U-6cirO4TT8cBrak?axe97^(&83zc_8 z(7ayV_?SE7TpzaQ`Y`WWBz=URlpFKpA~E~)7UkN#+MwOcT)T%FwEH91ZfS#dFXr0K zZP4zQT)W?j*p;f$<|=WlsZlphxVp@{N?cZ5_RCGXf049(ot&3zcR_=8FX!4VXwdGX zT)QO=+P#-+_d$bp&*s`aU&O9dji)v`sR^t*tMfCnDTR7-8G7C|6bH@(UR z(|aV>ZdQYKL2jH;gK@UYwOg@4yWY8R_H8iE9dhk@HE7o%w|%xR(mv(P+CjXrV&}2^ z?Aakil0ACpS+_zwo8ZQ{^fgjjw`#v@|n2?%o0L=Ysl7URt}ee`)Q$)GDpB?-aST zPvYtA^ju?p+O3PuI~+@nA~ltD3)Pmn^JK0e&ZC@VxKGS7l+=6I$(D(XD?eAV9i->S z8fpj2nwxG$ooDB`oAYE)_I$Kd{m5uOOdUtw{hX|8N%ilm)Ne^GGV^06n39}gX2}lb zI6hhZl$jqpC7BmHDR*A1R2^!L{-=jZ0QnYt9~P+iWtPwZn`%HWfv zhb^y%D^ni!X_N<_>%2P?c}MdCGoR_-Zb-M;*2&l<$1RuFt5T8f->69CXnP3nVCzxR zwrYDXqyG3?s{TQWvWBi{WDQ-Pih93BMlEL{Wz>1&uPo#A@u7Gbm&Ws_cFujFnK^Z1 zbwe$wMbQy**>BT4F2=)xx{3+nbvjPj0HUW`1sU{bo%q}sUj_$CNeE#e;j&;OEoS&Q*5{T5Sce2lY^ zf7YlNvhC`rM%gyCT|Fe)l^>@%Fmk7l1jCem8YMfH>wCBMUV_ciVAl?Yh3K zQR$Y51Gtn7P_%O0a2 z5M?gQDt&Y*URHMed%OvKqN#CzAmYxJ-OA={{jWa0tt8r+>}yXhcVDyRH>U~ZSE`=D z${jyGY*d@c+Rkj0ZHao0bR#h;HFR_>Q7>TCve8yAHOXpTlOmkoB&&r@vU;USR*SLP zSe-(B`EmWtWITT@cRc6L7FLZd`F3hL*MXrrjdjP&vp(dD=W~PF5Pt}_B z7OE}lEmd#Y={rd~^|7R%-z^r4*Y2M@-O$fGMVoi6;0^IYs_|PFEgs9pPg*lc)A)fcT}6w zT0h8Xt?748ir2UsGtzIDG_0L|kECJk^cy1$Yp350X;^!CW(0gMNWT}@u33X< z`*yiZzs*`qyF_&*lEz+L_;OFbL0mkIb(@eziCQ14#vb#0Wa;-ri$}I$lOo%=NmkvO zVCD0ZerLQ`e!O=2Ew^IYZ5n+pSO5IYwYBce?Ds&kKAU!|bv|$7u9Lol^Plv&c^oId zNoiV_ODlau$$MV>KN-W8e?eJo{TKM$*7)qm=wMr}E2}mzwNEIVLedwV!dDde$r0pby9s;4^`q_CEDQQ>It<9 zPk7E&&oCY}-N&!osnXkjeS|SQbSm`RtK_fs}5mZKicI< z^Dr#yM|)QiZL^#j8-|U)+aAVs{AFA}c^1QSEpA9G>>Tz;v_dtCb;R!(>E{@p1_-Gd2$*MK6AH}+pI+}H7)ym8W55nK{Bx-k$!>0I#NpXYR6iegxe2y`G z4?t&%x`Oqwi7)qtk1Io)>aj+Ds)^$v2w|Hpq3f~TJF)fUlrDqEbIZWRaHC26Cf2@AUP|i3ru9#g%B`$@oy?2t#K(Di zvve7i#5H-RvAsKEyEv)ObewaR8RtEC7^v=J?c;nSiPN@=2arbUz)T&waVgs_o+y%5 zx@;fBTVC0wTk*ptDmQ|;y33|DZ<%R5YEpTOwJ+Pxlk!QY^>~rAY|Kv@KXWpEo=*Iv z^Yx6;bTRujW7B%hr1Ao5U(R#la&}|g(MUV1msm&Y1=DZ#&a``fuI@^IUNfDZ9*;`Z zeC#@^g{;S@S6El7jj%pJUCa8oq%D^yJ087;#!&SNGT)D%F!6kmIzmj$?IVk^-CMoM z+L!H@NtvYE-s~cEl#b!m+!$OxvZLDDq*cE!yS`vYwRcTa#mfJE5lp1Ms*1mi`OX9J9S{j|!8J#~9owi200_$OF<%~}LxbtL@a$XJFz18Z* z&l;?KzT{Y7^R+fQ_48Fwo#A&>UFj8{C%xAFiZOQzNj*}1%^Y<<)*aPqXnvY#mc+-< zu6XI7TB3c08jr@bl!o=Oo$;|lu8(!Gj@0_BcUBv+9<93I!@t8@P~G6Js9M6Elz85_ zxqjwrCC(>o3cXAUy>n71sP5Q@sucUhiG2_3Bc2Q_QClD#rnX{T&hKpnY7t*GNPm5~ zpteC)tdt6ByJblIk#&dW`pr1|4L;B9mEyXb^u}MP`Wmm<@-C=;M%FHqlm2l|oHU!y z0n3!`mXv}m#Z62pZkk()Htn{VwD(B7cjfM53-yW_f!w(0jvUP58L4e2cNm@F`0?$; zmq-V`N~w=A`dd7%MD{Yd9++DqXXV5?h^Vy3Y`U{dx+62`x)e4u`RE`#5 zg4*B2mOba3$90s>L$-f(H2veq-1HAJUJl94p*>FxjCK8zUC7Aij={C~K0(c4ojwMq zul-gvUb4}as6$D2m?~pkZqB?HsR^P7j6f#)Wx1*Qoq1284`14l{bN*82exHZ;vrN= zu&zIf)X&3*A`jNbIOC%#$45aOX}I~db-PW8Fn$lgfFtleZ|w}Y^oV(g|FyRC)Y@kVx{k?9$qk5WGEUBQ!$tPipeQZn0< zrX!2^^1L2#eVo?rv|PJV+KIc*oxQ7FP-kEfsd7uJ z);nkHe1E>ExsFede4f`M;T6;+#`D%k^?7P0TYlHW4b=?RU6ia#`~1tz$U3Tir1yTR9BdD}#rXV6__S^A zHe~$DC3b!mqo8b^+<~lxItZD6(x9Mbn6c}cv+wq(nnXm5-%p#29{Db)fW%Zvi`hO$O={mKt z=ZtJ$A!|*1tEyvY#gUkC$);1md67@&1>)-BZT8Q5Wc{4XH`$emrD5p+huGrbV zooi>y_g!R>`jfo*8LdJ*WAna|$u``blv=0{k%jfzu=Tmb*j<+K$^0W+Ki~AJIa+k2 zeS8Ujb@estG3p#_rV@`EhizGXi>#xPW%WH=X7^~}FDDj}`nU1+lkw*KMCuplQ2oYw z!$jA&qCd=WO~21>$J+ZctsvrSSUSf`vG#8;INb*`y548d!rm)#Z;^eP(R18yGhA;S z8EYL;Nei9JZD>R3TsDK-L0v*=otTp!XIZ~b7m%aSqqy` zbz<#XT@=ru*fTd5L!`Q(Wq-XhUC-;lT|><#N9oeAJ$_Z<^f`>wdgw-K1J*uzAJs-l zd(1nsd%aDP(%2MFE!09%@n;-S{9DI&WJ)(u)@Kjm^k)mMhb$4bwi$khsvfMBcRxwf^l9oBm#brh)`pzYh~=`tP8xoc;2R3-=B=aX^mh00zV zxEcyoFTAB&fWBr-mvnFBeh$rD&w2a4Lf#KLZEr_nuWzpnJl!9Ce;<-xFA}sp8}a(P zA=j_>dqMls3)MjAp(*=8(CPfG&T$|e|6t_4orlUkx$V;%f;^qx zWKn+I%#q&fZ%VDF^LqgH9a8bwSp=_t5b|_9hY0!z#_zPhLy@QHGC`k6`sw_ZBTv&K z1igwVj|$}J{EmiB*GHw0A0hmW6Z$I&|F*~Y{M*sX)Ap%nPgmhDo!^0?JPN|Uo&WLn zFK|AVF5e@CzfjoMi1;}%&)C-?PnVB<2HK}L5qUa2`((A3PePtVKNhKVJAh5jtJJ_6c{N-pR<*{b{x=em z&w9PSejhqrz8$bnm+x)R>HOaz{NDwwQswn8A-`AH-!JGjg#JuH>t~SD`F#j^y8Iu3 zPM6m#LC+Tco)Gp=3HoX1bbUMvozDOB(CPO1qR^i!^k0Tfx5ouSe?6hU2s)kLR|Wk# zbo%)9hOmDNI$a*`2>LzfbUYtGr`!8S8GWjjOrtUbHz-KQs0AvS|~LOtk+VhGr+gtfL2O90@i7*)M?;r zFr;i)s#8}%mnS*D^&+>1Fcq9suElamV!e&kPpzlqf)hCF6g#~Qs;ojz#3~( zK41aZd@b?{J_7^RCT;L9utF!L#({r=W}TH931)+}yHH->R#51w)KG8_XuS?P;9>9u z*kxU%t^_}Tjn_jD+yh##uhdxZBv^9;;sY;$bvLB$z(UY%Bc)CPZ-Om1R_aXfA=q&f zr7i?tfL*%bAN&CJ+*GOS!S7(d?!*QPn~^5C2ejUt*ucYJl^)oFT5vsB2)gwoJ@6*j zatqP}kAsd|Dpd`h16{XL>KHH|Y`nEnCxE*_%Wag}9-I$810A+iYF983+z1wd!gfk+ z0fvL~z+>QRu=e&!?FEhnw}8c<#SThs3r2&B!BgM|u+EN3?F)_vcY=37+g?ib0%O4y z;5qOM*l;J>C^!Y&3qAxZ_onTGBf&LbE>L}x+6;^Y7l6mXx1e)hr3Qkj;5P6kXtgu> z1eM?t@D%tSbm@mKm;!DAuY+d0D76(B0nP)nz?Wc+{z~lzCW0Hl0ucO-7{H<63@{UX z3fd2#Uw}Gr9asQ@UC{-Hf-}HOumrT<4P8(Jt_5?!A7GQ+>Azq)xECw|9rmD|gF0{> zco{@{D%As&fwRCv;8W0kFZ_ZUa4nb%{s0^AP5FY8z+K>N&|{!dhk-M|1K=aD(mqP{ z24lfxU=H{mbRI;#gKNNxp!2?@0d52f0FxqWQ*bbt4ju$cz$*JG)fbEhv%yzjjUlu* zPzRm>EACG{gR{Y6u>JvzL*NuJAA|=gwKtdso&jwSqF%vq;3n`D*z#c7I=BHW1WUoj zhcI@5Dc}aM2(%wc{=jH(4!9q@1DYL5J%IzjG;kMq9R!ClzJVjaZD0{-SEke;FbO;Y zmV&;+lsXaI0v3Sw<)i~fgLA*(`2G4^pLAz1ZEf@^yz$IWNcmw!|V1B1bl;2dxpcoBRKT8vX_8!!aafb+n;;B(MwJpBgj21bLE!3^*;_y|N* z^m))59113b%fS6$5%>vom_XftgTO>^A@~=V555H}9LabH_66g?>EK52ICvZI_^MhH zYzy`S6Tq3^Ch#Qq7=$&9pJ02iKd1(0gFC?s;1f`&rEh^9!GWL_oC|IR&wvkssv|A1 z9oQdC1Q&vTf%)KDu=Y{34{$J;46X!EfDb@4QK_}Sj^I!*6CVyBX|rf20w$9CUGnVdxL6lF1Q`c2cLoF$IynsE?^iq7F-S<1@D93LC48TZ4LGV zM}zahonRsO4y<;pQa!;wU_3Yz%m9yrx4_R}92UXxWa6Y&m+y|ZkZ-P(2@1Xgq?bS z;CygBxDPx77K1OqpJ0VE82`YQ;BVkSa5$I*P6tMY6&Yyx%!dw`)}9GD8u0#}0Dz@uO;cpH2T z{s67crj3IQz*e9i7zE0|I4~KU1}*{Df?L2$@HAKiJ^rJ&6@xC~qe?gF#Gi{Mr85%?Yi=hEiDT3{2f9q11RgK|&>rhqfRWnc#Q7nlv^g2mt? z@D2C{6wbpR=m^#U-NANXXRsGI5R`+%K@FG+rh{|A72rm27kCgn1?GV_z{lWg@H2?c z=NtsI2c5wtU~AA9>A24lV-MfLp+QU^aLGECTO>&%uviDQI>9`2g)f zC$It73~U2>g8^V5H~^G^(Vz-U1k=EDa1OWxTn%P`JHY*57I+%W1+Rj4z!LB^_z5fp z%`PN;&>nOG8-UHgHlQ~c00x2sKp7Yfs=!1r4NM2;fJ?yDUtCDtHGh z0bhfkz*5lcBGL!#K_{>Q*bHm~dV>LAAUFV&fzhA}Oa#-wbZ`#11Y8YffIGnbU>0~9 z%muH4cfb13(!V4XVIIFbzxx=YUJV)nEp= z1Kba0fv3S-@G5u*ECFAGpTJVk>=M!k?LjB70q{$p^k3c*#{4MW7UFG*3RSZrHrkK= zf}@_NTGrAA9sP?yZdp<2uae`x8C@K&K?yRQzD_~MUc$B%Xu26b^|sXL=sTu1o`Tux zoD}$Dt^qBVA4t2Hf}kwqf0}Ri9mm;aO`KHvsqeW_oaJKRO*|+sn5i};9DKRtRIp#{0}X;&X=E= zjzZsU*8QY}{?tqz%518A`=t&0?Ra)}MV_8rIr;AJ>fBB1$lc{N)mmz8o*mItSGA5> zSFOjBQX4QAx)EOz*hFOVE z&iJY722T0TP-m*M_zK)P>Rff6I$vF&F60*{E>@SQOVwrSa_((kscu!bt2^TLb?SQT zZd5bWP5Ahyx&{7iMt&#s-TbA$FZZB&NIk3`QID!w>M=FjyqWo=dP>bvPxD^ov+6nZ zJnvd)%vJNie13mmA)^aOv9X%ItiDSmdMD+<@V`w-t)(rwFX3l%m9D`bv{fd88W^r53PMP4KB}{n|wrZo+8HF(7@)=`@TKa)9LB`Zp%pb zHgd21FDo`JP5abGkUafm`H?YdA89{2jy#|LRXkUgZkv~`K7Qu$(>z&6TBgfod7#VK z>A8Kkb$q#PNG|moqIF-;I&ND`|Mpa@TF3RZ<=MKr*ShKbW!dU@oLpZm=zX_!{PZ@h zulYKiwA}3%OTQT3*~zU=PohT-PwGh8+32m?mpaAhJbmY9Mb7!$zTU0?t>J9+R{wf{ z({VDr)qT^U4La>&r1v#%l~~uwm$hw#df%I+I&1UmbY|;PhI?`kJR9 zZ5*v*%G=jGU2d)MsD0^ur{}h5TgO}8t*aBgT`fk(I33NC&@!D5$#?X!Y}+rx&e`W} zH^ffoQQPbNblzIWw3e>{w2s@?w2nv94o;b1OgpU~L!PaUL+k6U!^ZKx^P_2LD{Y)@-qzV_nckN=dA`y- zXCwWjY3) ztxiiXcH1J}wlsFObXw-{oW1tdBG$G0*1y{~u955Cp>b(g-9k+xPjB76 zEA6zN-PgVy^eDG|Otaw^#?vy*ae1-(l6HRbwoX^e?4oBFZ5ldVXTu$Z)IRqU(9trt z^?E)Q%hSg>zohjX*ZFX?-llDxy|?vcWNp&*;nR11bQ{u3x_w!WU2DHPo8z&i^?rKk z8xt;$v@DO8m0Mq0uUq2B@^rr4vIBalAI?l$+va#8jV{NC)*~^?iXJ`G|G#pQF z-J<0VX&!gWl0NA8ew+2@W49X4pX6Jqvvpf<vVK{>BZHx)je?Pbi1KEw?0Bg_M5=Wj2~%-_DK zUNZByFURw@mnQSKbc+>N@_c4F_6u##%+p1@R-({BfaWj8A+sxm- zh3#X^0bh>S*O(7h!7C~ZqTo_h5|jpWnLmDsS?PyV>)=6VhVSHyJ5TWb_tVTJ->p^) z+6Vtq_Y(DsYRzD+;2zZ}xI%Rao>%wtO}+Jk^@9z94TFt>A5D6j2Hk_rg3W^-LC;`| zV9Q{uVC!I;;3u_Ruqh9AZOVEleDn$W20I7+f?b0C!QX-b!LIOk5B3Q54E7544h9Bg zYEYp6>d5h+-~jdx3Jwkq3Dyh_#Yo+ICibbnI)$X8$D*D3B!id&Q7-$-vQ<6}*u zxn{7M(XCUngU5p>f+vHgf;p6L8SSZ3Jx^|4G-=NZUJm953xb8gqM&u~Dz>i&i^U`g;v@M-W_@OkhBseKiE9efjf8+=Ep?@WntR}_mf>h>qq zpUAc(wpvnK%j94HasCZ|KdKEWiN54;D|9!bM0=8#Ey#<``L5*Z2kN(y_M)(kf-Yg# zaGh}7aJ{fK`gLlfP}dbT9CqiglGM6~CBc^L^P*m`ZMYrV9l{;q?HKkB`-FY@>xW){ zcmu*+!`;H&q4&hww&B3Ae>jKK$v!(+oKp|7Jp)aXX!Zvc|vVb9=V@zwH78_52wx88hYP}m z;1%|s313HRPWUGD9Pl)}7s3z155tebE7XGUlkijHXFiezc$$O!JNC8S0%J3uz2Cy$ z!#|KMW$KaJ=bPnZ4_-BZ4z~hHjTPRn?;*PJ))k`7SWc`R?*hcHqo}xcG33H4$+RR zcZzyPeWJe6&QZT;m#9C|O4SX^-OY9zG`peS9sXdpL%;#p926ZK9TE+NdstK!4U5X7 z;ZaF2GOCD1MWdrJa1W1;h{i_a;8#TxqLEQGyxOP^X?1jTG$}eJnj9S)O^K#reH^kA z;GPtn9GwzPk4|O(^ymyUweC6S9me*8=)<=;G*-=+fx2=A#>^=ve5ME}NU z4tNGU2QCNKfqCF6u#ojD(W~se9xW#Ai=(%&xioq=dJo;}q7S2wST8~E)95oi>v*n4 zgoU@FLZPIv1kbNWEeb6QtqNB~Z3=A* ze~(%g{=j+$HYJ5`qqc=l(M+E~(3u+e)7oAqz3;hy-JVUaeyW;7V`g1n=1!;5`TT!x zzl0eEy|my=xD985?UEwU^8+jMl?=V;`6s#P^FzC=#ioLZ^eE6?bHsO5ial2n=`~>OFzyVb$TrnO4HV_ahJ225Plha*+65Q8C=w$ zpRDV1@wk^^4dkqOuo}W2&y^X@K|C3u;+i!Ku{xBq6StIe)~%NcFd9^H);*51?IXF{ zU-uW)?8MxfoutEHX`TFKX-nm;wS`;set$!FKYs71M%HX=bvYKrmgTG2bE(<9#mAhL z=GW|GqxAo4Ru_Y>Prujrn!V%ylA4|J|7-R?+-G0-zocfT{ST;Fdrwra?c(T|%-MKd z{fut2cr1|{CY%}%dUskg&e=dwEcW_`>q zcDv25*^`a%zjw{*D zx<2aKw4+_!)!+V=efIrg6+jsOl~+Bd{ST;F{T7n_vvruI>es7&cEC^0>3!R0on3m{ z*gjiOKmTPl>toL5DZkI2@jswu?HyP9=9$ipqqF%>Q(o6<+nRND>1|_cHdGh?Wi?x5 z0z>JVJgSygit}@RMU7yZ;W`73vI^A*`cPRDpFK{22$$K%gxZn8{ z_c5R6PUg$p1$~wGUEWgfa*uR*myfxJT5S1(cUbhX&%UkT-yN~cOY61@@e zUK?h??VnAf4VyITr>wR0GAgoqcHTJc#qBqC-q`xBpQ7a~U(M=y<7NBF`ahTZ>-Jjr zf7TrVT?+2c=GfMjuib1pXiDqr<$p@en#^QYuKBw|L*N)_>0L($%=+uai;om^KQn@O z`SC)2uaI{(NqYT0LVmNgPZVA@dkM+2IC}9kKYfy+n^`(h_*D2mU#q9=ON70>|CyNc z=4Vzv68a|yxt-hd_EW7qVVrLET>0{bzn~`x`a(g6B7cF1$DSg3|5JtiTedt5SBLhi zp!KmkT|Vmy`A98J+6P~XvGtkwYcJxdvifGNS9@502|d>ACFbfLA#W!9KQH9_3;JEr zzFrsfVnN%dp?&&iiFj5O`RO3&tu`XfOv0eypR(g?JAc~`^x3h&T{#*w_HB$E|*XH%jF+e z#1HK;SD$v7%cp(j@@c2JeA;U+pLVPHKrg;L8UJ$mjDxv+#=~4b<6HqM3yMM)`U%|6} zlQB|1*_}3MQhvR>4x||2(`saKg zSD*8OTt4RqxqQwOa``6}@xysTuKo!{^f`~n)#rRd^Fg-UWWRFdCi|7+%YNnfvR^sA z>{pI2`<3I%e&zVGUpc<)SB@|HmE+5P<@lVhY%=|DQsC6Cqzwl-D{!KE~Ml zar;9fH}#Cfp{w+Zt2j8nP%Ym1Zv?J`%N@k{f8UVM4>F??4a_RUP>u^#id4AW;%uZXrt-vpUKFBg6tP3iFUeKMxU=u3V$04`9?y%u8=Pk^2dez zK9RniDfjW;E95H)`&TpmgYZ2g_w`_Db9_b9w|B?CM%1(8%l7X0vb{ULZ10XQ+q>h- z_U`zyy*s{a?~X6qyW`9D?)b93JN~sr%0aewr!U*P@`o>-;wDbi9FVKPcEB8bh_apBXRLL+P5Do=L^Xwr`wO?^CUEU z?~m(L?KfrwVN4sYUTvRg&q4TskvM(sP1*PpeVxCwzxOR|)|xNUbF`C(Z(IE&9!vXv zU}?UJCB|`=zu_ixUf=PFOXu5VTwP`MJbi_w<2d+2Z6@D*F+LL)Uluh7;0o!DO^ z{Fe*+ON9Ny!oHVCe+%KSM$o$n|I@8~63>M~ev(MXIp_Vho}D~eVD=LKj*jJgiP_2%daX=3DAife z&x&@mZ^l2r!eI4Xz-k}if4tCNLHPebl-oJNzP-?2O{Djup#LrOf69~(Uz8T~VK#k} zs$M-K@;g+-vyw=!O@`K1Af^!b&wGw+kYPwFpNm`OhnG+z+U$nh)k zf3%=C$i!>kT`{2;KYaOH_}fC*Z!PFwM0t(L*awB|{CGmgsRfa~pRkh8ucH&Wu#aNe z$$9HjFG+bh+Q|!Nv)9$j9qr`N$?UD*<&Jjp!dSDH_;<9EhuQ1$z|l@#7$@vwI+5%9 zKdbxn3o-5F(Ir`XM>}~K-)}UvKsh+t$qOQXj&}0sQsFbrZsJMB?`S6v~15^Vr)Ym3~Y+dBix*;`1NViF^Wk{(L@&VmguQ{+_mv=|rAAeg;uY zCvq{qM3y##wDubxkN8Cnh`i&?V8f4_4wDVUY^6O|PFNpeZw3C+#dq+EYLC78L`I^XQM z@^J4ydwRMl!bClO{?E%_igW^Aezx^zQq`*w!v6y6FEN-e{GTE0D+E1U(8r7P4-#@4 zn~(QQD>rLo(W55PnDn%h2l4Y6aee|vJ9#MV9i7M@%lZ#vI+4FD1{jGiuVXB2yii{8{JN9d{%3CW*>p^QhIZ+fiuUJd zCl9mZP2lK6F6_f>`wfB*v-xwhvoDDHadaXV{ihJqP9BN+akP^c=97MBU;d7c<;-uH zy`($}!k?p^zY<-4>v(^TcJhKK4@WzBiI6+m$-~b~c_rgzOq+g>j%f^N1PPz#eKlX$ zD@DK0)u;dG@)-wm{V*Qn@);L${V+b{@);*``HUC2e8!DjKI4bx1HJfiVBCP0)MNO$ zmB)JgVvrq&5_(M`Z!7xQ45R7%@r!(6NERV+{F?S%X`aU6Uslb)^5^f;~S(~r~4dl}C@{_~7|{dNW) zNqzoE+y0XHF1GeD?XNT7=h`1?>`kVy@%j%G(JvS2p{MQ4nr(Cw)XE91=OB#Stn15z z!VDxGQ~K?)`a&KQwEs19R;-?*lk~N}HeMdZbRxe|$Ya{c3z{Y#}3 zPtsr0r|)Pd53aZQGiz-Z(@tK{{cWw3eM~z!-;2{rOwzt$+R3B4j4Y8mI+1ICt9tt= zrW3jFkG)CHIaH-)&*j&B9o5qXar`)3&_hLe>-tKSUx{dsj!xpc+3K4$c9Es)*VkJ` z>Wg`9eEE1^w*Gwn1bw@RKN9iZohc97|5E+M^fzc1f2pWnM>~0Rr^sJSJ9$CWucMv3 zB<}yNd>rlM=<6jW>5tj^wc`Qfjk7o7fggW@f+&ASJO81OJKD)3-9P*<`UQ?oL6yhOBrLGw$zMl)8&SE*@Q_1hPxm?+gB560N`g=M@LQ&;~= z{U+K|XQNi%f2Sh;4-@V8Tx0M3&$sa>8E7l$602|4dPO=c-rnZO(^IrKX%GBrs-QP9 z_Vwe3pIbhoGw}z}Ec|s!rGK*UAJa*GDn$7mVf7RF^&-D5MgFeL&_Q&Pus>P&-_ZIq zT6%Rs#-3le753YS^pCZ2v(~E%g?%LG=2kz+&v_Yn5Pp)82f?Y(9a80Yww0T;UP=0? ztiR|4bkD= zPZ9JDmNsj>lJdhu{L=(|oS=^v^aCP44+{Dup?{j74;S=B!u}#5UqR4%JWH3y{X$+T z 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 I4Ybz(Uqy{96#xJL literal 0 HcmV?d00001 diff --git a/packages/language-sass/lib/main.js b/packages/language-sass/lib/main.js new file mode 100644 index 000000000..ea408cf95 --- /dev/null +++ b/packages/language-sass/lib/main.js @@ -0,0 +1,12 @@ + +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.css.scss', { + types: ['comment', 'single_line_comment'] + }); +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.css.scss', { + types: ['comment', 'single_line_comment'] + }); +}; diff --git a/packages/language-sass/package.json b/packages/language-sass/package.json index 293b3c7c8..cda7ec3f7 100644 --- a/packages/language-sass/package.json +++ b/packages/language-sass/package.json @@ -1,14 +1,27 @@ { "name": "language-sass", "version": "0.62.2", + "main": "lib/main", "description": "Sass/SCSS language support in Atom", "license": "MIT", "engines": { "atom": "*", - "node": "*" + "node": ">=12" }, "repository": "https://github.com/pulsar-edit/pulsar", "devDependencies": { "dedent": "^0.7.0" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-sass/snippets/scss.cson b/packages/language-sass/snippets/scss.cson new file mode 100644 index 000000000..9c31e1576 --- /dev/null +++ b/packages/language-sass/snippets/scss.cson @@ -0,0 +1,49 @@ +'.source.css.scss': + '!important': + prefix: '!' + body: 'i!important${:;}$0' + '@use': + prefix: 'use' + body: "@use '${1:file}'${2: as ${3:alias}};" + '@import': + prefix: 'import' + body: '@import "$0";' + description: "An enhanced version of CSS’s “@import” rule." + descriptionMoreURL: "https://sass-lang.com/documentation/at-rules/import/" + '@include': + prefix: 'include' + body: '@include ${1:mixin}${2:($3)};$0' + description: "Include a mixin into the current context." + descriptionMoreURL: "https://sass-lang.com/documentation/at-rules/mixin/" + '@extend': + prefix: 'extend' + body: '@extend ${1}$0'; + description: "Tells one selector to inherit the styles of another." + descriptionMoreURL: "https://sass-lang.com/documentation/at-rules/extend/" + '@if': + prefix: 'if' + body: """ + @if ${1:conditions} { + $0 + } + """ + description: "Controls whether or not its block gets evaluated." + descriptionMoreURL: "https://sass-lang.com/documentation/at-rules/control/if/" + '@mixin': + prefix: 'mixin' + body: """ + @mixin ${1:name}${2:($3)} { + $0 + } + """ + description: "Include a mixin." + descriptionMoreURL: "https://sass-lang.com/documentation/at-rules/mixin/" + '@function': + prefix: 'fun' + body: """ + @mixin ${1:name} { + $0 + } + """ + description: "Define your own function." + descriptionMoreURL: "https://sass-lang.com/documentation/at-rules/function/" From 5472e32e211aafccdc71880d3e81232cd1973984 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 14 Apr 2024 15:28:31 -0700 Subject: [PATCH 06/31] [language-sass] Force the TextMate grammar when running SassDoc specs --- packages/language-sass/spec/sassdoc-spec.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/language-sass/spec/sassdoc-spec.js b/packages/language-sass/spec/sassdoc-spec.js index e87d07ef1..cc40b8498 100644 --- a/packages/language-sass/spec/sassdoc-spec.js +++ b/packages/language-sass/spec/sassdoc-spec.js @@ -1,15 +1,19 @@ -describe('SassDoc grammar', function() { +describe('SassDoc grammar', function () { let grammar = null; - beforeEach(function() { + beforeEach(function () { + // There isn't a Tree-sitter grammar for SassDoc that I'm aware of. Users + // who expect thorough highlighting of SassDoc can add a scope-specific + // override to prefer the TextMate-style SCSS grammar. + atom.config.set('core.useTreeSitterParsers', false) waitsForPromise(() => atom.packages.activatePackage('language-sass')); runs(() => grammar = atom.grammars.grammarForScopeName('source.css.scss')); }); - describe('block tags', function() { - it('tokenises simple tags', function() { + describe('block tags', function () { + it('tokenises simple tags', function () { const {tokens} = grammar.tokenizeLine('/// @deprecated'); expect(tokens[0]).toEqual({value: '///', scopes: ['source.css.scss', 'comment.block.documentation.scss', 'punctuation.definition.comment.scss']}); expect(tokens[1]).toEqual({value: ' ', scopes: ['source.css.scss', 'comment.block.documentation.scss']}); @@ -17,7 +21,7 @@ describe('SassDoc grammar', function() { expect(tokens[3]).toEqual({value: 'deprecated', scopes: ['source.css.scss', 'comment.block.documentation.scss', 'storage.type.class.sassdoc']}); }); - it('tokenises @param tags with a description', function() { + it('tokenises @param tags with a description', function () { const {tokens} = grammar.tokenizeLine('/// @param {type} $name - Description'); expect(tokens[0]).toEqual({value: '///', scopes: ['source.css.scss', 'comment.block.documentation.scss', 'punctuation.definition.comment.scss']}); expect(tokens[2]).toEqual({value: '@', scopes: ['source.css.scss', 'comment.block.documentation.scss', 'storage.type.class.sassdoc', 'punctuation.definition.block.tag.sassdoc']}); @@ -30,7 +34,7 @@ describe('SassDoc grammar', function() { }); }); - describe('highlighted examples', () => it('highlights SCSS after an @example tag', function() { + describe('highlighted examples', () => it('highlights SCSS after an @example tag', function () { const lines = grammar.tokenizeLines(`\ /// /// @example scss - Description From 5ea9adf13b1ce1912bf1494f7a1332d55a496175 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 21 Apr 2024 18:04:01 -0700 Subject: [PATCH 07/31] Avoid the cost of reading `node.children` when we don't need to --- src/wasm-tree-sitter-language-mode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wasm-tree-sitter-language-mode.js b/src/wasm-tree-sitter-language-mode.js index 922382d4f..b28fbc879 100644 --- a/src/wasm-tree-sitter-language-mode.js +++ b/src/wasm-tree-sitter-language-mode.js @@ -4501,7 +4501,7 @@ class NodeRangeSet { getNodeSpec(node, getChildren) { let { startIndex, endIndex, startPosition, endPosition, id } = node; let result = { startIndex, endIndex, startPosition, endPosition, id }; - if (node.children && getChildren) { + if (getChildren && node.childCount > 0) { result.children = []; for (let child of node.children) { result.children.push(this.getNodeSpec(child, false)); From 6d36685df38fe2252ba832d0be8e9b46ec96376c Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 21 Apr 2024 18:38:52 -0700 Subject: [PATCH 08/31] [language-ruby] Update to latest Tree-sitter Ruby parser --- .../grammars/modern-tree-sitter-ruby.cson | 2 +- .../tree-sitter-ruby/tree-sitter-ruby.wasm | Bin 2109157 -> 2106948 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-ruby/grammars/modern-tree-sitter-ruby.cson b/packages/language-ruby/grammars/modern-tree-sitter-ruby.cson index 928fda052..164e0ed95 100644 --- a/packages/language-ruby/grammars/modern-tree-sitter-ruby.cson +++ b/packages/language-ruby/grammars/modern-tree-sitter-ruby.cson @@ -6,7 +6,7 @@ parser: 'tree-sitter-ruby' injectionRegex: 'rb|ruby|RB|RUBY' treeSitter: - parserSource: 'github:tree-sitter/tree-sitter-ruby#4d9ad3f010fdc47a8433adcf9ae30c8eb8475ae7' + parserSource: 'github:tree-sitter/tree-sitter-ruby#9d86f3761bb30e8dcc81e754b81d3ce91848477e' grammar: 'tree-sitter-ruby/tree-sitter-ruby.wasm' highlightsQuery: 'tree-sitter-ruby/highlights.scm' localsQuery: 'tree-sitter-ruby/locals.scm' diff --git a/packages/language-ruby/grammars/tree-sitter-ruby/tree-sitter-ruby.wasm b/packages/language-ruby/grammars/tree-sitter-ruby/tree-sitter-ruby.wasm index c4818c3cebe3a368feab1cbf0f2baaa679a979f3..bfca703c3d1aa76bb1aa149cb41141188131169f 100755 GIT binary patch delta 14274 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 delta 16422 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 Date: Sun, 21 Apr 2024 20:44:20 -0700 Subject: [PATCH 09/31] =?UTF-8?q?[language-gfm]=20Make=20each=20block-leve?= =?UTF-8?q?l=20HTML=20tag=20its=20own=20injection=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …which, surprising as it sounds, has a _huge_ performance benefit. --- packages/language-gfm/lib/main.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/language-gfm/lib/main.js b/packages/language-gfm/lib/main.js index f85b2f4cd..1ddb58d05 100644 --- a/packages/language-gfm/lib/main.js +++ b/packages/language-gfm/lib/main.js @@ -23,13 +23,11 @@ exports.activate = () => { languageScope: null }); - // Create one HTML injection layer for all block-level HTML nodes. + // A separate injection layer for each block-level HTML node. atom.grammars.addInjectionPoint('source.gfm', { - type: 'document', + type: 'html_block', language: () => 'html', - content(node) { - return node.descendantsOfType('html_block'); - }, + content: (node) => node, includeChildren: true }); From 8e9e05db44b06215233aa6935898be35ffb5b9c6 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 5 May 2024 10:50:55 -0700 Subject: [PATCH 10/31] [language-typescript] More highlighting fixes, especially for operators --- .../grammars/common/highlights.scm | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/language-typescript/grammars/common/highlights.scm b/packages/language-typescript/grammars/common/highlights.scm index 5b9d7cf53..73d34f2ee 100644 --- a/packages/language-typescript/grammars/common/highlights.scm +++ b/packages/language-typescript/grammars/common/highlights.scm @@ -18,16 +18,16 @@ (identifier) @variable.other.assignment.import.namespace._LANG_) ; The "*" in `export * from 'bar'` -(export_statement "*" @variable.other.assignment.export.all.js) +(export_statement "*" @variable.other.assignment.export.all._LANG_) ; The "*" in `export * as Foo from 'bar'` (export_statement - (namespace_export "*" @variable.other.assignment.export.all.js)) + (namespace_export "*" @variable.other.assignment.export.all._LANG_)) ; The "*" in `export * as Foo from 'bar'` (export_statement (namespace_export - (identifier) @variable.other.assignment.export.alias.js)) + (identifier) @variable.other.assignment.export.alias._LANG_)) ; The "Foo" in `export { Foo }` (export_specifier @@ -51,7 +51,7 @@ ; ========= (this) @variable.language.this._LANG_ -(super) @variable.language.super._LANG_._LANG_x +(super) @variable.language.super._LANG_ (required_parameter pattern: (identifier) @variable.parameter.with-default._LANG_ @@ -345,7 +345,7 @@ "=>" @storage.type.arrow._LANG_ -; TODO: If I allow scopes like `storage.type.string._LANG_`, I will make a lot of +; TODO: If I allow scopes like `storage.type.string.ts`, I will make a lot of ; text look like strings by accident. This really needs to be fixed in syntax ; themes. ; @@ -764,10 +764,10 @@ ) @meta.embedded.line.interpolation._LANG_ (string - (escape_sequence) @constant.character.escape.js) + (escape_sequence) @constant.character.escape._LANG_) (template_string - (escape_sequence) @constant.character.escape.js) + (escape_sequence) @constant.character.escape._LANG_) ; CONSTANTS @@ -817,16 +817,16 @@ ; REGEX ; ===== -(regex) @string.regexp.js +(regex) @string.regexp._LANG_ (regex - "/" @punctuation.definition.string.begin.js + "/" @punctuation.definition.string.begin._LANG_ (#is? test.first)) (regex - "/" @punctuation.definition.string.end.js + "/" @punctuation.definition.string.end._LANG_ (#is? test.last)) -(regex_flags) @keyword.other.js +(regex_flags) @keyword.other._LANG_ ; OPERATORS @@ -837,26 +837,28 @@ "=" @keyword.operator.assignment._LANG_ (non_null_expression "!" @keyword.operator.non-null._LANG_) -(unary_expression"!" @keyword.operator.unary._LANG_) +(unary_expression "!" @keyword.operator.unary._LANG_) [ + "&&=" + "||=" + "??=" "+=" "-=" "*=" + "**=" "/=" "%=" + "^=" + "&=" + "|=" "<<=" ">>=" ">>>=" - "&=" - "^=" - "|=" - "??=" - "||=" ] @keyword.operator.assignment.compound._LANG_ (binary_expression - ["+" "-" "*" "/" "%"] @keyword.operator.arithmetic._LANG_) + ["/" "+" "-" "*" "**" "%"] @keyword.operator.arithmetic._LANG_) (unary_expression ["+" "-"] @keyword.operator.unary._LANG_) @@ -866,15 +868,14 @@ "===" "!=" "!==" - ">=" - "<=" - ">" - "<" ] @keyword.operator.comparison._LANG_ ) ["++" "--"] @keyword.operator.increment._LANG_ +(binary_expression + [">=" "<=" ">" "<"] @keyword.operator.relational._LANG_) + [ "&&" "||" @@ -902,6 +903,20 @@ "." @keyword.operator.accessor._LANG_ "?." @keyword.operator.accessor.optional-chaining._LANG_ +; Optional chaining is illegal… + +; …on the left-hand side of an assignment. +(assignment_expression + left: (_) @_IGNORE_ + (#set! prohibitsOptionalChaining true)) + +; …within a `new` expression. +(new_expression + constructor: (_) @_IGNORE_ + (#set! prohibitsOptionalChaining true)) + +((optional_chain) @invalid.illegal.optional-chain._LANG_ + (#is? test.descendantOfNodeWithData prohibitsOptionalChaining)) (ternary_expression ["?" ":"] @keyword.operator.ternary._LANG_ From 62a35bccadb58d45b7b7033ab6f564e539fd7b1d Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 20 Apr 2024 21:23:49 -0700 Subject: [PATCH 11/31] =?UTF-8?q?Optimize=20re-rendering=20of=20content=20?= =?UTF-8?q?in=20a=20preview=20pane=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …especially syntax highlighting. This change brings in `morphdom` to allow us to be more efficient about how we apply the changes needed when the Markdown source re-renders. Instead of replacing all the content with every single change, we apply only the selective edits needed to adapt our existing markup to the target markup. Once this process is done, we introduce one `TextEditor` instance for each `pre` tag in the markup, then persist those editor elements in the rendered output so that we don't have to spend so much time creating and destroying editors. This is a _huge_ performance win, especially in documents with lots of code blocks. The editor instances are cached using the `pre` elements as keys, which is now possible because the `pre` elements themselves are preserved across re-renders. Editors are created when new `pre` elements appear, and are destroyed when they are no longer needed, change their contents whenever the contents of the `pre` changes, and change language modes whenever the code fence language identifier changes. This approach is now used no matter which Markdown renderer is employed; we manage syntax highlighting ourselves in all cases rather than letting `atom.ui.markdown` do it. --- packages/markdown-preview/lib/main.js | 47 ++- .../lib/markdown-preview-view.js | 33 +- packages/markdown-preview/lib/renderer.js | 323 +++++++++++------- packages/markdown-preview/package-lock.json | 14 +- packages/markdown-preview/package.json | 4 +- .../spec/markdown-preview-view-spec.js | 15 +- .../styles/markdown-preview.less | 7 + 7 files changed, 303 insertions(+), 140 deletions(-) diff --git a/packages/markdown-preview/lib/main.js b/packages/markdown-preview/lib/main.js index 0b87479b7..14bb95a36 100644 --- a/packages/markdown-preview/lib/main.js +++ b/packages/markdown-preview/lib/main.js @@ -12,10 +12,18 @@ const isMarkdownPreviewView = function (object) { } module.exports = { - activate () { + activate() { this.disposables = new CompositeDisposable() this.commandSubscriptions = new CompositeDisposable() + this.style = new CSSStyleSheet() + + // When we upgrade Electron, we can push onto `adoptedStyleSheets` + // directly. For now, we have to do this silly thing. + let styleSheets = Array.from(document.adoptedStyleSheets ?? []) + styleSheets.push(this.style) + document.adoptedStyleSheets = styleSheets + this.disposables.add( atom.config.observe('markdown-preview.grammars', grammars => { this.commandSubscriptions.dispose() @@ -53,6 +61,22 @@ module.exports = { }) ) + this.disposables.add( + atom.config.observe('editor.fontFamily', (fontFamily) => { + // Keep the user's `fontFamily` setting in sync with preview styles. + // `pre` blocks will use this font automatically, but `code` elements + // need a specific style rule. + // + // Since this applies to all content, we should declare this only once, + // instead of once per preview view. + this.style.replaceSync(` + .markdown-preview code { + font-family: ${fontFamily} !important; + } + `) + }) + ) + const previewFile = this.previewFile.bind(this) for (const extension of [ 'markdown', @@ -94,12 +118,12 @@ module.exports = { ) }, - deactivate () { + deactivate() { this.disposables.dispose() this.commandSubscriptions.dispose() }, - createMarkdownPreviewView (state) { + createMarkdownPreviewView(state) { if (state.editorId || fs.isFileSync(state.filePath)) { if (MarkdownPreviewView == null) { MarkdownPreviewView = require('./markdown-preview-view') @@ -108,7 +132,7 @@ module.exports = { } }, - toggle () { + toggle() { if (isMarkdownPreviewView(atom.workspace.getActivePaneItem())) { atom.workspace.destroyActivePaneItem() return @@ -129,11 +153,11 @@ module.exports = { } }, - uriForEditor (editor) { + uriForEditor(editor) { return `markdown-preview://editor/${editor.id}` }, - removePreviewForEditor (editor) { + removePreviewForEditor(editor) { const uri = this.uriForEditor(editor) const previewPane = atom.workspace.paneForURI(uri) if (previewPane != null) { @@ -144,7 +168,7 @@ module.exports = { } }, - addPreviewForEditor (editor) { + addPreviewForEditor(editor) { const uri = this.uriForEditor(editor) const previousActivePane = atom.workspace.getActivePane() const options = { searchAllPanes: true } @@ -161,7 +185,7 @@ module.exports = { }) }, - previewFile ({ target }) { + previewFile({ target }) { const filePath = target.dataset.path if (!filePath) { return @@ -178,7 +202,7 @@ module.exports = { }) }, - async copyHTML () { + async copyHTML() { const editor = atom.workspace.getActiveTextEditor() if (editor == null) { return @@ -191,13 +215,14 @@ module.exports = { const html = await renderer.toHTML( text, editor.getPath(), - editor.getGrammar() + editor.getGrammar(), + editor.id ) atom.clipboard.write(html) }, - saveAsHTML () { + saveAsHTML() { const activePaneItem = atom.workspace.getActivePaneItem() if (isMarkdownPreviewView(activePaneItem)) { atom.workspace.getActivePane().saveItemAs(activePaneItem) diff --git a/packages/markdown-preview/lib/markdown-preview-view.js b/packages/markdown-preview/lib/markdown-preview-view.js index 1f6410d4a..34e964f16 100644 --- a/packages/markdown-preview/lib/markdown-preview-view.js +++ b/packages/markdown-preview/lib/markdown-preview-view.js @@ -1,4 +1,5 @@ const path = require('path') +const morphdom = require('morphdom') const { Emitter, Disposable, CompositeDisposable, File } = require('atom') const _ = require('underscore-plus') @@ -17,6 +18,7 @@ module.exports = class MarkdownPreviewView { this.element = document.createElement('div') this.element.classList.add('markdown-preview') this.element.tabIndex = -1 + this.emitter = new Emitter() this.loaded = false this.disposables = new CompositeDisposable() @@ -32,6 +34,7 @@ module.exports = class MarkdownPreviewView { }) ) } + this.editorCache = new renderer.EditorCache(editorId) } serialize() { @@ -52,6 +55,7 @@ module.exports = class MarkdownPreviewView { destroy() { this.disposables.dispose() this.element.remove() + this.editorCache.destroy() } registerScrollCommands() { @@ -83,7 +87,7 @@ module.exports = class MarkdownPreviewView { return this.emitter.on('did-change-title', callback) } - onDidChangeModified(callback) { + onDidChangeModified(_callback) { // No op to suppress deprecation warning return new Disposable() } @@ -309,18 +313,33 @@ module.exports = class MarkdownPreviewView { async renderMarkdownText(text) { const { scrollTop } = this.element - try { - const domFragment = await renderer.toDOMFragment( + const [domFragment, done] = await renderer.toDOMFragment( text, this.getPath(), - this.getGrammar() + this.getGrammar(), + this.editorId ) this.loading = false this.loaded = true - this.element.textContent = '' - this.element.appendChild(domFragment) + + // Clone the existing container + let newElement = this.element.cloneNode(false) + newElement.appendChild(domFragment) + + morphdom(this.element, newElement, { + onBeforeNodeDiscarded(node) { + // Don't discard `atom-text-editor` elements despite the fact that + // they don't exist in the new content. + if (node.nodeName === 'ATOM-TEXT-EDITOR') { + return false + } + } + }) + + await done(this.element) + this.emitter.emit('did-change-markdown') this.element.scrollTop = scrollTop } catch (error) { @@ -400,7 +419,7 @@ module.exports = class MarkdownPreviewView { .join('\n') .replace(/atom-text-editor/g, 'pre.editor-colors') .replace(/:host/g, '.host') // Remove shadow-dom :host selector causing problem on FF - .replace(cssUrlRegExp, function (match, assetsName, offset, string) { + .replace(cssUrlRegExp, function (_match, assetsName, _offset, _string) { // base64 encode assets const assetPath = path.join(__dirname, '../assets', assetsName) const originalData = fs.readFileSync(assetPath, 'binary') diff --git a/packages/markdown-preview/lib/renderer.js b/packages/markdown-preview/lib/renderer.js index 636ad68c5..14d4db59d 100644 --- a/packages/markdown-preview/lib/renderer.js +++ b/packages/markdown-preview/lib/renderer.js @@ -17,91 +17,147 @@ const emojiFolder = path.join( 'pngs' ) -exports.toDOMFragment = async function (text, filePath, grammar, callback) { +// Creating `TextEditor` instances is costly, so we'll try to re-use instances +// when a preview changes. +class EditorCache { + static BY_ID = new Map() - text ??= ""; - - if (atom.config.get("markdown-preview.useOriginalParser")) { - const domFragment = render(text, filePath); - - await highlightCodeBlocks(domFragment, grammar, makeAtomEditorNonInteractive); - - return domFragment; - - } else { - // We use the new parser! - const domFragment = atom.ui.markdown.render(text, - { - renderMode: "fragment", - filePath: filePath, - breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), - useDefaultEmoji: true, - sanitizeAllowUnknownProtocols: atom.config.get('markdown-preview.allowUnsafeProtocols') - } - ); - const domHTMLFragment = atom.ui.markdown.convertToDOM(domFragment); - await atom.ui.markdown.applySyntaxHighlighting(domHTMLFragment, - { - renderMode: "fragment", - syntaxScopeNameFunc: scopeForFenceName, - grammar: grammar - } - ); - - return domHTMLFragment; + static findOrCreateById(id) { + let cache = EditorCache.BY_ID.get(id) + if (!cache) { + cache = new EditorCache(id) + EditorCache.BY_ID.set(id, cache) + } + return cache } + + constructor(id) { + this.id = id + this.editorsByPre = new Map() + this.possiblyUnusedEditors = new Set() + } + + destroy() { + let editors = Array.from(this.editorsByPre.values()) + for (let editor of editors) { + editor.destroy() + } + this.editorsByPre.clear() + this.possiblyUnusedEditors.clear() + EditorCache.BY_ID.delete(this.id) + } + + // Called when we start a render. Every `TextEditor` is assumed to be stale, + // but any editor that is successfully looked up from the cache during this + // render is saved from culling. + beginRender() { + this.possiblyUnusedEditors.clear() + for (let editor of this.editorsByPre.values()) { + this.possiblyUnusedEditors.add(editor) + } + } + + // Cache an editor by the PRE element that it's standing in for. + addEditor(pre, editor) { + this.editorsByPre.set(pre, editor) + } + + getEditor(pre) { + let editor = this.editorsByPre.get(pre) + if (editor) { + // Cache hit! This editor will be reused, so we should prevent it from + // getting culled. + this.possiblyUnusedEditors.delete(editor) + } + return editor + } + + endRender() { + // Any editor that didn't get claimed during the render is orphaned and + // should be disposed of. + let toBeDeleted = new Set() + for (let [pre, editor] of this.editorsByPre.entries()) { + if (!this.possiblyUnusedEditors.has(editor)) continue + toBeDeleted.add(pre) + } + + this.possiblyUnusedEditors.clear() + + for (let pre of toBeDeleted) { + let editor = this.editorsByPre.get(pre) + let element = editor.getElement() + if (element.parentNode) { + element.remove() + } + this.editorsByPre.delete(pre) + editor.destroy() + } + } +} + +exports.EditorCache = EditorCache + +function chooseRender(text, filePath) { + if (atom.config.get("markdown-preview.useOriginalParser")) { + // Legacy rendering with `marked`. + return render(text, filePath) + } else { + // Built-in rendering with `markdown-it`. + let html = atom.ui.markdown.render(text, { + renderMode: "fragment", + filePath: filePath, + breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), + useDefaultEmoji: true, + sanitizeAllowUnknownProtocols: atom.config.get('markdown-preview.allowUnsafeProtocols') + }) + return atom.ui.markdown.convertToDOM(html) + } +} + +exports.toDOMFragment = async function (text, filePath, grammar, editorId) { + text ??= "" + let defaultLanguage = getDefaultLanguageForGrammar(grammar) + + // We cache editor instances in this code path because it's the one used by + // the preview pane, so we expect it to be updated quite frequently. + let cache = EditorCache.findOrCreateById(editorId) + cache.beginRender() + + const domFragment = chooseRender(text, filePath) + annotatePreElements(domFragment, defaultLanguage) + + return [ + domFragment, + async (element) => { + await highlightCodeBlocks(element, grammar, cache, makeAtomEditorNonInteractive) + cache.endRender() + } + ] } exports.toHTML = async function (text, filePath, grammar) { - text ??= ""; - if (atom.config.get("markdown-preview.useOriginalParser")) { - const domFragment = render(text, filePath) - const div = document.createElement('div') + // We don't cache editor instances in this code path because it's the one + // used by the “Copy HTML” command, so this is likely to be a one-off for + // which caches won't help. - div.appendChild(domFragment) - document.body.appendChild(div) + const domFragment = chooseRender(text, filePath) + const div = document.createElement('div') + annotatePreElements(domFragment, getDefaultLanguageForGrammar(grammar)) + div.appendChild(domFragment) + document.body.appendChild(div) - await highlightCodeBlocks(div, grammar, convertAtomEditorToStandardElement) + await highlightCodeBlocks(div, grammar, null, convertAtomEditorToStandardElement) - const result = div.innerHTML - div.remove() + const result = div.innerHTML; + div.remove(); - return result - } else { - // We use the new parser! - const domFragment = atom.ui.markdown.render(text, - { - renderMode: "full", - filePath: filePath, - breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), - useDefaultEmoji: true, - sanitizeAllowUnknownProtocols: atom.config.get('markdown-preview.allowUnsafeProtocols') - } - ); - const domHTMLFragment = atom.ui.markdown.convertToDOM(domFragment); - - const div = document.createElement("div"); - div.appendChild(domHTMLFragment); - document.body.appendChild(div); - - await atom.ui.markdown.applySyntaxHighlighting(div, - { - renderMode: "full", - syntaxScopeNameFunc: scopeForFenceName, - grammar: grammar - } - ); - - const result = div.innerHTML; - div.remove(); - - return result; - } + return result; } -var render = function (text, filePath) { +// Render with the package's own `marked` library. +function render(text, filePath) { if (marked == null || yamlFrontMatter == null || cheerio == null) { marked = require('marked') yamlFrontMatter = require('yaml-front-matter') @@ -124,12 +180,13 @@ var render = function (text, filePath) { let html = marked.parse(renderYamlTable(vars) + __content) - // emoji-images is too aggressive, so replace images in monospace tags with the actual emoji text. + // emoji-images is too aggressive, so replace images in monospace tags with + // the actual emoji text. const $ = cheerio.load(emoji(html, emojiFolder, 20)) - $('pre img').each((index, element) => + $('pre img').each((_index, element) => $(element).replaceWith($(element).attr('title')) ) - $('code img').each((index, element) => + $('code img').each((_index, element) => $(element).replaceWith($(element).attr('title')) ) @@ -159,7 +216,7 @@ function renderYamlTable(variables) { const markdownRows = [ entries.map(entry => entry[0]), - entries.map(entry => '--'), + entries.map(_ => '--'), entries.map((entry) => { if (typeof entry[1] === "object" && !Array.isArray(entry[1])) { // Remove all newlines, or they ruin formatting of parent table @@ -175,7 +232,7 @@ function renderYamlTable(variables) { ) } -var resolveImagePaths = function (element, filePath) { +function resolveImagePaths(element, filePath) { const [rootDirectory] = atom.project.relativizePath(filePath) const result = [] @@ -219,55 +276,90 @@ var resolveImagePaths = function (element, filePath) { return result } -var highlightCodeBlocks = function (domFragment, grammar, editorCallback) { - let defaultLanguage, fontFamily - if ( - (grammar != null ? grammar.scopeName : undefined) === 'source.litcoffee' - ) { - defaultLanguage = 'coffee' - } else { - defaultLanguage = 'text' - } +function getDefaultLanguageForGrammar(grammar) { + return grammar?.scopeName === 'source.litcoffee' ? 'coffee' : 'text' +} - if ((fontFamily = atom.config.get('editor.fontFamily'))) { - for (const codeElement of domFragment.querySelectorAll('code')) { - codeElement.style.fontFamily = fontFamily - } +function annotatePreElements(fragment, defaultLanguage) { + for (let preElement of fragment.querySelectorAll('pre')) { + const codeBlock = preElement.firstElementChild ?? preElement + const className = codeBlock.getAttribute('class') + const fenceName = className?.replace(/^language-/, '') ?? defaultLanguage + preElement.classList.add('editor-colors', `lang-${fenceName}`) } +} + +function reassignEditorToLanguage(editor, languageScope) { + // When we successfully reassign the language on an editor, its + // `data-grammar` attribute updates on its own. + let result = atom.grammars.assignLanguageMode(editor, languageScope) + if (result) return true + + // When we fail to assign the language on an editor — maybe its package is + // deactivated — it won't reset itself to the default grammar, so we have to + // do it ourselves. + result = atom.grammars.assignLanguageMode(editor, `text.plain.null-grammar`) + if (!result) return false +} + +// After render, create an `atom-text-editor` for each `pre` element so that we +// enjoy syntax highlighting. +function highlightCodeBlocks(element, grammar, cache, editorCallback) { + let defaultLanguage = getDefaultLanguageForGrammar(grammar) const promises = [] - for (const preElement of domFragment.querySelectorAll('pre')) { - const codeBlock = - preElement.firstElementChild != null - ? preElement.firstElementChild - : preElement + + for (const preElement of element.querySelectorAll('pre')) { + const codeBlock = preElement.firstElementChild ?? preElement const className = codeBlock.getAttribute('class') - const fenceName = - className != null ? className.replace(/^language-/, '') : defaultLanguage + const fenceName = className?.replace(/^language-/, '') ?? defaultLanguage + let editorText = codeBlock.textContent.replace(/\r?\n$/, '') - const editor = new TextEditor({ - readonly: true, - keyboardInputEnabled: false - }) - const editorElement = editor.getElement() + // If this PRE element was present in the last render, then we should + // already have a cached text editor available for use. + let editor = cache?.getEditor(preElement) ?? null + let editorElement + if (!editor) { + editor = new TextEditor({ keyboardInputEnabled: false }) + editorElement = editor.getElement() + editorElement.setUpdatedSynchronously(true) + editor.setReadOnly(true) + cache?.addEditor(preElement, editor) + } else { + editorElement = editor.getElement() + } - preElement.classList.add('editor-colors', `lang-${fenceName}`) - editorElement.setUpdatedSynchronously(true) - preElement.innerHTML = '' - preElement.parentNode.insertBefore(editorElement, preElement) - editor.setText(codeBlock.textContent.replace(/\r?\n$/, '')) - atom.grammars.assignLanguageMode(editor, scopeForFenceName(fenceName)) - editor.setVisible(true) + // If the PRE changed its content, we need to change the content of its + // `TextEditor`. + if (editor.getText() !== editorText) { + editor.setReadOnly(false) + editor.setText(editorText) + editor.setReadOnly(true) + } + + // If the PRE changed its language, we need to change the language of its + // `TextEditor`. + let scopeDescriptor = editor.getRootScopeDescriptor()[0] + let languageScope = scopeForFenceName(fenceName) + if (languageScope !== scopeDescriptor && `.${languageScope}` !== scopeDescriptor) { + reassignEditorToLanguage(editor, languageScope) + } + + // If the editor is brand new, we'll have to insert it; otherwise it should + // already be in the right place. + if (!editorElement.parentNode) { + preElement.parentNode.insertBefore(editorElement, preElement) + editor.setVisible(true) + } promises.push(editorCallback(editorElement, preElement)) } return Promise.all(promises) } -var makeAtomEditorNonInteractive = function (editorElement, preElement) { - preElement.remove() - editorElement.setAttributeNode(document.createAttribute('gutter-hidden')) // Hide gutter - editorElement.removeAttribute('tabindex') // Make read-only +function makeAtomEditorNonInteractive(editorElement) { + editorElement.setAttributeNode(document.createAttribute('gutter-hidden')) + editorElement.removeAttribute('tabindex') // Remove line decorations from code blocks. for (const cursorLineDecoration of editorElement.getModel() @@ -276,9 +368,12 @@ var makeAtomEditorNonInteractive = function (editorElement, preElement) { } } -var convertAtomEditorToStandardElement = (editorElement, preElement) => { +function convertAtomEditorToStandardElement(editorElement, preElement) { return new Promise(function (resolve) { const editor = editorElement.getModel() + // In this code path, we're transplanting the highlighted editor HTML into + // the existing `pre` element, so we should empty its contents first. + preElement.innerHTML = '' const done = () => editor.component.getNextUpdatePromise().then(function () { for (const line of editorElement.querySelectorAll( diff --git a/packages/markdown-preview/package-lock.json b/packages/markdown-preview/package-lock.json index 887d4d766..689c1e03d 100644 --- a/packages/markdown-preview/package-lock.json +++ b/packages/markdown-preview/package-lock.json @@ -16,6 +16,7 @@ "fs-plus": "^3.0.0", "github-markdown-css": "^5.5.1", "marked": "5.0.3", + "morphdom": "^2.7.2", "underscore-plus": "^1.0.0", "yaml-front-matter": "^4.1.1" }, @@ -23,7 +24,8 @@ "temp": "^0.8.1" }, "engines": { - "atom": "*" + "atom": "*", + "node": ">=12" } }, "node_modules/@types/node": { @@ -278,6 +280,11 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/morphdom": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.2.tgz", + "integrity": "sha512-Dqb/lHFyTi7SZpY0a5R4I/0Edo+iPMbaUexsHHsLAByyixCDiLHPHyVoKVmrpL0THcT7V9Cgev9y21TQYq6wQg==" + }, "node_modules/nth-check": { "version": "1.0.2", "license": "BSD-2-Clause", @@ -567,6 +574,11 @@ "minimist": "0.0.8" } }, + "morphdom": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.2.tgz", + "integrity": "sha512-Dqb/lHFyTi7SZpY0a5R4I/0Edo+iPMbaUexsHHsLAByyixCDiLHPHyVoKVmrpL0THcT7V9Cgev9y21TQYq6wQg==" + }, "nth-check": { "version": "1.0.2", "requires": { diff --git a/packages/markdown-preview/package.json b/packages/markdown-preview/package.json index 0bb4a6e75..5ec7e4428 100644 --- a/packages/markdown-preview/package.json +++ b/packages/markdown-preview/package.json @@ -6,7 +6,8 @@ "repository": "https://github.com/pulsar-edit/pulsar", "license": "MIT", "engines": { - "atom": "*" + "atom": "*", + "node": ">=12" }, "scripts": { "generate-github-markdown-css": "node scripts/generate-github-markdown-css.js" @@ -19,6 +20,7 @@ "fs-plus": "^3.0.0", "github-markdown-css": "^5.5.1", "marked": "5.0.3", + "morphdom": "^2.7.2", "underscore-plus": "^1.0.0", "yaml-front-matter": "^4.1.1" }, diff --git a/packages/markdown-preview/spec/markdown-preview-view-spec.js b/packages/markdown-preview/spec/markdown-preview-view-spec.js index c34d5a727..02a2c7623 100644 --- a/packages/markdown-preview/spec/markdown-preview-view-spec.js +++ b/packages/markdown-preview/spec/markdown-preview-view-spec.js @@ -198,12 +198,15 @@ function f(x) { () => renderSpy.callCount === 1 ) - runs(function () { - const rubyEditor = preview.element.querySelector( - "atom-text-editor[data-grammar='source ruby']" - ) - expect(rubyEditor).toBeNull() - }) + waitsFor( + 'atom-text-editor to reassign all language modes after re-render', + () => { + let rubyEditor = preview.element.querySelector( + "atom-text-editor[data-grammar='source ruby']" + ) + return rubyEditor == null + } + ) waitsForPromise(() => atom.packages.activatePackage('language-ruby')) diff --git a/packages/markdown-preview/styles/markdown-preview.less b/packages/markdown-preview/styles/markdown-preview.less index bff8cec53..0853c74ef 100644 --- a/packages/markdown-preview/styles/markdown-preview.less +++ b/packages/markdown-preview/styles/markdown-preview.less @@ -2,6 +2,13 @@ // Global Markdown Preview styles .markdown-preview { + + // Hide a `pre` that comes directly after an `atom-text-editor` because the + // `atom-text-editor` is the syntax-highlighted representation. + atom-text-editor + pre { + display: none; + } + atom-text-editor { // only show scrollbars on hover .scrollbars-visible-always & { From 18e0da8baae8db3d197fb2162ebeb39aab32eec9 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 21 Apr 2024 11:36:06 -0700 Subject: [PATCH 12/31] Fix `atom.ui.markdown` issue with rendering of HTML in code blocks --- spec/ui-spec.js | 21 +++++++++++++++++++++ src/ui.js | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/spec/ui-spec.js b/spec/ui-spec.js index 8ad5e0e66..26dda6081 100644 --- a/spec/ui-spec.js +++ b/spec/ui-spec.js @@ -1,3 +1,4 @@ +const dedent = require('dedent'); describe("Renders Markdown", () => { describe("properly when given no opts", () => { @@ -7,6 +8,26 @@ describe("Renders Markdown", () => { }); }); + it(`escapes HTML in code blocks properly`, () => { + let input = dedent` + Lorem ipsum dolor. + + \`\`\`html +

sit amet

+ \`\`\` + ` + + let expected = dedent` +

Lorem ipsum dolor.

+
<p>sit amet</p>
+    
+ ` + + expect( + atom.ui.markdown.render(input).trim() + ).toBe(expected); + }) + describe("transforms links correctly", () => { it("makes no changes to a fqdn link", () => { expect(atom.ui.markdown.render("[Hello World](https://github.com)")) diff --git a/src/ui.js b/src/ui.js index 2a4662c44..66d208209 100644 --- a/src/ui.js +++ b/src/ui.js @@ -249,8 +249,8 @@ function renderMarkdown(content, givenOpts = {}) { // Here we can add some simple additions that make code highlighting possible later on, // but doesn't actually preform any code highlighting. - md.options.highlight = function(str, lang) { - return `
${str}
`; + md.options.highlight = function (str, lang) { + return `
${md.utils.escapeHtml(str)}
`; }; // Process disables From b46a7e8e86d1688ef19ed559ec9b65015fcb8a03 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 21 Apr 2024 19:48:53 -0700 Subject: [PATCH 13/31] =?UTF-8?q?Don't=20make=20`TextEditor`=20instances?= =?UTF-8?q?=20render=20synchronously=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …because we don't need it for our use case. Helps performance. --- packages/markdown-preview/lib/renderer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/markdown-preview/lib/renderer.js b/packages/markdown-preview/lib/renderer.js index 14d4db59d..342c7751f 100644 --- a/packages/markdown-preview/lib/renderer.js +++ b/packages/markdown-preview/lib/renderer.js @@ -322,7 +322,6 @@ function highlightCodeBlocks(element, grammar, cache, editorCallback) { if (!editor) { editor = new TextEditor({ keyboardInputEnabled: false }) editorElement = editor.getElement() - editorElement.setUpdatedSynchronously(true) editor.setReadOnly(true) cache?.addEditor(preElement, editor) } else { From 054100b43ce6e9a7642dd2300990a51e55673f89 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 21 Apr 2024 22:05:44 -0700 Subject: [PATCH 14/31] =?UTF-8?q?Rework=20loading=20indicator=20for=20`mar?= =?UTF-8?q?kdown-preview`=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and make the pane appear earlier on first open. --- .../lib/markdown-preview-view.js | 24 ++++++++--- .../spec/markdown-preview-spec.js | 7 ++++ .../styles/markdown-preview.less | 41 +++++++++++++++---- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/packages/markdown-preview/lib/markdown-preview-view.js b/packages/markdown-preview/lib/markdown-preview-view.js index 34e964f16..78a222efc 100644 --- a/packages/markdown-preview/lib/markdown-preview-view.js +++ b/packages/markdown-preview/lib/markdown-preview-view.js @@ -274,7 +274,22 @@ module.exports = class MarkdownPreviewView { return this.getMarkdownSource() .then(source => { if (source != null) { - return this.renderMarkdownText(source) + if (this.loaded) { + return this.renderMarkdownText(source); + } else { + // If we haven't loaded yet, defer before we render the Markdown + // for the first time. This allows the pane to appear and to + // display the loading indicator. Otherwise the first render + // happens before the pane is even visible. + // + // This doesn't slow anything down; it just shifts the work around + // so that the pane appears earlier in the cycle. + return new Promise((resolve) => { + setTimeout(() => { + resolve(this.renderMarkdownText(source)) + }, 0) + }) + } } }) .catch(reason => this.showError({ message: reason })) @@ -339,6 +354,7 @@ module.exports = class MarkdownPreviewView { }) await done(this.element) + this.element.classList.remove('loading') this.emitter.emit('did-change-markdown') this.element.scrollTop = scrollTop @@ -444,11 +460,7 @@ module.exports = class MarkdownPreviewView { showLoading() { this.loading = true - this.element.textContent = '' - const div = document.createElement('div') - div.classList.add('markdown-spinner') - div.textContent = 'Loading Markdown\u2026' - this.element.appendChild(div) + this.element.classList.add('loading') } selectAll() { diff --git a/packages/markdown-preview/spec/markdown-preview-spec.js b/packages/markdown-preview/spec/markdown-preview-spec.js index 8ea5bed7c..64627bcf3 100644 --- a/packages/markdown-preview/spec/markdown-preview-spec.js +++ b/packages/markdown-preview/spec/markdown-preview-spec.js @@ -41,6 +41,13 @@ describe('Markdown Preview', function () { .getActiveItem()) ) + waitsFor( + 'preview to finish loading', + () => { + return !preview.element.classList.contains('loading') + } + ) + runs(() => { expect(preview).toBeInstanceOf(MarkdownPreviewView) expect(preview.getPath()).toBe( diff --git a/packages/markdown-preview/styles/markdown-preview.less b/packages/markdown-preview/styles/markdown-preview.less index 0853c74ef..8e44e81b6 100644 --- a/packages/markdown-preview/styles/markdown-preview.less +++ b/packages/markdown-preview/styles/markdown-preview.less @@ -2,6 +2,7 @@ // Global Markdown Preview styles .markdown-preview { + contain: paint; // Hide a `pre` that comes directly after an `atom-text-editor` because the // `atom-text-editor` is the syntax-highlighted representation. @@ -35,14 +36,38 @@ .task-list-item { list-style-type: none; } + + &.loading { + display: flex; + flex-direction: column; + justify-content: center; + + // `.loading` on the preview element automatically shows the spinner/text. + // We add a slight animation delay so that, when the preview content is + // quick to appear (as usually happens), the spinner won't be shown. It + // only shows up when preview content takes a while to render. + &:before { + display: block; + content: 'Loading Markdown…'; + margin: auto; + background-image: url(images/octocat-spinner-128.gif); + background-repeat: no-repeat; + background-size: 64px; + background-position: top center; + padding-top: 70px; + text-align: center; + opacity: 0; + animation-duration: 1s; + animation-name: appear-after-short-delay; + animation-delay: 0.75s; + animation-fill-mode: forwards; + } + } } -.markdown-spinner { - margin: auto; - background-image: url(images/octocat-spinner-128.gif); - background-repeat: no-repeat; - background-size: 64px; - background-position: top center; - padding-top: 70px; - text-align: center; +// Not an actual animation; we just use an animation so that it can appear +// after a short delay. +@keyframes appear-after-short-delay { + 0% { opacity: 1; } + 100% { opacity: 1; } } From 74589feb9cb0986b614900a505575158bc659f72 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 5 May 2024 10:55:00 -0700 Subject: [PATCH 15/31] Remove the loading indicator when there's a rendering error --- packages/markdown-preview/lib/markdown-preview-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/markdown-preview/lib/markdown-preview-view.js b/packages/markdown-preview/lib/markdown-preview-view.js index 78a222efc..52586c106 100644 --- a/packages/markdown-preview/lib/markdown-preview-view.js +++ b/packages/markdown-preview/lib/markdown-preview-view.js @@ -448,6 +448,7 @@ module.exports = class MarkdownPreviewView { showError(result) { this.element.textContent = '' + this.element.classList.remove('loading') const h2 = document.createElement('h2') h2.textContent = 'Previewing Markdown Failed' this.element.appendChild(h2) From 4ff4a8bd42e9c63a6971f457cf6e903889f5f7e5 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 6 May 2024 17:11:41 -0700 Subject: [PATCH 16/31] [language-sass] Bump `tree-sitter-scss` to fix a bug --- .../grammars/modern-tree-sitter-scss.cson | 2 +- .../tree-sitter/tree-sitter-scss.wasm | Bin 245877 -> 235107 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-sass/grammars/modern-tree-sitter-scss.cson b/packages/language-sass/grammars/modern-tree-sitter-scss.cson index 853e8b167..60d69c021 100644 --- a/packages/language-sass/grammars/modern-tree-sitter-scss.cson +++ b/packages/language-sass/grammars/modern-tree-sitter-scss.cson @@ -15,7 +15,7 @@ fileTypes: [ injectionRegex: '^(scss|SCSS)$' treeSitter: - parserSource: 'github:savetheclocktower/tree-sitter-scss#e91dcb2c91b3e8853cccf7079b6a09fdeead75a3' + parserSource: 'github:savetheclocktower/tree-sitter-scss#090d25a5fc829ce6956201cf55ab6b6eacad999c' grammar: 'tree-sitter/tree-sitter-scss.wasm' highlightsQuery: 'tree-sitter/highlights.scm' foldsQuery: 'tree-sitter/folds.scm' diff --git a/packages/language-sass/grammars/tree-sitter/tree-sitter-scss.wasm b/packages/language-sass/grammars/tree-sitter/tree-sitter-scss.wasm index 51fbdd4eafb0e612f4db1d530095b03f0cc91db1..6ed858067783ce79b93242af6d0f78e6baeb0335 100755 GIT binary patch literal 235107 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 z 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 I4Ybz(Uqy{96#xJL From b2eff8751040a5920262bef53004e90dd377f068 Mon Sep 17 00:00:00 2001 From: confused_techie Date: Tue, 7 May 2024 07:40:33 -0700 Subject: [PATCH 17/31] 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 18/31] 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) | | From b997853b24b859da6bdaa821a757facf3d049e59 Mon Sep 17 00:00:00 2001 From: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Date: Sat, 11 May 2024 19:06:07 +0200 Subject: [PATCH 19/31] Update Renovate preset name --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 732c519e9..14fda4014 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,5 +1,5 @@ { - "extends": [ "config:base", ":dependencyDashboardApproval"], + "extends": [ "config:recommended", ":dependencyDashboardApproval"], "constraints": { "node": "< 16" }, From ea9182aab19e5d5e26705508e12d86929bfd99b4 Mon Sep 17 00:00:00 2001 From: DeeDeeG Date: Mon, 13 May 2024 18:16:04 -0400 Subject: [PATCH 20/31] CI: Remove workaround for Homebrew node in Cirrus on macOS This workaround was necessary before because something broke. Now the workaround is the thing that breaks things. Go figure. --- .cirrus.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index c4ba42d30..181b58cb1 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -134,7 +134,6 @@ silicon_mac_task: ROLLING_UPLOAD_TOKEN: ENCRYPTED[f935c396a9f4bca108ec2fdedb00dbc9be2f4c411f100d577acdab42db59ea134be059ce8535396db8222a2b1eb68c27] prepare_script: - brew update - - brew uninstall node - brew install git python@$PYTHON_VERSION python-setuptools - git submodule init - git submodule update @@ -199,7 +198,6 @@ silicon_mac_task: # - arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" # - export PATH="/usr/local/bin:$PATH" # - arch -x86_64 brew update -# - arch -x86_64 brew uninstall node # - arch -x86_64 brew install node@16 git python@$PYTHON_VERSION python-setuptools # - ln -s /usr/local/bin/python$PYTHON_VERSION /usr/local/bin/python # - npm install -g yarn From f560f7a2e52ab6f8178a34790fb83355e0232f83 Mon Sep 17 00:00:00 2001 From: DeeDeeG Date: Mon, 13 May 2024 19:05:36 -0400 Subject: [PATCH 21/31] CI: brew uninstall node@20 in .cirrus.yml Apparently it's not the "node" cask anymore in the base CI image, it's the "node@20" cask. Wow. The workaround of uninstalling this is still needed, it's just that the package name has changed. --- .cirrus.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cirrus.yml b/.cirrus.yml index 181b58cb1..34c84e1dc 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -134,6 +134,7 @@ silicon_mac_task: ROLLING_UPLOAD_TOKEN: ENCRYPTED[f935c396a9f4bca108ec2fdedb00dbc9be2f4c411f100d577acdab42db59ea134be059ce8535396db8222a2b1eb68c27] prepare_script: - brew update + - brew uninstall node@20 - brew install git python@$PYTHON_VERSION python-setuptools - git submodule init - git submodule update @@ -198,6 +199,7 @@ silicon_mac_task: # - arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" # - export PATH="/usr/local/bin:$PATH" # - arch -x86_64 brew update +# - arch -x86_64 brew uninstall node@20 # - arch -x86_64 brew install node@16 git python@$PYTHON_VERSION python-setuptools # - ln -s /usr/local/bin/python$PYTHON_VERSION /usr/local/bin/python # - npm install -g yarn From 678c1b7bd78942c4bd2deee9ebf142bac37a9bf4 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 14 May 2024 18:37:24 -0700 Subject: [PATCH 22/31] Address feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maurício Szabo --- packages/markdown-preview/lib/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/markdown-preview/lib/main.js b/packages/markdown-preview/lib/main.js index 14bb95a36..e5981d191 100644 --- a/packages/markdown-preview/lib/main.js +++ b/packages/markdown-preview/lib/main.js @@ -18,7 +18,7 @@ module.exports = { this.style = new CSSStyleSheet() - // When we upgrade Electron, we can push onto `adoptedStyleSheets` + // TODO: When we upgrade Electron, we can push onto `adoptedStyleSheets` // directly. For now, we have to do this silly thing. let styleSheets = Array.from(document.adoptedStyleSheets ?? []) styleSheets.push(this.style) From 90ce7dc211236878c1d5de49c8d214ffe608d338 Mon Sep 17 00:00:00 2001 From: savetheclocktower Date: Wed, 15 May 2024 03:25:37 +0000 Subject: [PATCH 23/31] GH Action Documentation --- yarn.lock | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/yarn.lock b/yarn.lock index 5382fb9d1..fbe83b620 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6654,6 +6654,7 @@ markdown-it@^13.0.2: fs-plus "^3.0.0" github-markdown-css "^5.5.1" marked "5.0.3" + morphdom "^2.7.2" underscore-plus "^1.0.0" yaml-front-matter "^4.1.1" @@ -6972,6 +6973,11 @@ moment@^2.19.3: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== +morphdom@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/morphdom/-/morphdom-2.7.2.tgz#d48a87254f9b3031c0e1ec367736721fbaf22167" + integrity sha512-Dqb/lHFyTi7SZpY0a5R4I/0Edo+iPMbaUexsHHsLAByyixCDiLHPHyVoKVmrpL0THcT7V9Cgev9y21TQYq6wQg== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" From 9afc0948c1f6d603cf231fefc8794bc470ac7c63 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 15 May 2024 19:18:38 -0700 Subject: [PATCH 24/31] Add new changelog --- CHANGELOG.md | 25 ++++++++++++++++++++++++- packages/welcome/lib/changelog-view.js | 14 +++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69dfb1f6a..723748e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,29 @@ ## [Unreleased] +## 1.117.0 + +* [markdown-preview] Improve rendering performance in preview panes, especially in documents with lots of fenced code blocks. +* [markdown-preview] GitHub-style Markdown preview now uses up-to-date styles and supports dark mode. +* Pulsar's OS level theme will now change according to the selected editor theme if `core.syncWindowThemeWithPulsarTheme` is enabled. +* [language-sass] Add SCSS Tree-sitter grammar. +* [language-ruby] Update to latest Tree-sitter Ruby parser. +* [language-gfm] Make each block-level HTML tag its own injection. +* [language-typescript] More highlighting fixes, especially for operators. + +### Pulsar +- Fixed: CI: Fix workaround for Homebrew node in Cirrus on macOS [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/1002) +- Added: [markdown-preview] Optimize re-rendering of content in a preview pane especially syntax highlighting [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/984) +- Fixed: Tree-sitter rolling fixes, 1.117 edition [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/974) +- Updated: Update Renovate preset name [@HonkingGoose](https://github.com/pulsar-edit/pulsar/pull/1000) +- Added: Debugging when a package service is incorrect [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/995) +- Added: Bundle snippets [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/993) +- Fixed: CI: Pin to macOS 12 runner images instead of macos-latest (GitHub Actions) [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/997) +- Added: [markdown-preview] Add dark mode for GitHub-style preview [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/973) +- Added: Change Window Theme with Pulsar Theme [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/545) +- Updated: CI: Upgrade or replace all deprecated GH Actions [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/983) +- Fixed: [language-clojure] Stop detecting `.org` files as `.language-clojure` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/980) + ## 1.116.0 * Added `TextEditor::getCommentDelimitersForBufferPosition` for retrieving comment delimiter strings appropriate for a given buffer position. This allows us to support three new snippet variables: `LINE_COMMENT`, `BLOCK_COMMENT_START`, and `BLOCK_COMMENT_END`. @@ -14,7 +37,7 @@ * Replaced our underlying Tree-sitter parser for Markdown files with one that’s more stable. * Fixed issues in Python with unwanted indentation after type annotations and applying scope names to constructor functions. * Removed Machine PATH handling for Pulsar on Windows, ensuring to only ever attempt PATH manipulation per user. Added additional safety mechanisms when handling a user's PATH variable. -* Update (Linux) metainfo from downstream Pulsar Flatpak +* Update (Linux) metainfo from downstream Pulsar Flatpak ### Pulsar - Updated: Update Pulsar's Linux desktop & metainfo mostly from Flatpak [@cat-master21](https://github.com/pulsar-edit/pulsar/pull/935) diff --git a/packages/welcome/lib/changelog-view.js b/packages/welcome/lib/changelog-view.js index 27cc64672..d3e9d73e2 100644 --- a/packages/welcome/lib/changelog-view.js +++ b/packages/welcome/lib/changelog-view.js @@ -50,25 +50,25 @@ export default class ChangeLogView {

Feel free to read our Full Change Log.

  • - Added TextEditor::getCommentDelimitersForBufferPosition for retrieving comment delimiter strings appropriate for a given buffer position. This allows us to support three new snippet variables: LINE_COMMENT, BLOCK_COMMENT_START, and BLOCK_COMMENT_END. + [markdown-preview] Improve rendering performance in preview panes, especially in documents with lots of fenced code blocks.
  • - Added ability to use “simple” transformation flags in snippets (like /upcase and /camelcase) within sed-style snippet transformation replacements. + [markdown-preview] GitHub-style Markdown preview now uses up-to-date styles and supports dark mode.
  • - Improved TypeScript syntax highlighting of regular expressions, TSX fragments, wildcard export identifiers, namespaced types, and template string punctuation. + Pulsar's OS level theme will now change according to the selected editor theme if core.syncWindowThemeWithPulsarTheme is enabled.
  • - Replaced our underlying Tree-sitter parser for Markdown files with one that’s more stable. + [language-sass] Add SCSS Tree-sitter grammar.
  • - Fixed issues in Python with unwanted indentation after type annotations and applying scope names to constructor functions. + [language-ruby] Update to latest Tree-sitter Ruby parser.
  • - Removed Machine PATH handling for Pulsar on Windows, ensuring to only ever attempt PATH manipulation per user. Added additional safety mechanisms when handling a user's PATH variable. + [language-gfm] Make each block-level HTML tag its own injection.
  • - Update (Linux) metainfo from downstream Pulsar Flatpak + [language-typescript] More highlighting fixes, especially for operators.
From 19195968670261bb1c4a6d25834f3a96083786d5 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 15 May 2024 19:31:41 -0700 Subject: [PATCH 25/31] Update Cirrus CI Token --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 34c84e1dc..b1a51c9d8 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,6 +1,6 @@ env: PYTHON_VERSION: 3.12 - GITHUB_TOKEN: ENCRYPTED[!c394f11378a8bc92ff1b05662ee3e574fc662692e45f0a048aa8cab42fb072b039d83f68fd6953f470af51846063ce46!] + GITHUB_TOKEN: ENCRYPTED[!9e497dd40c7819a1ddd425d718f50f539f4e080143e20518fc8398b117632551aae72e2680299a57c2be9f0c15a9d2f7!] # The above token, is a GitHub API Token, that allows us to download RipGrep without concern of API limits # linux_task: From c2ecd2ed4565f5e5363e9fafa9185c1216808a96 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 15 May 2024 19:32:47 -0700 Subject: [PATCH 26/31] Add new changelog item --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 723748e57..d137f16a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * [language-typescript] More highlighting fixes, especially for operators. ### Pulsar +- Updated: [ci] Update Cirrus CI Token [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/1006) - Fixed: CI: Fix workaround for Homebrew node in Cirrus on macOS [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/1002) - Added: [markdown-preview] Optimize re-rendering of content in a preview pane especially syntax highlighting [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/984) - Fixed: Tree-sitter rolling fixes, 1.117 edition [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/974) From 8cbeb16f8e8c5d1b0584c41533247a068452a672 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 15 May 2024 19:41:09 -0700 Subject: [PATCH 27/31] Update `package.json` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 45c899cb7..bb489091c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "pulsar", "author": "Pulsar-Edit ", "productName": "Pulsar", - "version": "1.116.0-dev", + "version": "1.117.0", "description": "A Community-led Hyper-Hackable Text Editor", "branding": { "id": "pulsar", From 1b44be3dacd702fffebf66c86244379b41d8331f Mon Sep 17 00:00:00 2001 From: DeeDeeG Date: Wed, 15 May 2024 23:40:18 -0400 Subject: [PATCH 28/31] Cirrus: Fix `gem install fpm` on ARM Linux Apparently we need `ruby-dev` (development headers) package to build fpm gem's native extensions now?! Well, it's an easy fix. Not sure why we didn't need this up until now, but oh, well. --- .cirrus.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.cirrus.yml b/.cirrus.yml index b1a51c9d8..97c258c28 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -72,6 +72,7 @@ arm_linux_task: procps curl ruby + ruby-dev rpm build-essential git From c8fe9fed9d4a825257f2deac5f90fdcb3e80aa3f Mon Sep 17 00:00:00 2001 From: DeeDeeG Date: Thu, 16 May 2024 00:53:07 -0400 Subject: [PATCH 29/31] Changelog: Last-minute entry for PR 1008 (Cirrus ARM Linux fix) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d137f16a6..db0e41216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * [language-typescript] More highlighting fixes, especially for operators. ### Pulsar +- Fixed: Cirrus: Fix gem install fpm on ARM Linux [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/1008) - Updated: [ci] Update Cirrus CI Token [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/1006) - Fixed: CI: Fix workaround for Homebrew node in Cirrus on macOS [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/1002) - Added: [markdown-preview] Optimize re-rendering of content in a preview pane especially syntax highlighting [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/984) From 70f1c2e8ff3f1c73bfa982183bb07d5fbaa461fe Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 16 May 2024 21:13:44 -0700 Subject: [PATCH 30/31] Add back `-dev` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb489091c..f48eef440 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "pulsar", "author": "Pulsar-Edit ", "productName": "Pulsar", - "version": "1.117.0", + "version": "1.117.0-dev", "description": "A Community-led Hyper-Hackable Text Editor", "branding": { "id": "pulsar", From 24e8a8c07ac9cc662c6c5b748b2dedfa9fa468f7 Mon Sep 17 00:00:00 2001 From: DeeDeeG Date: Mon, 20 May 2024 15:19:47 -0400 Subject: [PATCH 31/31] Cirrus: Update Rolling upload token The old token had expired and needed to be regenerated. --- .cirrus.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 97c258c28..b45846d72 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -63,7 +63,7 @@ arm_linux_task: memory: 8G env: USE_SYSTEM_FPM: 'true' - ROLLING_UPLOAD_TOKEN: ENCRYPTED[f935c396a9f4bca108ec2fdedb00dbc9be2f4c411f100d577acdab42db59ea134be059ce8535396db8222a2b1eb68c27] + ROLLING_UPLOAD_TOKEN: ENCRYPTED[b318d12e0d41475229383e74c3a98b3921024096b066d9f535ca42bfe025558eca69d1e0f56413abedcf2d5817615c4c] prepare_script: - apt-get update - export DEBIAN_FRONTEND="noninteractive" @@ -132,7 +132,7 @@ silicon_mac_task: APPLEID: ENCRYPTED[549ce052bd5666dba5245f4180bf93b74ed206fe5e6e7c8f67a8596d3767c1f682b84e347b326ac318c62a07c8844a57] APPLEID_PASSWORD: ENCRYPTED[774c3307fd3b62660ecf5beb8537a24498c76e8d90d7f28e5bc816742fd8954a34ffed13f9aa2d1faf66ce08b4496e6f] TEAM_ID: ENCRYPTED[11f3fedfbaf4aff1859bf6c105f0437ace23d84f5420a2c1cea884fbfa43b115b7834a463516d50cb276d4c4d9128b49] - ROLLING_UPLOAD_TOKEN: ENCRYPTED[f935c396a9f4bca108ec2fdedb00dbc9be2f4c411f100d577acdab42db59ea134be059ce8535396db8222a2b1eb68c27] + ROLLING_UPLOAD_TOKEN: ENCRYPTED[b318d12e0d41475229383e74c3a98b3921024096b066d9f535ca42bfe025558eca69d1e0f56413abedcf2d5817615c4c] prepare_script: - brew update - brew uninstall node@20