diff --git a/.cirrus.yml b/.cirrus.yml index c4ba42d30..b45846d72 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: @@ -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" @@ -72,6 +72,7 @@ arm_linux_task: procps curl ruby + ruby-dev rpm build-essential git @@ -131,10 +132,10 @@ 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 + - brew uninstall node@20 - brew install git python@$PYTHON_VERSION python-setuptools - git submodule init - git submodule update @@ -199,7 +200,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 +# - 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 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" }, 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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ad761ce..6b1023709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,31 @@ better support for "def" elements (example - don't syntax `default` or `definition` as a `def`, but highlights `p/defresolver`) +## 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: 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) +- 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`. @@ -26,7 +51,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/package.json b/package.json index c48ff6149..f48eef440 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-dev", "description": "A Community-led Hyper-Hackable Text Editor", "branding": { "id": "pulsar", @@ -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..f24453c50 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/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 }); 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 c4818c3ce..bfca703c3 100755 Binary files a/packages/language-ruby/grammars/tree-sitter-ruby/tree-sitter-ruby.wasm and b/packages/language-ruby/grammars/tree-sitter-ruby/tree-sitter-ruby.wasm differ 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..60d69c021 --- /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#090d25a5fc829ce6956201cf55ab6b6eacad999c' + 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 000000000..6ed858067 Binary files /dev/null and b/packages/language-sass/grammars/tree-sitter/tree-sitter-scss.wasm differ 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/" 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 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_ diff --git a/packages/markdown-preview/lib/main.js b/packages/markdown-preview/lib/main.js index 0b87479b7..e5981d191 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() + + // 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) + 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..52586c106 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() } @@ -270,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 })) @@ -309,18 +328,34 @@ 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.element.classList.remove('loading') + this.emitter.emit('did-change-markdown') this.element.scrollTop = scrollTop } catch (error) { @@ -400,7 +435,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') @@ -413,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) @@ -425,11 +461,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/lib/renderer.js b/packages/markdown-preview/lib/renderer.js index 636ad68c5..342c7751f 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,89 @@ 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() + 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 +367,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-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/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..8e44e81b6 100644 --- a/packages/markdown-preview/styles/markdown-preview.less +++ b/packages/markdown-preview/styles/markdown-preview.less @@ -2,6 +2,14 @@ // 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. + atom-text-editor + pre { + display: none; + } + atom-text-editor { // only show scrollbars on hover .scrollbars-visible-always & { @@ -28,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; } } 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..e68decb79 --- /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/pulsar", + "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'); + }); +}); 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.

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/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}`) } } } 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 diff --git a/src/wasm-tree-sitter-language-mode.js b/src/wasm-tree-sitter-language-mode.js index d7f112763..a893985d0 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)); diff --git a/yarn.lock b/yarn.lock index 7fbdb5877..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" @@ -8650,9 +8656,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"